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 871b2197 perf(compute): vectorize mixed 64-bit filters (#1311)
871b2197 is described below

commit 871b21973ef1a52baba41daa16f2bbfa67f21197
Author: Minh Vu <[email protected]>
AuthorDate: Wed Sep 23 20:19:55 2026 +0200

    perf(compute): vectorize mixed 64-bit filters (#1311)
    
    ## What changed
    
    - Add AVX2 and ARM64 NEON paths for null-free 64-bit fixed-width filters.
    - Compact two groups of four uint64 values from each filter byte with 
table-driven permutations and exact-size stores.
    - Keep the existing path for nulls, unsupported CPUs, short or unaligned 
inputs, dense masks, and noasm builds.
    - Add exhaustive mask tests, guard tests, and int64/float64 benchmarks.
    
    ## Benchmark
    
    Apple M1 Pro, macOS arm64, Go 1.26.3. Median of five runs. The baseline 
uses GODEBUG=cpu.all=off.
    
    | Case | Baseline | NEON | Speedup |
    | --- | ---: | ---: | ---: |
    | int64, 1M, alternating | 3.97 ms | 0.45 ms | 8.7x |
    | int64, 1M, random50 | 2.86 ms | 1.61 ms | 1.8x |
    | float64, 1M, alternating | 4.07 ms | 0.47 ms | 8.6x |
    | float64, 1M, random50 | 2.92 ms | 1.65 ms | 1.8x |
    
    The mixed-mask cases also go from 33 to 32 allocations per operation. The 
output allocation is still the main B/op cost.
    
    ## Tests
    
    - Full go test suite with Parquet test data
    - go test ./arrow/compute/...
    - go test -tags noasm ./arrow/compute/...
    - go test -race ./arrow/compute/internal/kernels ./arrow/compute
    - amd64 cross-build of the kernel package
---
 arrow/compute/internal/kernels/Makefile            |  10 +-
 .../compute/internal/kernels/_lib/filter_uint64.cc |  78 ++++++++++
 .../kernels/_lib/filter_uint64_avx2_amd64.s        |  82 +++++++++++
 .../internal/kernels/filter_uint64_arch_test.go    | 125 ++++++++++++++++
 .../internal/kernels/filter_uint64_avx2_amd64.go   |  92 ++++++++++++
 .../internal/kernels/filter_uint64_avx2_amd64.s    |  79 ++++++++++
 .../kernels/filter_uint64_avx2_amd64_test.go       |  46 ++++++
 .../internal/kernels/filter_uint64_avx2_noasm.go   |  23 +++
 .../internal/kernels/filter_uint64_neon_arm64.go   |  92 ++++++++++++
 .../internal/kernels/filter_uint64_neon_arm64.s    | 160 +++++++++++++++++++++
 .../kernels/filter_uint64_neon_arm64_test.go       |  46 ++++++
 .../internal/kernels/filter_uint64_neon_noasm.go   |  23 +++
 arrow/compute/internal/kernels/vector_selection.go |   8 ++
 arrow/compute/vector_selection_bench_test.go       |  66 ++++++++-
 14 files changed, 928 insertions(+), 2 deletions(-)

diff --git a/arrow/compute/internal/kernels/Makefile 
b/arrow/compute/internal/kernels/Makefile
index 91a12e62..a1a8b419 100644
--- a/arrow/compute/internal/kernels/Makefile
+++ b/arrow/compute/internal/kernels/Makefile
@@ -41,7 +41,8 @@ ALL_SOURCES := $(shell find . -path ./_lib -prune -o -name 
'*.go' -name '*.s' -n
 INTEL_SOURCES := \
        cast_numeric_avx2_amd64.s cast_numeric_sse4_amd64.s 
constant_factor_avx2_amd64.s \
        constant_factor_sse4_amd64.s base_arithmetic_avx2_amd64.s 
base_arithmetic_sse4_amd64.s \
-       scalar_comparison_avx2_amd64.s scalar_comparison_sse4_amd64.s 
filter_uint32_avx2_amd64.s
+       scalar_comparison_avx2_amd64.s scalar_comparison_sse4_amd64.s \
+       filter_uint32_avx2_amd64.s filter_uint64_avx2_amd64.s
 
 #
 # ARROW-15336: DO NOT add the assembly target for Arm64 (ARM_SOURCES) until 
c2goasm added the Arm64 support.
@@ -75,6 +76,9 @@ _lib/scalar_comparison_sse4_amd64.s: _lib/scalar_comparison.cc
 _lib/filter_uint32_avx2_amd64.s: _lib/filter_uint32.cc
        $(CXX) -std=c++17 -S $(C_FLAGS) $(ASM_FLAGS_AVX2) $^ -o $@ ; 
$(PERL_FIXUP_ROTATE) $@
 
+_lib/filter_uint64_avx2_amd64.s: _lib/filter_uint64.cc
+       $(CXX) -std=c++17 -S $(C_FLAGS) $(ASM_FLAGS_AVX2) -mno-stackrealign 
-fomit-frame-pointer $^ -o $@ ; $(PERL_FIXUP_ROTATE) $@
+
 _lib/base_arithmetic_neon.s: _lib/base_arithmetic.cc
        $(CXX) -std=c++17 -S $(C_FLAGS_NEON) $^ -o $@ ; $(PERL_FIXUP_ROTATE) $@
 
@@ -114,6 +118,10 @@ scalar_comparison_sse4_amd64.s: 
_lib/scalar_comparison_sse4_amd64.s
 filter_uint32_avx2_amd64.s: _lib/filter_uint32_avx2_amd64.s
        $(C2GOASM) -a -f $^ $@
 
+filter_uint64_avx2_amd64.s: _lib/filter_uint64_avx2_amd64.s
+       $(C2GOASM) -a -f $^ $@
+       perl -i -pe 's{^//\s*\+build\b.*}{//go:build go1.18 && amd64 && !noasm 
&& !appengine}' $@
+
 clean:
        rm -f $(INTEL_SOURCES)
        rm -f $(addprefix _lib/,$(INTEL_SOURCES))
diff --git a/arrow/compute/internal/kernels/_lib/filter_uint64.cc 
b/arrow/compute/internal/kernels/_lib/filter_uint64.cc
new file mode 100644
index 00000000..f00fed30
--- /dev/null
+++ b/arrow/compute/internal/kernels/_lib/filter_uint64.cc
@@ -0,0 +1,78 @@
+// 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 <arch.h>
+#include <immintrin.h>
+#include <stdint.h>
+
+extern "C" void FULL_NAME(filter_uint64)(const uint64_t* values,
+                                           const uint8_t* filter,
+                                           uint64_t* output,
+                                           const uint8_t* tables,
+                                           const int64_t length) {
+    const uint8_t* permutation_tables = tables;
+    const uint64_t* store_masks = reinterpret_cast<const uint64_t*>(tables + 
512);
+    const uint8_t* popcount = tables + 672;
+    int64_t output_length = 0;
+    const int64_t num_bytes = length / 8;
+
+    for (int64_t i = 0; i < num_bytes; ++i) {
+        const uint8_t mask = filter[i];
+        const uint64_t* input = values + i * 8;
+
+        if (mask == 0) {
+            continue;
+        }
+
+        if (mask == 0xff) {
+            const __m256i low = _mm256_loadu_si256(reinterpret_cast<const 
__m256i*>(input));
+            const __m256i high = _mm256_loadu_si256(reinterpret_cast<const 
__m256i*>(input + 4));
+            _mm256_storeu_si256(reinterpret_cast<__m256i*>(output + 
output_length), low);
+            _mm256_storeu_si256(reinterpret_cast<__m256i*>(output + 
output_length + 4), high);
+            output_length += 8;
+            continue;
+        }
+
+        const uint8_t low_mask = mask & 0xf;
+        const uint8_t high_mask = mask >> 4;
+        const int low_count = popcount[low_mask];
+        const int high_count = popcount[high_mask];
+
+        if (low_count != 0) {
+            const __m256i input_values = 
_mm256_loadu_si256(reinterpret_cast<const __m256i*>(input));
+            const __m256i permutation = _mm256_loadu_si256(
+                reinterpret_cast<const __m256i*>(permutation_tables + low_mask 
* 32));
+            const __m256i compacted = 
_mm256_permutevar8x32_epi32(input_values, permutation);
+            const __m256i store_mask = _mm256_loadu_si256(
+                reinterpret_cast<const __m256i*>(store_masks + low_count * 4));
+            _mm256_maskstore_epi32(reinterpret_cast<int*>(output + 
output_length),
+                                   store_mask, compacted);
+            output_length += low_count;
+        }
+
+        if (high_count != 0) {
+            const __m256i input_values = 
_mm256_loadu_si256(reinterpret_cast<const __m256i*>(input + 4));
+            const __m256i permutation = _mm256_loadu_si256(
+                reinterpret_cast<const __m256i*>(permutation_tables + 
high_mask * 32));
+            const __m256i compacted = 
_mm256_permutevar8x32_epi32(input_values, permutation);
+            const __m256i store_mask = _mm256_loadu_si256(
+                reinterpret_cast<const __m256i*>(store_masks + high_count * 
4));
+            _mm256_maskstore_epi32(reinterpret_cast<int*>(output + 
output_length),
+                                   store_mask, compacted);
+            output_length += high_count;
+        }
+    }
+}
diff --git a/arrow/compute/internal/kernels/_lib/filter_uint64_avx2_amd64.s 
b/arrow/compute/internal/kernels/_lib/filter_uint64_avx2_amd64.s
new file mode 100644
index 00000000..bdca2b4a
--- /dev/null
+++ b/arrow/compute/internal/kernels/_lib/filter_uint64_avx2_amd64.s
@@ -0,0 +1,82 @@
+       .intel_syntax noprefix
+       .file   "filter_uint64.cc"
+       .text
+       .globl  filter_uint64_avx2              # -- Begin function 
filter_uint64_avx2
+       .p2align        4
+       .type   filter_uint64_avx2,@function
+filter_uint64_avx2:                     # @filter_uint64_avx2
+# %bb.0:
+       lea     rax, [r8 + 7]
+       test    r8, r8
+       cmovns  rax, r8
+       cmp     r8, 8
+       jl      .LBB0_11
+# %bb.1:
+       push    r14
+       push    rbx
+       sar     rax, 3
+       add     rdi, 32
+       xor     r8d, r8d
+       xor     r9d, r9d
+       jmp     .LBB0_2
+       .p2align        4
+.LBB0_4:                                #   in Loop: Header=BB0_2 Depth=1
+       vmovdqu ymm0, ymmword ptr [rdi - 32]
+       vmovdqu ymm1, ymmword ptr [rdi]
+       vmovdqu ymmword ptr [rdx + 8*r8], ymm0
+       vmovdqu ymmword ptr [rdx + 8*r8 + 32], ymm1
+       add     r8, 8
+.LBB0_9:                                #   in Loop: Header=BB0_2 Depth=1
+       inc     r9
+       add     rdi, 64
+       cmp     rax, r9
+       je      .LBB0_10
+.LBB0_2:                                # =>This Inner Loop Header: Depth=1
+       movzx   r10d, byte ptr [rsi + r9]
+       test    r10d, r10d
+       je      .LBB0_9
+# %bb.3:                                #   in Loop: Header=BB0_2 Depth=1
+       cmp     r10d, 255
+       je      .LBB0_4
+# %bb.5:                                #   in Loop: Header=BB0_2 Depth=1
+       mov     r14d, r10d
+       and     r14d, 15
+       shr     r10d, 4
+       movzx   ebx, byte ptr [rcx + r14 + 672]
+       movzx   r11d, byte ptr [rcx + r10 + 672]
+       test    rbx, rbx
+       je      .LBB0_7
+# %bb.6:                                #   in Loop: Header=BB0_2 Depth=1
+       shl     r14d, 5
+       vmovdqu ymm0, ymmword ptr [rcx + r14]
+       vpermd  ymm0, ymm0, ymmword ptr [rdi - 32]
+       mov     r14d, ebx
+       shl     r14d, 5
+       vmovdqu ymm1, ymmword ptr [rcx + r14 + 512]
+       vpmaskmovd      ymmword ptr [rdx + 8*r8], ymm1, ymm0
+       add     r8, rbx
+.LBB0_7:                                #   in Loop: Header=BB0_2 Depth=1
+       test    r11, r11
+       je      .LBB0_9
+# %bb.8:                                #   in Loop: Header=BB0_2 Depth=1
+       shl     r10d, 5
+       vmovdqu ymm0, ymmword ptr [rcx + r10]
+       vpermd  ymm0, ymm0, ymmword ptr [rdi]
+       mov     r10d, r11d
+       shl     r10d, 5
+       vmovdqu ymm1, ymmword ptr [rcx + r10 + 512]
+       vpmaskmovd      ymmword ptr [rdx + 8*r8], ymm1, ymm0
+       add     r8, r11
+       jmp     .LBB0_9
+.LBB0_10:
+       pop     rbx
+       pop     r14
+.LBB0_11:
+       vzeroupper
+       ret
+.Lfunc_end0:
+       .size   filter_uint64_avx2, .Lfunc_end0-filter_uint64_avx2
+                                        # -- End function
+       .ident  "Apple clang version 21.0.0 (clang-2100.1.1.101)"
+       .section        ".note.GNU-stack","",@progbits
+       .addrsig
diff --git a/arrow/compute/internal/kernels/filter_uint64_arch_test.go 
b/arrow/compute/internal/kernels/filter_uint64_arch_test.go
new file mode 100644
index 00000000..8eb42d0e
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint64_arch_test.go
@@ -0,0 +1,125 @@
+// 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.18 && (amd64 || arm64) && !noasm && !appengine
+
+package kernels
+
+import "testing"
+
+type filterUint64Func func(values []uint64, output []uint64, filter []byte, 
filterOffset, length int64) bool
+
+func testFilterUint64AllMasks(t *testing.T, filter filterUint64Func) {
+       t.Helper()
+
+       const (
+               filterBytes = 256
+               length      = filterBytes * 8
+       )
+       values := make([]uint64, length)
+       filterData := make([]byte, filterBytes)
+       for i := range values {
+               values[i] = uint64(i*17 + 3)
+       }
+       for i := range filterData {
+               filterData[i] = byte(i)
+       }
+
+       expected := make([]uint64, 0, length)
+       for i, mask := range filterData {
+               for lane := 0; lane < 8; lane++ {
+                       if mask&(1<<uint(lane)) != 0 {
+                               expected = append(expected, values[i*8+lane])
+                       }
+               }
+       }
+
+       backing := make([]uint64, len(expected)+1)
+       backing[len(expected)] = 0xdeadbeef
+       if !filter(values, backing[:len(expected)], filterData, 0, length) {
+               t.Fatal("filterUint64 did not select the mixed-mask path")
+       }
+       for i, want := range expected {
+               if got := backing[i]; got != want {
+                       t.Fatalf("output[%d] = %d, want %d", i, got, want)
+               }
+       }
+       if got := backing[len(expected)]; got != 0xdeadbeef {
+               t.Fatalf("sentinel = %#x, want %#x", got, uint64(0xdeadbeef))
+       }
+}
+
+func testFilterUint64AlignedOffset(t *testing.T, filter filterUint64Func) {
+       t.Helper()
+
+       const (
+               offset = 8
+               length = 64
+       )
+       values := make([]uint64, length)
+       for i := range values {
+               values[i] = uint64(i)
+       }
+       filterData := make([]byte, offset/8+length/8)
+       for i := range filterData[offset/8:] {
+               filterData[offset/8+i] = 0x55
+       }
+
+       output := make([]uint64, length/2)
+       if !filter(values, output, filterData, offset, length) {
+               t.Fatal("filterUint64 did not select the aligned-offset path")
+       }
+       for i, got := range output {
+               want := uint64(2 * i)
+               if got != want {
+                       t.Fatalf("output[%d] = %d, want %d", i, got, want)
+               }
+       }
+}
+
+func testFilterUint64Guards(t *testing.T, filter filterUint64Func) {
+       t.Helper()
+
+       values := make([]uint64, 128)
+       mixed := make([]byte, 16)
+       for i := range mixed {
+               mixed[i] = 0x55
+       }
+       output := make([]uint64, 64)
+
+       tests := []struct {
+               name   string
+               filter []byte
+               offset int64
+               length int64
+               want   bool
+       }{
+               {name: "short", filter: mixed, length: 56},
+               {name: "non-multiple-of-eight", filter: mixed, length: 71},
+               {name: "unaligned-filter-offset", filter: mixed, offset: 3, 
length: 64},
+               {name: "all-false", filter: make([]byte, 8), length: 64},
+               {name: "all-true", filter: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 
0xff, 0xff, 0xff}, length: 64},
+               {name: "too-few-mixed-bytes", filter: []byte{0x55, 0x55, 0x55, 
0x00, 0x00, 0x00, 0x00, 0x00}, length: 64},
+               {name: "out-of-bounds-filter", filter: mixed[:4], length: 64},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if got := filter(values, output, tt.filter, tt.offset, 
tt.length); got != tt.want {
+                               t.Fatalf("filterUint64() = %t, want %t", got, 
tt.want)
+                       }
+               })
+       }
+}
diff --git a/arrow/compute/internal/kernels/filter_uint64_avx2_amd64.go 
b/arrow/compute/internal/kernels/filter_uint64_avx2_amd64.go
new file mode 100644
index 00000000..a1733bb6
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint64_avx2_amd64.go
@@ -0,0 +1,92 @@
+// 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.18 && amd64 && !noasm && !appengine
+
+package kernels
+
+import (
+       "encoding/binary"
+       "math/bits"
+       "unsafe"
+
+       "golang.org/x/sys/cpu"
+)
+
+var filterUint64Tables = makeFilterUint64Tables()
+
+func makeFilterUint64Tables() (tables [688]byte) {
+       for mask := 0; mask < 16; mask++ {
+               pos := 0
+               for lane := 0; lane < 4; lane++ {
+                       if mask&(1<<uint(lane)) == 0 {
+                               continue
+                       }
+                       binary.LittleEndian.PutUint32(tables[mask*32+pos*8:], 
uint32(lane*2))
+                       binary.LittleEndian.PutUint32(tables[mask*32+pos*8+4:], 
uint32(lane*2+1))
+                       pos++
+               }
+       }
+
+       for count := 1; count <= 4; count++ {
+               for lane := 0; lane < count*2; lane++ {
+                       
binary.LittleEndian.PutUint32(tables[512+count*32+lane*4:], ^uint32(0))
+               }
+       }
+       for mask := 0; mask < 16; mask++ {
+               tables[672+mask] = byte(bits.OnesCount8(uint8(mask)))
+       }
+       return tables
+}
+
+//go:noescape
+func _filter_uint64_avx2(values, filter, output, tables unsafe.Pointer, length 
int64)
+
+func filterUint64Avx2(values []uint64, output []uint64, filterData []byte, 
filterOffset, length int64) bool {
+       if !cpu.X86.HasAVX2 || length < 64 || length%8 != 0 || filterOffset%8 
!= 0 {
+               return false
+       }
+
+       numBytes := length / 8
+       filterByteOffset := filterOffset / 8
+       if filterByteOffset < 0 || filterByteOffset+numBytes > 
int64(len(filterData)) {
+               return false
+       }
+
+       mixedBytes := 0
+       const sampleBytes = 64
+       for i := int64(0); i < numBytes && i < sampleBytes; i++ {
+               mask := filterData[filterByteOffset+i]
+               if mask != 0 && mask != 0xff {
+                       mixedBytes++
+                       if mixedBytes == 4 {
+                               break
+                       }
+               }
+       }
+       if mixedBytes < 4 || len(output) == 0 {
+               return false
+       }
+
+       _filter_uint64_avx2(
+               unsafe.Pointer(&values[0]),
+               unsafe.Pointer(&filterData[filterByteOffset]),
+               unsafe.Pointer(&output[0]),
+               unsafe.Pointer(&filterUint64Tables[0]),
+               length,
+       )
+       return true
+}
diff --git a/arrow/compute/internal/kernels/filter_uint64_avx2_amd64.s 
b/arrow/compute/internal/kernels/filter_uint64_avx2_amd64.s
new file mode 100644
index 00000000..c1b87d32
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint64_avx2_amd64.s
@@ -0,0 +1,79 @@
+//go:build go1.18 && amd64 && !noasm && !appengine
+// AUTO-GENERATED BY C2GOASM -- DO NOT EDIT
+
+TEXT ·_filter_uint64_avx2(SB), $0-40
+
+       MOVQ values+0(FP), DI
+       MOVQ filter+8(FP), SI
+       MOVQ output+16(FP), DX
+       MOVQ tables+24(FP), CX
+       MOVQ length+32(FP), R8
+
+       LONG $0x07408d49         // lea    rax, [r8 + 7]
+       WORD $0x854d; BYTE $0xc0 // test    r8, r8
+       LONG $0xc0490f49         // cmovns    rax, r8
+       LONG $0x08f88349         // cmp    r8, 8
+       JL   LBB0_11
+       WORD $0x5641             // push    r14
+       BYTE $0x53               // push    rbx
+       LONG $0x03f8c148         // sar    rax, 3
+       LONG $0x20c78348         // add    rdi, 32
+       WORD $0x3145; BYTE $0xc0 // xor    r8d, r8d
+       WORD $0x3145; BYTE $0xc9 // xor    r9d, r9d
+       JMP  LBB0_2
+
+LBB0_4:
+       LONG $0x476ffec5; BYTE $0xe0               // vmovdqu    ymm0, yword 
[rdi - 32]
+       LONG $0x0f6ffec5                           // vmovdqu    ymm1, yword 
[rdi]
+       LONG $0x7f7ea1c4; WORD $0xc204             // vmovdqu    yword [rdx + 
8*r8], ymm0
+       LONG $0x7f7ea1c4; WORD $0xc24c; BYTE $0x20 // vmovdqu    yword [rdx + 
8*r8 + 32], ymm1
+       LONG $0x08c08349                           // add    r8, 8
+
+LBB0_9:
+       WORD $0xff49; BYTE $0xc1 // inc    r9
+       LONG $0x40c78348         // add    rdi, 64
+       WORD $0x394c; BYTE $0xc8 // cmp    rax, r9
+       JE   LBB0_10
+
+LBB0_2:
+       LONG $0x14b60f46; BYTE $0x0e               // movzx    r10d, byte [rsi 
+ r9]
+       WORD $0x8545; BYTE $0xd2                   // test    r10d, r10d
+       JE   LBB0_9
+       LONG $0xfffa8141; WORD $0x0000; BYTE $0x00 // cmp    r10d, 255
+       JE   LBB0_4
+       WORD $0x8945; BYTE $0xd6                   // mov    r14d, r10d
+       LONG $0x0fe68341                           // and    r14d, 15
+       LONG $0x04eac141                           // shr    r10d, 4
+       QUAD $0x0002a0319cb60f42; BYTE $0x00       // movzx    ebx, byte [rcx + 
r14 + 672]
+       QUAD $0x0002a0119cb60f46; BYTE $0x00       // movzx    r11d, byte [rcx 
+ r10 + 672]
+       WORD $0x8548; BYTE $0xdb                   // test    rbx, rbx
+       JE   LBB0_7
+       LONG $0x05e6c141                           // shl    r14d, 5
+       LONG $0x6f7ea1c4; WORD $0x3104             // vmovdqu    ymm0, yword 
[rcx + r14]
+       LONG $0x367de2c4; WORD $0xe047             // vpermd    ymm0, ymm0, 
yword [rdi - 32]
+       WORD $0x8941; BYTE $0xde                   // mov    r14d, ebx
+       LONG $0x05e6c141                           // shl    r14d, 5
+       QUAD $0x0200318c6f7ea1c4; WORD $0x0000     // vmovdqu    ymm1, yword 
[rcx + r14 + 512]
+       LONG $0x8e75a2c4; WORD $0xc204             // vpmaskmovd    yword [rdx 
+ 8*r8], ymm1, ymm0
+       WORD $0x0149; BYTE $0xd8                   // add    r8, rbx
+
+LBB0_7:
+       WORD $0x854d; BYTE $0xdb               // test    r11, r11
+       JE   LBB0_9
+       LONG $0x05e2c141                       // shl    r10d, 5
+       LONG $0x6f7ea1c4; WORD $0x1104         // vmovdqu    ymm0, yword [rcx + 
r10]
+       LONG $0x367de2c4; BYTE $0x07           // vpermd    ymm0, ymm0, yword 
[rdi]
+       WORD $0x8945; BYTE $0xda               // mov    r10d, r11d
+       LONG $0x05e2c141                       // shl    r10d, 5
+       QUAD $0x0200118c6f7ea1c4; WORD $0x0000 // vmovdqu    ymm1, yword [rcx + 
r10 + 512]
+       LONG $0x8e75a2c4; WORD $0xc204         // vpmaskmovd    yword [rdx + 
8*r8], ymm1, ymm0
+       WORD $0x014d; BYTE $0xd8               // add    r8, r11
+       JMP  LBB0_9
+
+LBB0_10:
+       BYTE $0x5b   // pop    rbx
+       WORD $0x5e41 // pop    r14
+
+LBB0_11:
+       VZEROUPPER
+       RET
diff --git a/arrow/compute/internal/kernels/filter_uint64_avx2_amd64_test.go 
b/arrow/compute/internal/kernels/filter_uint64_avx2_amd64_test.go
new file mode 100644
index 00000000..7ba53948
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint64_avx2_amd64_test.go
@@ -0,0 +1,46 @@
+// 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.18 && amd64 && !noasm && !appengine
+
+package kernels
+
+import (
+       "testing"
+
+       "golang.org/x/sys/cpu"
+)
+
+func TestFilterUint64Avx2AllMasks(t *testing.T) {
+       if !cpu.X86.HasAVX2 {
+               t.Skip("AVX2 is not available")
+       }
+       testFilterUint64AllMasks(t, filterUint64Avx2)
+}
+
+func TestFilterUint64Avx2AlignedOffset(t *testing.T) {
+       if !cpu.X86.HasAVX2 {
+               t.Skip("AVX2 is not available")
+       }
+       testFilterUint64AlignedOffset(t, filterUint64Avx2)
+}
+
+func TestFilterUint64Avx2Guards(t *testing.T) {
+       if !cpu.X86.HasAVX2 {
+               t.Skip("AVX2 is not available")
+       }
+       testFilterUint64Guards(t, filterUint64Avx2)
+}
diff --git a/arrow/compute/internal/kernels/filter_uint64_avx2_noasm.go 
b/arrow/compute/internal/kernels/filter_uint64_avx2_noasm.go
new file mode 100644
index 00000000..842c185a
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint64_avx2_noasm.go
@@ -0,0 +1,23 @@
+// 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.18 && (noasm || !amd64 || appengine)
+
+package kernels
+
+func filterUint64Avx2([]uint64, []uint64, []byte, int64, int64) bool {
+       return false
+}
diff --git a/arrow/compute/internal/kernels/filter_uint64_neon_arm64.go 
b/arrow/compute/internal/kernels/filter_uint64_neon_arm64.go
new file mode 100644
index 00000000..08766040
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint64_neon_arm64.go
@@ -0,0 +1,92 @@
+// 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.18 && arm64 && !noasm && !appengine
+
+package kernels
+
+import (
+       "math/bits"
+       "unsafe"
+
+       "golang.org/x/sys/cpu"
+)
+
+var filterUint64NeonTables = makeFilterUint64NeonTables()
+
+func makeFilterUint64NeonTables() (tables [528]byte) {
+       for mask := 0; mask < 16; mask++ {
+               for i := 0; i < 32; i++ {
+                       tables[mask*32+i] = 0x80
+               }
+
+               pos := 0
+               for lane := 0; lane < 4; lane++ {
+                       if mask&(1<<uint(lane)) == 0 {
+                               continue
+                       }
+                       vectorOffset := mask*32 + (pos/2)*16 + (pos%2)*8
+                       for byteInLane := 0; byteInLane < 8; byteInLane++ {
+                               tables[vectorOffset+byteInLane] = byte(lane*8 + 
byteInLane)
+                       }
+                       pos++
+               }
+       }
+
+       for mask := 0; mask < 16; mask++ {
+               tables[512+mask] = byte(bits.OnesCount8(uint8(mask)))
+       }
+       return tables
+}
+
+//go:noescape
+func _filter_uint64_neon(values, filter, output, tables unsafe.Pointer, length 
int64)
+
+func filterUint64Neon(values []uint64, output []uint64, filterData []byte, 
filterOffset, length int64) bool {
+       if !cpu.ARM64.HasASIMD || length < 64 || length%8 != 0 || 
filterOffset%8 != 0 {
+               return false
+       }
+
+       numBytes := length / 8
+       filterByteOffset := filterOffset / 8
+       if filterByteOffset < 0 || filterByteOffset+numBytes > 
int64(len(filterData)) {
+               return false
+       }
+
+       mixedBytes := 0
+       const sampleBytes = 64
+       for i := int64(0); i < numBytes && i < sampleBytes; i++ {
+               mask := filterData[filterByteOffset+i]
+               if mask != 0 && mask != 0xff {
+                       mixedBytes++
+                       if mixedBytes == 4 {
+                               break
+                       }
+               }
+       }
+       if mixedBytes < 4 || len(output) == 0 {
+               return false
+       }
+
+       _filter_uint64_neon(
+               unsafe.Pointer(&values[0]),
+               unsafe.Pointer(&filterData[filterByteOffset]),
+               unsafe.Pointer(&output[0]),
+               unsafe.Pointer(&filterUint64NeonTables[0]),
+               length,
+       )
+       return true
+}
diff --git a/arrow/compute/internal/kernels/filter_uint64_neon_arm64.s 
b/arrow/compute/internal/kernels/filter_uint64_neon_arm64.s
new file mode 100644
index 00000000..0f51e128
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint64_neon_arm64.s
@@ -0,0 +1,160 @@
+// 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.18 && arm64 && !noasm && !appengine
+
+#include "textflag.h"
+
+// Each filter byte selects up to four uint64 values from each of two
+// four-value halves. The table-driven VTBL compacts one half into two
+// vectors. The stores write exactly the selected number of bytes.
+
+// func _filter_uint64_neon(values, filter, output, tables unsafe.Pointer, 
length int64)
+TEXT ·_filter_uint64_neon(SB), NOSPLIT|NOFRAME, $0-40
+       MOVD values+0(FP), R0
+       MOVD filter+8(FP), R1
+       MOVD output+16(FP), R2
+       MOVD tables+24(FP), R3
+       MOVD length+32(FP), R4
+
+       LSR $3, R4, R4
+
+filter_loop:
+       MOVBU (R1), R5
+       ADD $1, R1
+       CBZ R5, filter_next
+       CMPW $255, R5
+       BEQ filter_all
+
+       ANDW $15, R5, R6
+       ADD $512, R3, R8
+       MOVBU (R8)(R6<<0), R9
+       CBZ R9, filter_high
+
+       VLD1 (R0), [V0.D2]
+       ADD $16, R0, R8
+       VLD1 (R8), [V1.D2]
+
+       LSL $5, R6, R10
+       ADD R3, R10, R10
+       VLD1 (R10), [V2.B16]
+       VTBL V2.B16, [V0.B16, V1.B16], V4.B16
+       ADD $16, R10, R10
+       VLD1 (R10), [V2.B16]
+       VTBL V2.B16, [V0.B16, V1.B16], V5.B16
+
+       CMPW $1, R9
+       BEQ filter_low_store1
+       CMPW $2, R9
+       BEQ filter_low_store2
+       CMPW $3, R9
+       BEQ filter_low_store3
+       VST1 [V4.D2], (R2)
+       ADD $16, R2
+       VST1 [V5.D2], (R2)
+       ADD $16, R2
+       B filter_high
+
+filter_low_store1:
+       VMOV V4.D[0], R8
+       MOVD R8, (R2)
+       ADD $8, R2
+       B filter_high
+
+filter_low_store2:
+       VST1 [V4.D2], (R2)
+       ADD $16, R2
+       B filter_high
+
+filter_low_store3:
+       VST1 [V4.D2], (R2)
+       ADD $16, R2
+       VMOV V5.D[0], R8
+       MOVD R8, (R2)
+       ADD $8, R2
+
+filter_high:
+       LSR $4, R5, R6
+       ADD $512, R3, R8
+       MOVBU (R8)(R6<<0), R9
+       CBZ R9, filter_next
+
+       ADD $32, R0, R8
+       VLD1 (R8), [V0.D2]
+       ADD $48, R0, R8
+       VLD1 (R8), [V1.D2]
+
+       LSL $5, R6, R10
+       ADD R3, R10, R10
+       VLD1 (R10), [V2.B16]
+       VTBL V2.B16, [V0.B16, V1.B16], V4.B16
+       ADD $16, R10, R10
+       VLD1 (R10), [V2.B16]
+       VTBL V2.B16, [V0.B16, V1.B16], V5.B16
+
+       CMPW $1, R9
+       BEQ filter_high_store1
+       CMPW $2, R9
+       BEQ filter_high_store2
+       CMPW $3, R9
+       BEQ filter_high_store3
+       VST1 [V4.D2], (R2)
+       ADD $16, R2
+       VST1 [V5.D2], (R2)
+       ADD $16, R2
+       B filter_next
+
+filter_high_store1:
+       VMOV V4.D[0], R8
+       MOVD R8, (R2)
+       ADD $8, R2
+       B filter_next
+
+filter_high_store2:
+       VST1 [V4.D2], (R2)
+       ADD $16, R2
+       B filter_next
+
+filter_high_store3:
+       VST1 [V4.D2], (R2)
+       ADD $16, R2
+       VMOV V5.D[0], R8
+       MOVD R8, (R2)
+       ADD $8, R2
+       B filter_next
+
+filter_all:
+       VLD1 (R0), [V0.D2]
+       ADD $16, R0, R8
+       VLD1 (R8), [V1.D2]
+       ADD $32, R0, R8
+       VLD1 (R8), [V2.D2]
+       ADD $48, R0, R8
+       VLD1 (R8), [V3.D2]
+       VST1 [V0.D2], (R2)
+       ADD $16, R2
+       VST1 [V1.D2], (R2)
+       ADD $16, R2
+       VST1 [V2.D2], (R2)
+       ADD $16, R2
+       VST1 [V3.D2], (R2)
+       ADD $16, R2
+
+filter_next:
+       ADD $64, R0
+       SUBS $1, R4, R4
+       BNE filter_loop
+       RET
diff --git a/arrow/compute/internal/kernels/filter_uint64_neon_arm64_test.go 
b/arrow/compute/internal/kernels/filter_uint64_neon_arm64_test.go
new file mode 100644
index 00000000..2eae877c
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint64_neon_arm64_test.go
@@ -0,0 +1,46 @@
+// 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.18 && arm64 && !noasm && !appengine
+
+package kernels
+
+import (
+       "testing"
+
+       "golang.org/x/sys/cpu"
+)
+
+func TestFilterUint64NeonAllMasks(t *testing.T) {
+       if !cpu.ARM64.HasASIMD {
+               t.Skip("ARM64 SIMD is not available")
+       }
+       testFilterUint64AllMasks(t, filterUint64Neon)
+}
+
+func TestFilterUint64NeonAlignedOffset(t *testing.T) {
+       if !cpu.ARM64.HasASIMD {
+               t.Skip("ARM64 SIMD is not available")
+       }
+       testFilterUint64AlignedOffset(t, filterUint64Neon)
+}
+
+func TestFilterUint64NeonGuards(t *testing.T) {
+       if !cpu.ARM64.HasASIMD {
+               t.Skip("ARM64 SIMD is not available")
+       }
+       testFilterUint64Guards(t, filterUint64Neon)
+}
diff --git a/arrow/compute/internal/kernels/filter_uint64_neon_noasm.go 
b/arrow/compute/internal/kernels/filter_uint64_neon_noasm.go
new file mode 100644
index 00000000..1459d23e
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint64_neon_noasm.go
@@ -0,0 +1,23 @@
+// 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.18 && (noasm || !arm64 || appengine)
+
+package kernels
+
+func filterUint64Neon([]uint64, []uint64, []byte, int64, int64) bool {
+       return false
+}
diff --git a/arrow/compute/internal/kernels/vector_selection.go 
b/arrow/compute/internal/kernels/vector_selection.go
index 32a6f4b7..8ea29377 100644
--- a/arrow/compute/internal/kernels/vector_selection.go
+++ b/arrow/compute/internal/kernels/vector_selection.go
@@ -495,6 +495,14 @@ func PrimitiveFilter(ctx *exec.KernelCtx, batch 
*exec.ExecSpan, out *exec.ExecRe
                        return nil
                }
        }
