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
+}