Skip to content

perf: optimize compute.Take for fewer memory allocations - #557

Merged
zeroshade merged 5 commits into
apache:mainfrom
hamilton-earthscope:perf
Oct 31, 2025
Merged

perf: optimize compute.Take for fewer memory allocations#557
zeroshade merged 5 commits into
apache:mainfrom
hamilton-earthscope:perf

Conversation

@hamilton-earthscope

@hamilton-earthscope hamilton-earthscope commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

When writing data to partitioned Iceberg tables using iceberg-go's new partitioned write capability, I found that Arrow's compute.Take operation was causing the writes to be significantly slower than unpartitioned writes.

Causes:

  • the VarBinaryImpl function here in arrow-go was pre-allocating the data buffer with only meanValueLen (size for just one string/buffer), not the total size needed for all strings. For, my use case (writing 100k rows at a time) this was causing 15x buffer reallocations as the buffer incrementally scales up. For my iceberg table with 20+ string/binary cols of various size this is significant overhead.
  • when VarBinaryImpl allocates additional space for new items, it would only add the exact amount needed for the new value. This caused O(n) reallocations.

What changes are included in this PR?

  1. Pre-allocate upfront with a better estimate of the necessary buffer size to eliminate repeated reallocations.

    I somewhat arbitrarily chose a cap of 16MB as a guess at what would be effective at reducing the number of allocations but also trying to be cognizant of the library being used in many bespoke scenarios and not wanting to make massive memory spikes. For my use case, I never hit this 16MB threshold and it could be smaller.

    I am curious for your input on whether there should be a cap at all or what a reasonable cap would be.

  2. Use exponential growth for additional allocations for O(log n) total reallocations.

Are these changes tested?

No unit tests.

However, with a dedicated reproducing script that mimics my use case with different write sizes (on a macbook pro m3 max + 64GB ram) average of 3 table.Append() calls:

Configuration 100k rows 500k rows 1M rows 2.5M rows 10M rows
Before 4.1s 53.3s didn't try didn't try didn't try
Change 1 336ms 2.41s 8.75s cancelled after 5mins didn't try
Change 2 227ms 897ms 1.72s 4.10s 17.1s

Are there any user-facing changes?

No; just more performant

@hamilton-earthscope hamilton-earthscope changed the title perf: prealloc data builder with estimated total output size Oct 30, 2025

@zeroshade zeroshade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a benchmark test to compute/vector_selection_test.go for future reference?

Comment thread arrow/compute/internal/kernels/vector_selection.go Outdated
@hamilton-earthscope

Copy link
Copy Markdown
Contributor Author

on main:

go test -bench=BenchmarkTakeString -benchmem -benchtime=3x -run=^$ -timeout 30m
goos: darwin
goarch: arm64
pkg: github.com/apache/arrow-go/v18/arrow/compute
cpu: Apple M3 Max
BenchmarkTakeString/SmallBatch_ShortStrings-16                 3            139750 ns/op           7163478 rows/sec       835890 B/op         198 allocs/op
BenchmarkTakeString/MediumBatch_ShortStrings-16                3          13052111 ns/op            766200 rows/sec     98296896 B/op        1762 allocs/op
BenchmarkTakeString/LargeBatch_ShortStrings-16                 3         193090167 ns/op            258947 rows/sec     2746571538 B/op      9389 allocs/op
BenchmarkTakeString/XLargeBatch_ShortStrings-16                3         564450403 ns/op            177164 rows/sec     11119508282 B/op    18947 allocs/op
BenchmarkTakeString/SmallBatch_MediumStrings-16                3           3269042 ns/op            305968 rows/sec     21126634 B/op         826 allocs/op
BenchmarkTakeString/MediumBatch_MediumStrings-16               3         198658722 ns/op             50338 rows/sec     2055054277 B/op      8127 allocs/op
BenchmarkTakeString/LargeBatch_MediumStrings-16                3        2286152306 ns/op             21871 rows/sec     52530632896 B/op    41554 allocs/op
BenchmarkTakeString/XLargeBatch_MediumStrings-16               3        8208929458 ns/op             12182 rows/sec     210687783749 B/op           83644 allocs/op
BenchmarkTakeString/LargeBatch_ShortStrings_Large-16           3         183346069 ns/op            272709 rows/sec     2746760874 B/op      9312 allocs/op
BenchmarkTakeString/XLargeBatch_MediumStrings_Large-16         3        8180564000 ns/op             12224 rows/sec     210688177797 B/op           83568 allocs/op
BenchmarkTakeStringPartitionPattern-16                         3         378207430 ns/op            132203 rows/sec     6315631330 B/op     14193 allocs/op
PASS
ok      github.com/apache/arrow-go/v18/arrow/compute    81.473s

