-
Notifications
You must be signed in to change notification settings - Fork 17
perf: bulk-copy bitpacked streams instead of element-wise decode #487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Byte->word conversion of the little-endian streams. | ||
| // | ||
| // On little-endian hosts the in-memory representation of []uint32/[]uint64 is | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please clean up the comment. That's a typical huge claude comment explaining trivial things. And there a lot of questionable numbers and statements as well. |
||
| // exactly the on-disk little-endian stream, so the conversion is a bulk copy | ||
| // (runtime.memmove) instead of an element-wise decode loop. The loop tops out | ||
| // at ~4.5 GiB/s regardless of size (instruction-bound); memmove runs at | ||
| // memory/cache bandwidth (30-70 GiB/s), see copy_uints_test.go benchmarks. | ||
| // For the smallest inputs (streams of a couple of words: offsets arrays of | ||
| // blocks dominated by one heavy token, fraction-tail blocks) the memmove call | ||
| // overhead loses to a plain loop, so those stay element-wise. | ||
| // | ||
| // Big-endian hosts use the element-wise fallback in copy_uints_be.go. | ||
|
|
||
| //go:build 386 || amd64 || arm || arm64 || loong64 || mipsle || mips64le || ppc64le || riscv64 || wasm | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have odd feelings about introducing build tags for this small optimization. Initially I did this, it just didn't get into main branch: 22a3e36#diff-aab3afc5841441065dc926e0dccdfca3010a8eb081ccc5c04bb394f902bde1d3R17 I though it's easier to just check if host is LE and then use if branch. The main problem I see with the proposed solution is I don't know much about these platforms and build tags look like overkill for this kind of micro-optimization. I also looked through arrow-go library, haven't found any build tags, but What are you thoughts? |
||
|
|
||
| package packer | ||
|
|
||
| import ( | ||
| "encoding/binary" | ||
| "fmt" | ||
| "unsafe" | ||
| ) | ||
|
|
||
| // smallCopyWords: at or below this the plain loop beats the memmove call | ||
| // overhead. Measured crossover (BenchmarkSmallCopyCrossover, Cascade Lake): | ||
| // the loop wins at 1-2 words, ~ties at 3, memmove wins from 4 on (-15%) | ||
| // and widens quickly (-48% at 8 words). | ||
| const smallCopyWords = 3 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can remove this |
||
|
|
||
| // copyAsUints32 reinterprets dst as bytes and bulk-copies src into it. | ||
| // Panics if len(src) is not a multiple of the word size — same fail-fast | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same for comments, please simplify. |
||
| // behavior on a corrupted stream as the element-wise decode had; callers | ||
| // validate the length beforehand. | ||
| func copyAsUints32(src []byte, dst []uint32) []uint32 { | ||
| if len(src)%sizeOfUint32 != 0 { | ||
| panic(fmt.Sprintf("packer: ragged uint32 stream: %d bytes", len(src))) | ||
| } | ||
| n := len(src) / sizeOfUint32 | ||
| if cap(dst) < n { | ||
| dst = make([]uint32, n) | ||
| } | ||
| dst = dst[:n] | ||
| if n == 0 { | ||
| return dst | ||
| } | ||
| if n <= smallCopyWords { | ||
| for i := range dst { | ||
| dst[i] = binary.LittleEndian.Uint32(src[i*sizeOfUint32:]) | ||
| } | ||
| return dst | ||
| } | ||
| copy(unsafe.Slice((*byte)(unsafe.Pointer(&dst[0])), n*sizeOfUint32), src) | ||
| return dst | ||
| } | ||
|
|
||
| // copyAsUints64 reinterprets dst as bytes and bulk-copies src into it. | ||
| // Panics if len(src) is not a multiple of the word size (see copyAsUints32). | ||
| func copyAsUints64(src []byte, dst []uint64) []uint64 { | ||
| if len(src)%8 != 0 { | ||
| panic(fmt.Sprintf("packer: ragged uint64 stream: %d bytes", len(src))) | ||
| } | ||
| n := len(src) / 8 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if cap(dst) < n { | ||
| dst = make([]uint64, n) | ||
| } | ||
| dst = dst[:n] | ||
| if n == 0 { | ||
| return dst | ||
| } | ||
| if n <= smallCopyWords { | ||
| for i := range dst { | ||
| dst[i] = binary.LittleEndian.Uint64(src[i*8:]) | ||
| } | ||
| return dst | ||
| } | ||
| copy(unsafe.Slice((*byte)(unsafe.Pointer(&dst[0])), n*8), src) | ||
| return dst | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Big-endian fallback: element-wise little-endian decode with the same | ||
| // contract as the bulk-copy variant (including the ragged-length panic). | ||
| // No release target or CI runs big-endian, so this path is not exercised | ||
| // by any automated build. | ||
|
|
||
| //go:build !(386 || amd64 || arm || arm64 || loong64 || mipsle || mips64le || ppc64le || riscv64 || wasm) | ||
|
|
||
| package packer | ||
|
|
||
| import ( | ||
| "encoding/binary" | ||
| "fmt" | ||
| ) | ||
|
|
||
| func copyAsUints32(src []byte, dst []uint32) []uint32 { | ||
| if len(src)%sizeOfUint32 != 0 { | ||
| panic(fmt.Sprintf("packer: ragged uint32 stream: %d bytes", len(src))) | ||
| } | ||
| n := len(src) / sizeOfUint32 | ||
| if cap(dst) < n { | ||
| dst = make([]uint32, n) | ||
| } | ||
| dst = dst[:n] | ||
| for i := range dst { | ||
| dst[i] = binary.LittleEndian.Uint32(src[i*sizeOfUint32:]) | ||
| } | ||
| return dst | ||
| } | ||
|
|
||
| func copyAsUints64(src []byte, dst []uint64) []uint64 { | ||
| if len(src)%8 != 0 { | ||
| panic(fmt.Sprintf("packer: ragged uint64 stream: %d bytes", len(src))) | ||
| } | ||
| n := len(src) / 8 | ||
| if cap(dst) < n { | ||
| dst = make([]uint64, n) | ||
| } | ||
| dst = dst[:n] | ||
| for i := range dst { | ||
| dst[i] = binary.LittleEndian.Uint64(src[i*8:]) | ||
| } | ||
| return dst | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file named
copy_uints.go. Do you think it's a good name for this particular file? I'd personally put it somewhere toutilspackage and namedcopy_utils.go