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 7cea75b2 perf(parquet): vectorize fixed-width dictionary 
materialization (#1269)
7cea75b2 is described below

commit 7cea75b2b0bfb9de995242c842f66cfd81c44008
Author: Minh Vu <[email protected]>
AuthorDate: Wed Sep 2 19:59:17 2026 +0200

    perf(parquet): vectorize fixed-width dictionary materialization (#1269)
    
    This speeds up fixed-width Parquet dictionary materialization on AVX2
    CPUs.
    
    **What changed**
    
    - Gather 8 x 32-bit or 4 x 64-bit dictionary values per SIMD iteration.
    - Cover int32, float32, int64, and float64.
    - Keep the scalar path for short batches, unsupported types, non-AVX2
    CPUs, and noasm builds.
    - Reuse the existing dictionary index validation before the gather.
    - Add correctness tests for tails, boundaries, fallback behavior, and
    exact float bit patterns.
    - Add a benchmark matrix for dictionary sizes, batch sizes, and index
    distributions.
    
    **Benchmarks**
    
    `BenchmarkCopyDictionary` compares scalar and dispatch paths for:
    
    - dictionary sizes 16, 256, 4096, and 65536
    - output batches of 1024 and 65536 values
    - sequential, clustered, and uniform indexes
    
    **Tests**
    
    - `go test -count=1 ./parquet/internal/... ./parquet/compress
    ./parquet/metadata ./parquet/schema ./parquet/variant`
    - `go test -race -count=1 ./parquet/internal/utils
    ./parquet/internal/encoding`
    - `go test -count=1 -tags noasm ./parquet/internal/utils
    ./parquet/internal/encoding`
    - `go vet -composites=false ./parquet/internal/utils
    ./parquet/internal/encoding`
    - Linux amd64 and arm64 test-package cross-builds
---
 parquet/internal/encoding/typed_encoder.go         |   4 +
 parquet/internal/utils/Makefile                    |   6 +
 .../internal/utils/_lib/dictionary_gather_avx2.c   |  50 +++++
 .../internal/utils/_lib/dictionary_gather_avx2.s   | 158 ++++++++++++++
 parquet/internal/utils/dictionary_gather_amd64.go  |  82 +++++++
 .../internal/utils/dictionary_gather_amd64_test.go |  88 ++++++++
 .../internal/utils/dictionary_gather_avx2_amd64.go |  28 +++
 .../internal/utils/dictionary_gather_avx2_amd64.s  |  86 ++++++++
 .../internal/utils/dictionary_gather_default.go    |  29 +++
 parquet/internal/utils/dictionary_gather_test.go   | 237 +++++++++++++++++++++
 10 files changed, 768 insertions(+)

diff --git a/parquet/internal/encoding/typed_encoder.go 
b/parquet/internal/encoding/typed_encoder.go
index d547137a..735ee925 100644
--- a/parquet/internal/encoding/typed_encoder.go
+++ b/parquet/internal/encoding/typed_encoder.go
@@ -394,6 +394,10 @@ func (dc *dictConverter[T]) FillZero(o []T) {
 }
 
 func (dc *dictConverter[T]) Copy(o []T, vals []utils.IndexType) error {
+       if utils.CopyDictionary(o, dc.dict, vals) {
+               return nil
+       }
+
        for idx, val := range vals {
                o[idx] = dc.dict[val]
        }
diff --git a/parquet/internal/utils/Makefile b/parquet/internal/utils/Makefile
index f6dce461..260ef0c0 100644
--- a/parquet/internal/utils/Makefile
+++ b/parquet/internal/utils/Makefile
@@ -49,9 +49,15 @@ ARM_SOURCES := \
 
 assembly: $(INTEL_SOURCES)
 
+# c2goasm does not translate AVX2 VSIB gather instructions, so the matching
+# Go assembly is maintained manually in dictionary_gather_avx2_amd64.s.
+
 _lib/bit_packing_avx2.s: _lib/bit_packing_avx2.c
        $(CC) -S $(C_FLAGS) $(ASM_FLAGS_AVX2) $^ -o $@ ; $(PERL_FIXUP_ROTATE) 
$@; perl -i -pe 's/mem(cpy|set)/clib·_mem\1(SB)/' $@
 
+_lib/dictionary_gather_avx2.s: _lib/dictionary_gather_avx2.c
+       $(CC) -S $(C_FLAGS) $(ASM_FLAGS_AVX2) $^ -o $@ ; $(PERL_FIXUP_ROTATE) $@
+
 _lib/unpack_bool_avx2.s: _lib/unpack_bool.c
        $(CC) -S $(C_FLAGS) $(ASM_FLAGS_AVX2) $^ -o $@ ; $(PERL_FIXUP_ROTATE) $@
 
diff --git a/parquet/internal/utils/_lib/dictionary_gather_avx2.c 
b/parquet/internal/utils/_lib/dictionary_gather_avx2.c
new file mode 100644
index 00000000..a25d012b
--- /dev/null
+++ b/parquet/internal/utils/_lib/dictionary_gather_avx2.c
@@ -0,0 +1,50 @@
+// 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.
+
+#include <immintrin.h>
+#include <stdint.h>
+
+void dictionary_gather_32_avx2(const uint32_t* dictionary, uint32_t* output,
+                               const int32_t* indices, const int len) {
+  int i = 0;
+  for (; i + 8 <= len; i += 8) {
+    const __m256i index =
+        _mm256_loadu_si256((const __m256i*)(indices + i));
+    const __m256i values = _mm256_i32gather_epi32(
+        (const int*)dictionary, index, sizeof(uint32_t));
+    _mm256_storeu_si256((__m256i*)(output + i), values);
+  }
+
+  for (; i < len; ++i) {
+    output[i] = dictionary[indices[i]];
+  }
+}
+
+void dictionary_gather_64_avx2(const uint64_t* dictionary, uint64_t* output,
+                               const int32_t* indices, const int len) {
+  int i = 0;
+  for (; i + 4 <= len; i += 4) {
+    const __m128i index =
+        _mm_loadu_si128((const __m128i*)(indices + i));
+    const __m256i values = _mm256_i32gather_epi64(
+        (const long long*)dictionary, index, sizeof(uint64_t));
+    _mm256_storeu_si256((__m256i*)(output + i), values);
+  }
+
+  for (; i < len; ++i) {
+    output[i] = dictionary[indices[i]];
+  }
+}
diff --git a/parquet/internal/utils/_lib/dictionary_gather_avx2.s 
b/parquet/internal/utils/_lib/dictionary_gather_avx2.s
new file mode 100644
index 00000000..7402703a
--- /dev/null
+++ b/parquet/internal/utils/_lib/dictionary_gather_avx2.s
@@ -0,0 +1,158 @@
+       .intel_syntax noprefix
+       .file   "dictionary_gather_avx2.c"
+       .text
+       .globl  dictionary_gather_32_avx2       # -- Begin function 
dictionary_gather_32_avx2
+       .p2align        4
+       .type   dictionary_gather_32_avx2,@function
+dictionary_gather_32_avx2:              # @dictionary_gather_32_avx2
+# %bb.0:
+                                        # kill: def $ecx killed $ecx def $rcx
+       xor     r8d, r8d
+       mov     eax, ecx
+       cmp     ecx, 8
+       jl      .LBB0_4
+# %bb.1:
+       push    rbp
+       mov     rbp, rsp
+       and     rsp, -8
+       xor     r8d, r8d
+       .p2align        4
+.LBB0_2:                                # =>This Inner Loop Header: Depth=1
+       mov     r9, r8
+       vmovdqu ymm0, ymmword ptr [rdx + 4*r8]
+       vpcmpeqd        ymm1, ymm1, ymm1
+       vpxor   xmm2, xmm2, xmm2
+       vpgatherdd      ymm2, dword ptr [rdi + 4*ymm0], ymm1
+       vmovdqu ymmword ptr [rsi + 4*r8], ymm2
+       add     r8, 8
+       add     r9, 16
+       cmp     r9, rax
+       jbe     .LBB0_2
+# %bb.3:
+       mov     rsp, rbp
+       pop     rbp
+.LBB0_4:
+       cmp     r8d, ecx
+       jge     .LBB0_10
+# %bb.5:
+       mov     r9d, r8d
+       sub     ecx, r8d
+       mov     r8, r9
+       and     ecx, 3
+       je      .LBB0_8
+# %bb.6:
+       mov     r8, r9
+       .p2align        4
+.LBB0_7:                                # =>This Inner Loop Header: Depth=1
+       movsxd  r10, dword ptr [rdx + 4*r8]
+       mov     r10d, dword ptr [rdi + 4*r10]
+       mov     dword ptr [rsi + 4*r8], r10d
+       inc     r8
+       dec     rcx
+       jne     .LBB0_7
+.LBB0_8:
+       sub     r9, rax
+       cmp     r9, -4
+       ja      .LBB0_10
+       .p2align        4
+.LBB0_9:                                # =>This Inner Loop Header: Depth=1
+       movsxd  rcx, dword ptr [rdx + 4*r8]
+       mov     ecx, dword ptr [rdi + 4*rcx]
+       mov     dword ptr [rsi + 4*r8], ecx
+       movsxd  rcx, dword ptr [rdx + 4*r8 + 4]
+       mov     ecx, dword ptr [rdi + 4*rcx]
+       mov     dword ptr [rsi + 4*r8 + 4], ecx
+       movsxd  rcx, dword ptr [rdx + 4*r8 + 8]
+       mov     ecx, dword ptr [rdi + 4*rcx]
+       mov     dword ptr [rsi + 4*r8 + 8], ecx
+       movsxd  rcx, dword ptr [rdx + 4*r8 + 12]
+       mov     ecx, dword ptr [rdi + 4*rcx]
+       mov     dword ptr [rsi + 4*r8 + 12], ecx
+       add     r8, 4
+       cmp     rax, r8
+       jne     .LBB0_9
+.LBB0_10:
+       vzeroupper
+       ret
+.Lfunc_end0:
+       .size   dictionary_gather_32_avx2, .Lfunc_end0-dictionary_gather_32_avx2
+                                        # -- End function
+       .globl  dictionary_gather_64_avx2       # -- Begin function 
dictionary_gather_64_avx2
+       .p2align        4
+       .type   dictionary_gather_64_avx2,@function
+dictionary_gather_64_avx2:              # @dictionary_gather_64_avx2
+# %bb.0:
+                                        # kill: def $ecx killed $ecx def $rcx
+       xor     r8d, r8d
+       mov     eax, ecx
+       cmp     ecx, 4
+       jl      .LBB1_4
+# %bb.1:
+       push    rbp
+       mov     rbp, rsp
+       and     rsp, -8
+       xor     r8d, r8d
+       .p2align        4
+.LBB1_2:                                # =>This Inner Loop Header: Depth=1
+       mov     r9, r8
+       vmovdqu xmm0, xmmword ptr [rdx + 4*r8]
+       vpcmpeqd        ymm1, ymm1, ymm1
+       vpxor   xmm2, xmm2, xmm2
+       vpgatherdq      ymm2, qword ptr [rdi + 8*xmm0], ymm1
+       vmovdqu ymmword ptr [rsi + 8*r8], ymm2
+       add     r8, 4
+       add     r9, 8
+       cmp     r9, rax
+       jbe     .LBB1_2
+# %bb.3:
+       mov     rsp, rbp
+       pop     rbp
+.LBB1_4:
+       cmp     r8d, ecx
+       jge     .LBB1_10
+# %bb.5:
+       mov     r9d, r8d
+       sub     ecx, r8d
+       mov     r8, r9
+       and     ecx, 3
+       je      .LBB1_8
+# %bb.6:
+       mov     r8, r9
+       .p2align        4
+.LBB1_7:                                # =>This Inner Loop Header: Depth=1
+       movsxd  r10, dword ptr [rdx + 4*r8]
+       mov     r10, qword ptr [rdi + 8*r10]
+       mov     qword ptr [rsi + 8*r8], r10
+       inc     r8
+       dec     rcx
+       jne     .LBB1_7
+.LBB1_8:
+       sub     r9, rax
+       cmp     r9, -4
+       ja      .LBB1_10
+       .p2align        4
+.LBB1_9:                                # =>This Inner Loop Header: Depth=1
+       movsxd  rcx, dword ptr [rdx + 4*r8]
+       mov     rcx, qword ptr [rdi + 8*rcx]
+       mov     qword ptr [rsi + 8*r8], rcx
+       movsxd  rcx, dword ptr [rdx + 4*r8 + 4]
+       mov     rcx, qword ptr [rdi + 8*rcx]
+       mov     qword ptr [rsi + 8*r8 + 8], rcx
+       movsxd  rcx, dword ptr [rdx + 4*r8 + 8]
+       mov     rcx, qword ptr [rdi + 8*rcx]
+       mov     qword ptr [rsi + 8*r8 + 16], rcx
+       movsxd  rcx, dword ptr [rdx + 4*r8 + 12]
+       mov     rcx, qword ptr [rdi + 8*rcx]
+       mov     qword ptr [rsi + 8*r8 + 24], rcx
+       add     r8, 4
+       cmp     rax, r8
+       jne     .LBB1_9
+.LBB1_10:
+       vzeroupper
+       ret
+.Lfunc_end1:
+       .size   dictionary_gather_64_avx2, .Lfunc_end1-dictionary_gather_64_avx2
+                                        # -- End function
+       .ident  "Apple clang version 21.0.0 (clang-2100.1.1.101)"
+       .section        ".note.GNU-stack","",@progbits
+       .addrsig
diff --git a/parquet/internal/utils/dictionary_gather_amd64.go 
b/parquet/internal/utils/dictionary_gather_amd64.go
new file mode 100644
index 00000000..fc007ad0
--- /dev/null
+++ b/parquet/internal/utils/dictionary_gather_amd64.go
@@ -0,0 +1,82 @@
+// 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 amd64 && !noasm && !appengine
+// +build amd64,!noasm,!appengine
+
+package utils
+
+import (
+       "unsafe"
+
+       "github.com/apache/arrow-go/v18/parquet"
+       "golang.org/x/sys/cpu"
+)
+
+type dictionaryGatherFunc func(dictionary, output, indices unsafe.Pointer, 
length int)
+
+const (
+       dictionaryGather32MinValues = 32
+       dictionaryGather64MinValues = 16
+)
+
+var (
+       dictionaryGather32 dictionaryGatherFunc
+       dictionaryGather64 dictionaryGatherFunc
+)
+
+func init() {
+       if cpu.X86.HasAVX2 {
+               dictionaryGather32 = _dictionary_gather_32_avx2
+               dictionaryGather64 = _dictionary_gather_64_avx2
+       }
+}
+
+// CopyDictionary reports whether a fixed-width dictionary copy was performed
+// by the AVX2 implementation. The caller must validate dictionary indexes
+// before calling this function.
+func CopyDictionary[T parquet.ColumnTypes](out, dictionary []T, indices 
[]IndexType) bool {
+       if len(indices) == 0 || len(out) < len(indices) || len(dictionary) == 0 
{
+               return false
+       }
+
+       var gather dictionaryGatherFunc
+       switch any(out).(type) {
+       case []int32, []float32:
+               if len(indices) < dictionaryGather32MinValues {
+                       return false
+               }
+               gather = dictionaryGather32
+       case []int64, []float64:
+               if len(indices) < dictionaryGather64MinValues {
+                       return false
+               }
+               gather = dictionaryGather64
+       default:
+               return false
+       }
+       if gather == nil {
+               return false
+       }
+
+       gather(
+               unsafe.Pointer(unsafe.SliceData(dictionary)),
+               unsafe.Pointer(unsafe.SliceData(out)),
+               unsafe.Pointer(unsafe.SliceData(indices)),
+               len(indices),
+       )
+       return true
+}
diff --git a/parquet/internal/utils/dictionary_gather_amd64_test.go 
b/parquet/internal/utils/dictionary_gather_amd64_test.go
new file mode 100644
index 00000000..7f2e4334
--- /dev/null
+++ b/parquet/internal/utils/dictionary_gather_amd64_test.go
@@ -0,0 +1,88 @@
+// 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 amd64 && !noasm && !appengine
+
+package utils
+
+import (
+       "slices"
+       "testing"
+       "unsafe"
+
+       "golang.org/x/sys/cpu"
+)
+
+func TestCopyDictionaryAVX2Bounds(t *testing.T) {
+       t.Run("int32", testCopyDictionaryAVX2Bounds[int32])
+       t.Run("int64", testCopyDictionaryAVX2Bounds[int64])
+       t.Run("float32", testCopyDictionaryAVX2Bounds[float32])
+       t.Run("float64", testCopyDictionaryAVX2Bounds[float64])
+}
+
+func testCopyDictionaryAVX2Bounds[T int32 | int64 | float32 | float64](t 
*testing.T) {
+       dictionaryStorage := make([]T, 35)
+       dictionary := dictionaryStorage[1:34]
+       for i := range dictionary {
+               dictionary[i] = T(i*17 - 100)
+       }
+       originalDictionary := slices.Clone(dictionaryStorage)
+       threshold := dictionaryGather32MinValues
+       if unsafe.Sizeof(T(0)) == 8 {
+               threshold = dictionaryGather64MinValues
+       }
+       for _, length := range []int{0, 1, 15, 16, 17, 31, 32, 33, 64, 65, 
1023, 1024, 1025} {
+               for _, offset := range []int{0, 1, 7} {
+                       indexStorage := make([]IndexType, length+offset+1)
+                       indices := indexStorage[offset : offset+length]
+                       for i := range indices {
+                               indices[i] = IndexType((i*19 + length) % 
len(dictionary))
+                       }
+                       originalIndices := slices.Clone(indexStorage)
+                       output := make([]T, length+offset+2)
+                       for i := range output {
+                               output[i] = -77
+                       }
+                       expected := slices.Clone(output)
+                       wantDispatch := cpu.X86.HasAVX2 && length >= threshold
+                       if wantDispatch {
+                               copyDictionaryScalar(expected[offset:], 
dictionary, indices)
+                       }
+                       if got := CopyDictionary(output[offset:len(output)-1], 
dictionary, indices); got != wantDispatch {
+                               t.Fatalf("length=%d offset=%d: dispatch=%v, 
want %v", length, offset, got, wantDispatch)
+                       }
+                       if !slices.Equal(expected, output) {
+                               t.Fatalf("length=%d offset=%d: unexpected 
output or overwritten guard", length, offset)
+                       }
+                       if !slices.Equal(originalDictionary, dictionaryStorage) 
|| !slices.Equal(originalIndices, indexStorage) {
+                               t.Fatal("dictionary copy modified its input")
+                       }
+               }
+       }
+}
+
+func TestCopyDictionaryDisabledAVX2(t *testing.T) {
+       saved32, saved64 := dictionaryGather32, dictionaryGather64
+       dictionaryGather32, dictionaryGather64 = nil, nil
+       defer func() { dictionaryGather32, dictionaryGather64 = saved32, 
saved64 }()
+       indices := make([]IndexType, 64)
+       if CopyDictionary(make([]int32, len(indices)), []int32{1}, indices) {
+               t.Fatal("32-bit gather ran while disabled")
+       }
+       if CopyDictionary(make([]int64, len(indices)), []int64{1}, indices) {
+               t.Fatal("64-bit gather ran while disabled")
+       }
+}
diff --git a/parquet/internal/utils/dictionary_gather_avx2_amd64.go 
b/parquet/internal/utils/dictionary_gather_avx2_amd64.go
new file mode 100644
index 00000000..1d4cb587
--- /dev/null
+++ b/parquet/internal/utils/dictionary_gather_avx2_amd64.go
@@ -0,0 +1,28 @@
+// 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 amd64 && !noasm && !appengine
+// +build amd64,!noasm,!appengine
+
+package utils
+
+import "unsafe"
+
+//go:noescape
+func _dictionary_gather_32_avx2(dictionary, output, indices unsafe.Pointer, 
length int)
+
+//go:noescape
+func _dictionary_gather_64_avx2(dictionary, output, indices unsafe.Pointer, 
length int)
diff --git a/parquet/internal/utils/dictionary_gather_avx2_amd64.s 
b/parquet/internal/utils/dictionary_gather_avx2_amd64.s
new file mode 100644
index 00000000..ded51cbf
--- /dev/null
+++ b/parquet/internal/utils/dictionary_gather_avx2_amd64.s
@@ -0,0 +1,86 @@
+// 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 amd64 && !noasm && !appengine
+// +build amd64,!noasm,!appengine
+
+#include "textflag.h"
+
+TEXT ·_dictionary_gather_32_avx2(SB), NOSPLIT, $0-32
+       MOVQ dictionary+0(FP), DI
+       MOVQ output+8(FP), SI
+       MOVQ indices+16(FP), DX
+       MOVQ length+24(FP), CX
+
+       XORQ R8, R8
+       MOVQ CX, R9
+       ANDQ $-8, R9
+
+gather32_vector:
+       CMPQ R8, R9
+       JAE gather32_scalar
+       VMOVDQU (DX)(R8*4), Y0
+       VPCMPEQD Y1, Y1, Y1
+       VPGATHERDD Y1, (DI)(Y0*4), Y2
+       VMOVDQU Y2, (SI)(R8*4)
+       ADDQ $8, R8
+       JMP gather32_vector
+
+gather32_scalar:
+       CMPQ R8, CX
+       JAE gather32_done
+       MOVL (DX)(R8*4), R10
+       MOVL (DI)(R10*4), R11
+       MOVL R11, (SI)(R8*4)
+       INCQ R8
+       JMP gather32_scalar
+
+gather32_done:
+       VZEROUPPER
+       RET
+
+TEXT ·_dictionary_gather_64_avx2(SB), NOSPLIT, $0-32
+       MOVQ dictionary+0(FP), DI
+       MOVQ output+8(FP), SI
+       MOVQ indices+16(FP), DX
+       MOVQ length+24(FP), CX
+
+       XORQ R8, R8
+       MOVQ CX, R9
+       ANDQ $-4, R9
+
+gather64_vector:
+       CMPQ R8, R9
+       JAE gather64_scalar
+       VMOVDQU (DX)(R8*4), X0
+       VPCMPEQD Y1, Y1, Y1
+       VPGATHERDQ Y1, (DI)(X0*8), Y2
+       VMOVDQU Y2, (SI)(R8*8)
+       ADDQ $4, R8
+       JMP gather64_vector
+
+gather64_scalar:
+       CMPQ R8, CX
+       JAE gather64_done
+       MOVL (DX)(R8*4), R10
+       MOVQ (DI)(R10*8), R11
+       MOVQ R11, (SI)(R8*8)
+       INCQ R8
+       JMP gather64_scalar
+
+gather64_done:
+       VZEROUPPER
+       RET
diff --git a/parquet/internal/utils/dictionary_gather_default.go 
b/parquet/internal/utils/dictionary_gather_default.go
new file mode 100644
index 00000000..1bea4d2e
--- /dev/null
+++ b/parquet/internal/utils/dictionary_gather_default.go
@@ -0,0 +1,29 @@
+// 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 !amd64 || noasm || appengine
+// +build !amd64 noasm appengine
+
+package utils
+
+import "github.com/apache/arrow-go/v18/parquet"
+
+// CopyDictionary reports whether a fixed-width dictionary copy was performed
+// by an architecture-specific implementation. The caller must validate
+// dictionary indexes before calling this function.
+func CopyDictionary[T parquet.ColumnTypes](out, dictionary []T, indices 
[]IndexType) bool {
+       return false
+}
diff --git a/parquet/internal/utils/dictionary_gather_test.go 
b/parquet/internal/utils/dictionary_gather_test.go
new file mode 100644
index 00000000..e0d9bebd
--- /dev/null
+++ b/parquet/internal/utils/dictionary_gather_test.go
@@ -0,0 +1,237 @@
+// 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 utils
+
+import (
+       "fmt"
+       "math"
+       "reflect"
+       "runtime"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/parquet"
+)
+
+func TestCopyDictionary(t *testing.T) {
+       t.Run("int32", func(t *testing.T) {
+               testCopyDictionary(t, []int32{-10, 0, 10, 100, math.MinInt32, 
math.MaxInt32})
+       })
+       t.Run("float32", func(t *testing.T) {
+               testCopyDictionary(t, []float32{-10.5, 0, 10.5, 100.25})
+       })
+       t.Run("int64", func(t *testing.T) {
+               testCopyDictionary(t, []int64{-10, 0, 10, 100, math.MinInt64, 
math.MaxInt64})
+       })
+       t.Run("float64", func(t *testing.T) {
+               testCopyDictionary(t, []float64{-10.5, 0, 10.5, 100.25})
+       })
+}
+
+func testCopyDictionary[T parquet.ColumnTypes](t *testing.T, dictionary []T) {
+       t.Helper()
+       for _, length := range []int{0, 1, 3, 4, 7, 8, 9, 15, 16, 17, 31, 32, 
33} {
+               indices := make([]IndexType, length)
+               for i := range indices {
+                       indices[i] = IndexType((i*5 + 1) % len(dictionary))
+               }
+
+               got := make([]T, length)
+               if !CopyDictionary(got, dictionary, indices) {
+                       copyDictionaryScalar(got, dictionary, indices)
+               }
+
+               want := make([]T, length)
+               copyDictionaryScalar(want, dictionary, indices)
+               if !reflect.DeepEqual(got, want) {
+                       t.Fatalf("length %d: got %v, want %v", length, got, 
want)
+               }
+       }
+}
+
+func TestCopyDictionaryPreservesFloatBits(t *testing.T) {
+       dict32 := []float32{
+               math.Float32frombits(0x00000001),
+               math.Float32frombits(0x80000000),
+               math.Float32frombits(0x7fc00001),
+               math.Float32frombits(0xffc00001),
+       }
+       indices := make([]IndexType, 33)
+       for i := range indices {
+               indices[i] = IndexType((i * 3) % len(dict32))
+       }
+       got32 := make([]float32, len(indices))
+       if !CopyDictionary(got32, dict32, indices) {
+               copyDictionaryScalar(got32, dict32, indices)
+       }
+       for i, idx := range indices {
+               if got, want := math.Float32bits(got32[i]), 
math.Float32bits(dict32[idx]); got != want {
+                       t.Fatalf("float32 index %d: got %#x, want %#x", i, got, 
want)
+               }
+       }
+
+       dict64 := []float64{
+               math.Float64frombits(0x0000000000000001),
+               math.Float64frombits(0x8000000000000000),
+               math.Float64frombits(0x7ff8000000000001),
+               math.Float64frombits(0xfff8000000000001),
+       }
+       got64 := make([]float64, len(indices))
+       if !CopyDictionary(got64, dict64, indices) {
+               copyDictionaryScalar(got64, dict64, indices)
+       }
+       for i, idx := range indices {
+               if got, want := math.Float64bits(got64[i]), 
math.Float64bits(dict64[idx]); got != want {
+                       t.Fatalf("float64 index %d: got %#x, want %#x", i, got, 
want)
+               }
+       }
+}
+
+func TestCopyDictionaryRejectsUnsupportedOrShortOutput(t *testing.T) {
+       if CopyDictionary(make([]bool, 64), []bool{false, true}, 
make([]IndexType, 64)) {
+               t.Fatal("boolean dictionary unexpectedly used fixed-width 
gather")
+       }
+
+       indices := make([]IndexType, 64)
+       out := make([]int32, len(indices)-1)
+       for i := range out {
+               out[i] = -1
+       }
+       if CopyDictionary(out, []int32{1, 2}, indices) {
+               t.Fatal("short output unexpectedly used fixed-width gather")
+       }
+       for i, got := range out {
+               if got != -1 {
+                       t.Fatalf("short output was modified at %d: got %d", i, 
got)
+               }
+       }
+}
+
+func copyDictionaryScalar[T parquet.ColumnTypes](out, dictionary []T, indices 
[]IndexType) {
+       for i, idx := range indices {
+               out[i] = dictionary[idx]
+       }
+}
+
+func BenchmarkCopyDictionary(b *testing.B) {
+       for _, typ := range []struct {
+               name string
+               run  func(*testing.B, int, int, string, bool)
+       }{
+               {
+                       name: "int32",
+                       run: func(b *testing.B, dictionarySize, length int, 
distribution string, dispatch bool) {
+                               benchmarkCopyDictionary(b, 
makeInt32Dictionary(dictionarySize), makeDictionaryIndices(length, 
dictionarySize, distribution), 4, dispatch)
+                       },
+               },
+               {
+                       name: "float32",
+                       run: func(b *testing.B, dictionarySize, length int, 
distribution string, dispatch bool) {
+                               benchmarkCopyDictionary(b, 
makeFloat32Dictionary(dictionarySize), makeDictionaryIndices(length, 
dictionarySize, distribution), 4, dispatch)
+                       },
+               },
+               {
+                       name: "int64",
+                       run: func(b *testing.B, dictionarySize, length int, 
distribution string, dispatch bool) {
+                               benchmarkCopyDictionary(b, 
makeInt64Dictionary(dictionarySize), makeDictionaryIndices(length, 
dictionarySize, distribution), 8, dispatch)
+                       },
+               },
+               {
+                       name: "float64",
+                       run: func(b *testing.B, dictionarySize, length int, 
distribution string, dispatch bool) {
+                               benchmarkCopyDictionary(b, 
makeFloat64Dictionary(dictionarySize), makeDictionaryIndices(length, 
dictionarySize, distribution), 8, dispatch)
+                       },
+               },
+       } {
+               for _, dictionarySize := range []int{16, 256, 4096, 65536} {
+                       for _, length := range []int{1024, 65536} {
+                               for _, distribution := range 
[]string{"sequential", "clustered", "uniform"} {
+                                       name := 
fmt.Sprintf("%s/dict=%d/values=%d/%s", typ.name, dictionarySize, length, 
distribution)
+                                       b.Run(name+"/scalar", func(b 
*testing.B) {
+                                               typ.run(b, dictionarySize, 
length, distribution, false)
+                                       })
+                                       b.Run(name+"/dispatch", func(b 
*testing.B) {
+                                               typ.run(b, dictionarySize, 
length, distribution, true)
+                                       })
+                               }
+                       }
+               }
+       }
+}
+
+func benchmarkCopyDictionary[T parquet.ColumnTypes](b *testing.B, dictionary 
[]T, indices []IndexType, bytes int64, dispatch bool) {
+       out := make([]T, len(indices))
+       b.ReportAllocs()
+       b.SetBytes(int64(len(indices)) * bytes)
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               if dispatch && CopyDictionary(out, dictionary, indices) {
+                       continue
+               }
+               copyDictionaryScalar(out, dictionary, indices)
+       }
+       b.StopTimer()
+       runtime.KeepAlive(out)
+}
+
+func makeDictionaryIndices(length, dictionarySize int, distribution string) 
[]IndexType {
+       indices := make([]IndexType, length)
+       state := uint32(1)
+       for i := range indices {
+               switch distribution {
+               case "sequential":
+                       indices[i] = IndexType(i % dictionarySize)
+               case "clustered":
+                       indices[i] = IndexType((i/8 + i%4) % dictionarySize)
+               case "uniform":
+                       state = state*1664525 + 1013904223
+                       indices[i] = IndexType(state % uint32(dictionarySize))
+               }
+       }
+       return indices
+}
+
+func makeInt32Dictionary(length int) []int32 {
+       dict := make([]int32, length)
+       for i := range dict {
+               dict[i] = int32(i*17 - length)
+       }
+       return dict
+}
+
+func makeFloat32Dictionary(length int) []float32 {
+       dict := make([]float32, length)
+       for i := range dict {
+               dict[i] = float32(i)*1.25 - float32(length)
+       }
+       return dict
+}
+
+func makeInt64Dictionary(length int) []int64 {
+       dict := make([]int64, length)
+       for i := range dict {
+               dict[i] = int64(i*17 - length)
+       }
+       return dict
+}
+
+func makeFloat64Dictionary(length int) []float64 {
+       dict := make([]float64, length)
+       for i := range dict {
+               dict[i] = float64(i)*1.25 - float64(length)
+       }
+       return dict
+}

Reply via email to