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 372e1eab perf(arrow/ipc): release dictionary payloads eagerly (#1259)
372e1eab is described below

commit 372e1eabeb9d8db3962e75219049431677528083
Author: Minh Vu <[email protected]>
AuthorDate: Mon Aug 31 22:38:59 2026 +0200

    perf(arrow/ipc): release dictionary payloads eagerly (#1259)
    
    ## What
    
    - Release each dictionary payload as soon as `WritePayload` returns.
    - Keep the optional dictionary delta slice in the same short-lived
    scope.
    - This keeps encoded dictionary payloads from accumulating until
    `writeDictionaryPayloads` returns.
    
    ## Benchmark
    
    The benchmark uses a tracking allocator and reports peak bytes held by
    dictionary payloads beyond the input record.
    
    Apple M1 Pro, Go 1.26.3, 1 CPU, 150 ms, 7 runs:
    
    | Dictionary columns | Before | After |
    | ---: | ---: | ---: |
    | 8 | 1,536 B | 192 B |
    | 64 | 12,288 B | 192 B |
    | 256 | 49,152 B | 192 B |
    
    ## Tests
    
    - `go test ./arrow/ipc`
    - `PARQUET_TEST_DATA="$(pwd)/parquet-testing/data" go test ./...`
    - `go vet ./arrow/ipc`
---
 arrow/ipc/dictionary_payload_lifetime_test.go | 217 ++++++++++++++++++++++++++
 arrow/ipc/writer.go                           |  26 +--
 2 files changed, 231 insertions(+), 12 deletions(-)

diff --git a/arrow/ipc/dictionary_payload_lifetime_test.go 
b/arrow/ipc/dictionary_payload_lifetime_test.go
new file mode 100644
index 00000000..20e19c8a
--- /dev/null
+++ b/arrow/ipc/dictionary_payload_lifetime_test.go
@@ -0,0 +1,217 @@
+// 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.
+
+package ipc
+
+import (
+       "errors"
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/internal/dictutils"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/stretchr/testify/require"
+)
+
+type dictionaryTrackingAllocator struct {
+       memory.Allocator
+       current int64
+}
+
+func (a *dictionaryTrackingAllocator) Allocate(size int) []byte {
+       out := a.Allocator.Allocate(size)
+       a.current += int64(size)
+       return out
+}
+
+func (a *dictionaryTrackingAllocator) Reallocate(size int, b []byte) []byte {
+       out := a.Allocator.Reallocate(size, b)
+       a.current += int64(size - len(b))
+       return out
+}
+
+func (a *dictionaryTrackingAllocator) Free(b []byte) {
+       a.current -= int64(len(b))
+       a.Allocator.Free(b)
+}
+
+type dictionaryPayloadTrackingWriter struct {
+       allocator *dictionaryTrackingAllocator
+       peak      int64
+       payloads  []int64
+}
+
+func (w *dictionaryPayloadTrackingWriter) Start() error { return nil }
+func (w *dictionaryPayloadTrackingWriter) Close() error { return nil }
+
+func (w *dictionaryPayloadTrackingWriter) WritePayload(p Payload) error {
+       if p.msg != MessageDictionaryBatch {
+               return nil
+       }
+
+       current := w.allocator.current
+       w.payloads = append(w.payloads, current)
+       if current > w.peak {
+               w.peak = current
+       }
+       return nil
+}
+
+func newDictionaryRecordBatch(mem memory.Allocator, numColumns, numValues int) 
arrow.RecordBatch {
+       dictType := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int32,
+               ValueType: arrow.PrimitiveTypes.Int64,
+       }
+       fields := make([]arrow.Field, numColumns)
+       for i := range fields {
+               fields[i] = arrow.Field{Name: fmt.Sprintf("dict_%d", i), Type: 
dictType}
+       }
+
+       schema := arrow.NewSchema(fields, nil)
+       builder := array.NewRecordBuilder(mem, schema)
+       for i := 0; i < numColumns; i++ {
+               column := builder.Field(i).(*array.Int64DictionaryBuilder)
+               column.Reserve(numValues)
+               for j := 0; j < numValues; j++ {
+                       column.UnsafeAppend(int64(j))
+               }
+       }
+
+       record := builder.NewRecordBatch()
+       builder.Release()
+       return record
+}
+
+func TestWriteDictionaryPayloadsReleasesPayloadPerDictionary(t *testing.T) {
+       const (
+               numColumns = 8
+               numValues  = 4096
+       )
+
+       mem := &dictionaryTrackingAllocator{Allocator: memory.DefaultAllocator}
+       record := newDictionaryRecordBatch(mem, numColumns, numValues)
+       defer record.Release()
+
+       mapper := &dictutils.Mapper{}
+       mapper.ImportSchema(record.Schema())
+       encoder := newRecordEncoder(mem, 0, kMaxNestingDepth, true, -1, 1, 0, 
nil)
+       lastWrittenDicts := make(map[int64]arrow.Array, numColumns)
+       defer func() {
+               for _, dict := range lastWrittenDicts {
+                       dict.Release()
+               }
+       }()
+
+       writer := &dictionaryPayloadTrackingWriter{allocator: mem}
+       before := mem.current
+       require.NoError(t, writeDictionaryPayloads(mem, record, false, false, 
mapper, lastWrittenDicts, writer, encoder))
+       require.Len(t, writer.payloads, numColumns)
+
+       payloadBytes := writer.payloads[0] - before
+       require.Positive(t, payloadBytes)
+       lastPayloadGrowth := writer.payloads[len(writer.payloads)-1] - 
writer.payloads[0]
+       require.Less(t, lastPayloadGrowth, payloadBytes)
+}
+
+func TestWriteDictionaryPayloadsReleasesPayloadOnFailure(t *testing.T) {
+       const numColumns = 3
+       for _, delta := range []bool{false, true} {
+               for failAfter := 1; failAfter <= numColumns; failAfter++ {
+                       t.Run(fmt.Sprintf("delta=%t/failAfter=%d", delta, 
failAfter), func(t *testing.T) {
+                               mem := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                               defer mem.AssertSize(t, 0)
+                               record := newDictionaryRecordBatch(mem, 
numColumns, 64)
+                               defer record.Release()
+
+                               mapper := &dictutils.Mapper{}
+                               mapper.ImportSchema(record.Schema())
+                               encoder := newRecordEncoder(mem, 0, 
kMaxNestingDepth, true, -1, 1, 0, nil)
+                               lastWrittenDicts := make(map[int64]arrow.Array)
+                               defer func() {
+                                       for _, dict := range lastWrittenDicts {
+                                               dict.Release()
+                                       }
+                               }()
+
+                               if delta {
+                                       previous := 
newDictionaryRecordBatch(mem, numColumns, 32)
+                                       defer previous.Release()
+                                       require.NoError(t, 
writeDictionaryPayloads(mem, previous, false, true,
+                                               mapper, lastWrittenDicts, 
&failingPayloadWriter{}, encoder))
+                               }
+
+                               before := mem.CurrentAlloc()
+                               want := errors.New("dictionary payload write 
failed")
+                               writer := &failingPayloadWriter{err: want, 
failAfter: failAfter}
+                               err := writeDictionaryPayloads(mem, record, 
false, delta,
+                                       mapper, lastWrittenDicts, writer, 
encoder)
+                               require.ErrorIs(t, err, want)
+                               require.Equal(t, failAfter, writer.payloads)
+                               require.Equal(t, before, mem.CurrentAlloc())
+                       })
+               }
+       }
+}
+
+func BenchmarkWriteDictionaryPayloadsPeak(b *testing.B) {
+       for _, tc := range []struct {
+               name       string
+               numColumns int
+               numValues  int
+       }{
+               {name: "8cols", numColumns: 8, numValues: 4096},
+               {name: "64cols", numColumns: 64, numValues: 4096},
+               {name: "256cols", numColumns: 256, numValues: 4096},
+       } {
+               tc := tc
+               b.Run(tc.name, func(b *testing.B) {
+                       mem := &dictionaryTrackingAllocator{Allocator: 
memory.DefaultAllocator}
+                       record := newDictionaryRecordBatch(mem, tc.numColumns, 
tc.numValues)
+                       defer record.Release()
+
+                       mapper := &dictutils.Mapper{}
+                       mapper.ImportSchema(record.Schema())
+                       encoder := newRecordEncoder(mem, 0, kMaxNestingDepth, 
true, -1, 1, 0, nil)
+                       base := mem.current
+                       var peakPayloadBytes int64
+
+                       b.ReportAllocs()
+                       b.ResetTimer()
+                       for i := 0; i < b.N; i++ {
+                               writer := 
&dictionaryPayloadTrackingWriter{allocator: mem}
+                               lastWrittenDicts := make(map[int64]arrow.Array, 
tc.numColumns)
+                               if err := writeDictionaryPayloads(mem, record, 
false, false, mapper, lastWrittenDicts, writer, encoder); err != nil {
+                                       b.Fatal(err)
+                               }
+                               if payloadBytes := writer.peak - base; 
payloadBytes > peakPayloadBytes {
+                                       peakPayloadBytes = payloadBytes
+                               }
+                               for _, dict := range lastWrittenDicts {
+                                       dict.Release()
+                               }
+                               if mem.current != base {
+                                       b.Fatalf("allocator bytes changed from 
%d to %d", base, mem.current)
+                               }
+                       }
+                       b.StopTimer()
+                       b.ReportMetric(float64(peakPayloadBytes), 
"peak-payload-B")
+               })
+       }
+}
+
+var _ memory.Allocator = (*dictionaryTrackingAllocator)(nil)
diff --git a/arrow/ipc/writer.go b/arrow/ipc/writer.go
index a1c768ba..ed8e60a9 100644
--- a/arrow/ipc/writer.go
+++ b/arrow/ipc/writer.go
@@ -291,19 +291,21 @@ func writeDictionaryPayloads(mem memory.Allocator, batch 
arrow.RecordBatch, isFi
                        }
                }
 
-               var data = Payload{msg: MessageDictionaryBatch}
-               defer data.Release()
-
-               dict := pair.Dict
-               if deltaStart > 0 {
-                       dict = array.NewSlice(dict, deltaStart, 
int64(dict.Len()))
-                       defer dict.Release()
-               }
-               if err := enc.Encode(&data, pair.ID, deltaStart > 0, dict); err 
!= nil {
-                       return err
-               }
+               if err := func() error {
+                       data := Payload{msg: MessageDictionaryBatch}
+                       defer data.Release()
+
+                       dict := pair.Dict
+                       if deltaStart > 0 {
+                               dict = array.NewSlice(dict, deltaStart, 
int64(dict.Len()))
+                               defer dict.Release()
+                       }
+                       if err := enc.Encode(&data, pair.ID, deltaStart > 0, 
dict); err != nil {
+                               return err
+                       }
 
-               if err := pw.WritePayload(data); err != nil {
+                       return pw.WritePayload(data)
+               }(); err != nil {
                        return err
                }
 

Reply via email to