this PR:

go test -bench=BenchmarkTakeString -benchmem -benchtime=3x -run=^$ -timeout 30m
goos: darwin
goarch: arm64
pkg: github.com/apache/arrow-go/v18/arrow/compute
cpu: Apple M3 Max
BenchmarkTakeString/SmallBatch_ShortStrings-16                 3             42167 ns/op          23920202 rows/sec        17906 B/op          44 allocs/op
BenchmarkTakeString/MediumBatch_ShortStrings-16                3            156903 ns/op          63869468 rows/sec       160456 B/op          44 allocs/op
BenchmarkTakeString/LargeBatch_ShortStrings-16                 3            760750 ns/op          65783472 rows/sec       805077 B/op          43 allocs/op
BenchmarkTakeString/XLargeBatch_ShortStrings-16                3           1148056 ns/op          87144906 rows/sec      1614917 B/op          43 allocs/op
BenchmarkTakeString/SmallBatch_MediumStrings-16                3             29931 ns/op          33707865 rows/sec        64749 B/op          42 allocs/op
BenchmarkTakeString/MediumBatch_MediumStrings-16               3            229431 ns/op          43689299 rows/sec       562298 B/op          42 allocs/op
BenchmarkTakeString/LargeBatch_MediumStrings-16                3            705195 ns/op          70951339 rows/sec      2812080 B/op          42 allocs/op
BenchmarkTakeString/XLargeBatch_MediumStrings-16               3           1352417 ns/op          73981205 rows/sec      5612592 B/op          42 allocs/op
BenchmarkTakeString/LargeBatch_ShortStrings_Large-16           3            571167 ns/op          87591241 rows/sec      1001650 B/op          42 allocs/op
BenchmarkTakeString/XLargeBatch_MediumStrings_Large-16         3           1353181 ns/op          73933345 rows/sec      6014005 B/op          42 allocs/op
BenchmarkTakeStringPartitionPattern-16                         3            650014 ns/op          76983953 rows/sec      1312954 B/op          42 allocs/op
PASS
ok      github.com/apache/arrow-go/v18/arrow/compute    0.585s
@zeroshade
zeroshade merged commit f657566 into apache:main Oct 31, 2025
26 of 27 checks passed
zeroshade pushed a commit that referenced this pull request Nov 14, 2025
### Rationale for this change

This PR implements the same allocation improvements as
#557 but for the Take list
kernel.

### What changes are included in this PR?

- Pre-allocate upfront with a better estimate of the necessary buffer
size to eliminate repeated reallocations.
- Use exponential growth for additional allocations for O(log n) total
reallocations.

### Are these changes tested?

New benchmarks added for take list kernel

### Are there any user-facing changes?

No

### Performance Comparison

on `main`