+       if bitWidth == 64 && values.Nulls == 0 && filter.Nulls == 0 {
+               valuesData := exec.GetSpanValues[uint64](values, 1)
+               outData := exec.GetSpanValues[uint64](out, 1)
+               if filterUint64Avx2(valuesData, outData, filter.Buffers[1].Buf, 
filter.Offset, values.Len) ||
+                       filterUint64Neon(valuesData, outData, 
filter.Buffers[1].Buf, filter.Offset, values.Len) {
+                       return nil
+               }
+       }
 
        var wr writeFiltered
        switch bitWidth {
diff --git a/arrow/compute/vector_selection_bench_test.go 
b/arrow/compute/vector_selection_bench_test.go
index 4a14f6ad..8bef0201 100644
--- a/arrow/compute/vector_selection_bench_test.go
+++ b/arrow/compute/vector_selection_bench_test.go
@@ -39,8 +39,28 @@ func BenchmarkFilterFloat32MixedMasks(b *testing.B) {
        benchmarkFilter32MixedMasks(b, makeFilterFloat32BenchmarkInput)
 }
 
+func BenchmarkFilterInt64MixedMasks(b *testing.B) {
+       benchmarkFilter64MixedMasks(b, makeFilterInt64BenchmarkInput)
+}
+
+func BenchmarkFilterFloat64MixedMasks(b *testing.B) {
+       benchmarkFilter64MixedMasks(b, makeFilterFloat64BenchmarkInput)
+}
+
 func benchmarkFilter32MixedMasks(b *testing.B,
        makeInput func(*testing.B, int, func(int) bool) (arrow.Array, 
arrow.Array),
+) {
+       benchmarkFilterMixedMasks(b, makeInput, 4)
+}
+
+func benchmarkFilter64MixedMasks(b *testing.B,
+       makeInput func(*testing.B, int, func(int) bool) (arrow.Array, 
arrow.Array),
+) {
+       benchmarkFilterMixedMasks(b, makeInput, 8)
+}
+
+func benchmarkFilterMixedMasks(b *testing.B,
+       makeInput func(*testing.B, int, func(int) bool) (arrow.Array, 
arrow.Array), valueBytes int,
 ) {
        patterns := []struct {
                name     string
@@ -65,7 +85,7 @@ func benchmarkFilter32MixedMasks(b *testing.B,
                                defer filter.Release()
 
                                b.ReportAllocs()
-                               b.SetBytes(int64(size * 4))
+                               b.SetBytes(int64(size * valueBytes))
                                b.ResetTimer()
                                for i := 0; i < b.N; i++ {
                                        result, err := 
compute.FilterArray(context.Background(), values, filter, 
*compute.DefaultFilterOptions())
@@ -166,6 +186,50 @@ func makeFilterFloat32BenchmarkInput(b *testing.B, size 
int, selected func(int)
        return values, filter
 }
 
+func makeFilterInt64BenchmarkInput(b *testing.B, size int, selected func(int) 
bool) (arrow.Array, arrow.Array) {
+       b.Helper()
+       mem := memory.DefaultAllocator
+
+       valuesBuilder := array.NewInt64Builder(mem)
+       valuesBuilder.Reserve(size)
+       for i := 0; i < size; i++ {
+               valuesBuilder.Append(int64(i))
+       }
+       values := valuesBuilder.NewInt64Array()
+       valuesBuilder.Release()
+
+       filterBuilder := array.NewBooleanBuilder(mem)
+       filterBuilder.Reserve(size)
+       for i := 0; i < size; i++ {
+               filterBuilder.Append(selected(i))
+       }
+       filter := filterBuilder.NewBooleanArray()
+       filterBuilder.Release()
+       return values, filter
+}
+
+func makeFilterFloat64BenchmarkInput(b *testing.B, size int, selected 
func(int) bool) (arrow.Array, arrow.Array) {
+       b.Helper()
+       mem := memory.DefaultAllocator
+
+       valuesBuilder := array.NewFloat64Builder(mem)
+       valuesBuilder.Reserve(size)
+       for i := 0; i < size; i++ {
+               valuesBuilder.Append(float64(i))
+       }
+       values := valuesBuilder.NewFloat64Array()
+       valuesBuilder.Release()
+
+       filterBuilder := array.NewBooleanBuilder(mem)
+       filterBuilder.Reserve(size)
+       for i := 0; i < size; i++ {
+               filterBuilder.Append(selected(i))
+       }
+       filter := filterBuilder.NewBooleanArray()
+       filterBuilder.Release()
+       return values, filter
+}
+
 func filterBenchmarkRandom(i, selectedPercent int) bool {
        x := uint32(i)*747796405 + 2891336453
        x = ((x >> ((x >> 28) + 4)) ^ x) * 277803737

Reply via email to