This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 0a73a0b3 perf(compute): use stack scratch for null propagation (#1229)
0a73a0b3 is described below

commit 0a73a0b32dc99ea7cb043a15bb0a40266574a6bc
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 28 23:38:29 2026 +0200

    perf(compute): use stack scratch for null propagation (#1229)
    
    ## What
    
    - Use an 8-entry stack scratch array for the common input counts in
    `propagateNulls`.
    - Keep an exact-capacity heap fallback for larger batches.
    
    ## Benchmark
    
    Apple M1 Pro, Go 1.26.3. Representative results from `go test -vet=off
    ./arrow/compute -run "^$" -bench "^BenchmarkPropagateNullsScratch$"
    -benchmem -benchtime=400ms -count=3 -cpu=1`.
    
    | Case | Before | After |
    | --- | ---: | ---: |
    | nullable/5 | 138 ns/op, 48 B/op, 1 alloc | 95 ns/op, 0 B/op, 0 allocs
    |
    | nullable/8 | 221 ns/op, 64 B/op, 1 alloc | 166 ns/op, 0 B/op, 0 allocs
    |
    | nullable/9 | 255 ns/op, 80 B/op, 1 alloc | 256 ns/op, 80 B/op, 1 alloc
    |
    
    ## Tests
    
    - `go test -vet=off ./arrow/compute -count=1`
    - `PARQUET_TEST_DATA=parquet-testing/data go test ./... -count=1`
---
 arrow/compute/executor.go                       |  10 +-
 arrow/compute/propagate_nulls_benchmark_test.go | 132 ++++++++++++++++++++++++
 2 files changed, 139 insertions(+), 3 deletions(-)

diff --git a/arrow/compute/executor.go b/arrow/compute/executor.go
index c32c9768..c9e4c2ef 100644
--- a/arrow/compute/executor.go
+++ b/arrow/compute/executor.go
@@ -246,10 +246,14 @@ func propagateNulls(ctx *exec.KernelCtx, batch 
*exec.ExecSpan, out *exec.ArraySp
                return fmt.Errorf("%w: can only propagate nulls into 
pre-allocated memory when output offset is non-zero", arrow.ErrInvalid)
        }
 
-       var (
+       var scratch [8]*exec.ArraySpan
+       arrsWithNulls := scratch[:0]
+       if len(batch.Values) > len(scratch) {
                arrsWithNulls = make([]*exec.ArraySpan, 0, len(batch.Values))
-               isAllNull     bool
-               prealloc      = out.Buffers[0].Buf != nil
+       }
+       var (
+               isAllNull bool
+               prealloc  = out.Buffers[0].Buf != nil
        )
 
        for i := range batch.Values {
diff --git a/arrow/compute/propagate_nulls_benchmark_test.go 
b/arrow/compute/propagate_nulls_benchmark_test.go
new file mode 100644
index 00000000..e81d0b74
--- /dev/null
+++ b/arrow/compute/propagate_nulls_benchmark_test.go
@@ -0,0 +1,132 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build go1.24
+
+package compute
+
+import (
+       "context"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/compute/exec"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+)
+
+func BenchmarkPropagateNullsScratch(b *testing.B) {
+       cases := []struct {
+               name          string
+               inputCount    int
+               nullableInput bool
+       }{
+               {name: "all-valid/1", inputCount: 1},
+               {name: "all-valid/2", inputCount: 2},
+               {name: "nullable/1", inputCount: 1, nullableInput: true},
+               {name: "nullable/2", inputCount: 2, nullableInput: true},
+               {name: "nullable/4", inputCount: 4, nullableInput: true},
+               {name: "nullable/5", inputCount: 5, nullableInput: true},
+               {name: "nullable/8", inputCount: 8, nullableInput: true},
+               {name: "nullable/9", inputCount: 9, nullableInput: true},
+       }
+
+       for _, tc := range cases {
+               b.Run(tc.name, func(b *testing.B) {
+                       batch := newPropagateNullsBenchmarkBatch(tc.inputCount, 
tc.nullableInput)
+                       out := newPropagateNullsBenchmarkOutput()
+                       ctx := &exec.KernelCtx{}
+
+                       b.ReportAllocs()
+                       b.ResetTimer()
+                       for b.Loop() {
+                               if err := propagateNulls(ctx, batch, out); err 
!= nil {
+                                       b.Fatal(err)
+                               }
+                       }
+               })
+       }
+}
+
+func newPropagateNullsBenchmarkBatch(inputCount int, nullableInput bool) 
*exec.ExecSpan {
+       const length int64 = 64
+
+       batch := &exec.ExecSpan{
+               Len:    length,
+               Values: make([]exec.ExecValue, inputCount),
+       }
+       bitmap := []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
+       for i := range batch.Values {
+               batch.Values[i].Array.Type = arrow.PrimitiveTypes.Int32
+               batch.Values[i].Array.Len = length
+               if nullableInput {
+                       batch.Values[i].Array.Nulls = 1
+                       batch.Values[i].Array.Buffers[0].Buf = bitmap
+               }
+       }
+       return batch
+}
+
+func newPropagateNullsBenchmarkOutput() *exec.ArraySpan {
+       out := &exec.ArraySpan{
+               Type: arrow.PrimitiveTypes.Int32,
+               Len:  64,
+       }
+       out.Buffers[0].Buf = make([]byte, 8)
+       return out
+}
+
+func BenchmarkPropagateNullsSmallArrayAdd(b *testing.B) {
+       const length = 16
+       values := make([]int32, length)
+       valid := make([]bool, length)
+       for i := range valid {
+               valid[i] = i != 0
+       }
+
+       leftBuilder := array.NewInt32Builder(memory.DefaultAllocator)
+       leftBuilder.AppendValues(values, valid)
+       left := leftBuilder.NewInt32Array()
+       leftBuilder.Release()
+       defer left.Release()
+
+       rightBuilder := array.NewInt32Builder(memory.DefaultAllocator)
+       rightBuilder.AppendValues(values, nil)
+       right := rightBuilder.NewInt32Array()
+       rightBuilder.Release()
+       defer right.Release()
+
+       leftDatum := NewDatum(left)
+       defer leftDatum.Release()
+       rightDatum := NewDatum(right)
+       defer rightDatum.Release()
+
+       ctx := SetExecCtx(context.Background(), ExecCtx{
+               Registry:           GetFunctionRegistry(),
+               ChunkSize:          length,
+               PreallocContiguous: true,
+       })
+
+       b.ReportAllocs()
+       b.ResetTimer()
+       for b.Loop() {
+               out, err := CallFunction(ctx, "add", nil, leftDatum, rightDatum)
+               if err != nil {
+                       b.Fatal(err)
+               }
+               out.Release()
+       }
+}

Reply via email to