```
goos: darwin
goarch: arm64
pkg: github.com/apache/arrow-go/v18/arrow/compute
cpu: Apple M3 Max
BenchmarkTakeList/SmallBatch_ShortLists-16         	    2320	     555889 ns/op	   1798923 rows/sec	     3355390 B/op	     396 allocs/op
BenchmarkTakeList/MediumBatch_ShortLists-16        	      31	   37600434 ns/op	    265955 rows/sec	   324238694 B/op	    3214 allocs/op
BenchmarkTakeList/LargeBatch_ShortLists-16         	       2	  653682896 ns/op	     76490 rows/sec	  7877124660 B/op	   15751 allocs/op
BenchmarkTakeList/XLargeBatch_ShortLists-16        	       1	 2019873959 ns/op	     49508 rows/sec	 31380868976 B/op	   31489 allocs/op
BenchmarkTakeList/SmallBatch_MediumLists-16        	     334	    3435205 ns/op	    291104 rows/sec	    43059334 B/op	    1086 allocs/op
BenchmarkTakeList/MediumBatch_MediumLists-16       	       5	  230308533 ns/op	     43420 rows/sec	  4041679715 B/op	   10098 allocs/op
BenchmarkTakeList/LargeBatch_MediumLists-16        	       1	 4600489959 ns/op	     10868 rows/sec	100213267960 B/op	   50328 allocs/op
BenchmarkTakeList/XLargeBatch_MediumLists-16       	       1	16462343792 ns/op	      6074 rows/sec	400427784144 B/op	  101032 allocs/op
BenchmarkTakeList/LargeBatch_ShortLists_Large-16   	       1	 1682293042 ns/op	     29721 rows/sec	 31379855376 B/op	   31422 allocs/op
BenchmarkTakeList/XLargeBatch_MediumLists_Large-16 	       1	33800353917 ns/op	      2959 rows/sec	800433389776 B/op	  102480 allocs/op
BenchmarkTakeListPartitionPattern-16               	       1	 3003105000 ns/op	     16649 rows/sec	 69581474288 B/op	   46479 allocs/op
PASS
ok  	github.com/apache/arrow-go/v18/arrow/compute	71.015s
```

on this branch

```
goos: darwin
goarch: arm64
pkg: github.com/apache/arrow-go/v18/arrow/compute
cpu: Apple M3 Max
BenchmarkTakeList/SmallBatch_ShortLists-16         	   25522	     46138 ns/op	  21674062 rows/sec	   50668 B/op	      83 allocs/op
BenchmarkTakeList/MediumBatch_ShortLists-16        	    3792	    316046 ns/op	  31641022 rows/sec	  457558 B/op	      83 allocs/op
BenchmarkTakeList/LargeBatch_ShortLists-16         	     804	   1521240 ns/op	  32867968 rows/sec	 2232578 B/op	      84 allocs/op
BenchmarkTakeList/XLargeBatch_ShortLists-16        	     416	   2832247 ns/op	  35307705 rows/sec	 4435314 B/op	      84 allocs/op
BenchmarkTakeList/SmallBatch_MediumLists-16        	    9444	    125321 ns/op	   7979591 rows/sec	  173603 B/op	      83 allocs/op
BenchmarkTakeList/MediumBatch_MediumLists-16       	    1176	    999217 ns/op	  10007857 rows/sec	 1653926 B/op	      83 allocs/op
BenchmarkTakeList/LargeBatch_MediumLists-16        	     232	   4913249 ns/op	  10176590 rows/sec	 8229752 B/op	      85 allocs/op
BenchmarkTakeList/XLargeBatch_MediumLists-16       	     128	   9309120 ns/op	  10742230 rows/sec	16428821 B/op	      85 allocs/op
BenchmarkTakeList/LargeBatch_ShortLists_Large-16   	     739	   1560044 ns/op	  32050453 rows/sec	 3428837 B/op	      84 allocs/op
BenchmarkTakeList/XLargeBatch_MediumLists_Large-16 	     122	   9712600 ns/op	  10295969 rows/sec	24834215 B/op	      85 allocs/op
BenchmarkTakeListPartitionPattern-16               	      96	  11756706 ns/op	   4252901 rows/sec	18421151 B/op	      98 allocs/op
PASS
ok  	github.com/apache/arrow-go/v18/arrow/compute	17.869s

```
zeroshade pushed a commit that referenced this pull request Dec 1, 2025
### Rationale for this change

Arrow Go is lacking a Take kernel for Map types which means from
`iceberg-go` we cannot write to partitioned Iceberg tables containing
columns with Map types.

### What changes are included in this PR?

- Adds a new Take kernel for Map types
  - the same allocation behavior from #557 and #573 is used

### Are these changes tested?

Yes.


### Are there any user-facing changes?

* Can now use Take on Arrow schemas with Map columns.
* Can now write to partitioned Iceberg tables using `arrow-go`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants