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 e9cc75ce perf(compute): vectorize mixed int32 filters with AVX2 (#1284)
e9cc75ce is described below

commit e9cc75ce43a80350885c1a488dc1e160d082df13
Author: Minh Vu <[email protected]>
AuthorDate: Thu Sep 3 17:55:13 2026 +0200

    perf(compute): vectorize mixed int32 filters with AVX2 (#1284)
    
    ## Summary
    
    * **Add an AVX2 path** for filtering 32-bit fixed-width values with
    mixed selection masks.
    * **Compress two groups of four values** with `vpshufb` and use masked
    stores for the compact output.
    * **Keep the current path** for nulls, short or unaligned inputs, dense
    masks, unsupported CPUs, and `noasm` builds.
    * **Add tests and a benchmark** for mixed masks, offsets, and 1K, 64K,
    and 1M rows.
    
    ## Benchmark
    
    The benchmark is in `arrow/compute/vector_selection_bench_test.go`.
    
    It covers alternating, random 25/50/75%, clustered 50%, all-selected,
    and none-selected masks at 1K, 64K, and 1M rows.
    
    A native AVX2 run needs an x86_64 machine with AVX2. The benchmark also
    runs on the local Apple M1, but that only exercises the existing arm64
    fallback, so no local AVX2 timing is included.
    
    ## Tests
    
    * `go test ./arrow/compute/internal/kernels ./arrow/compute`
    * `go test -tags noasm ./arrow/compute/internal/kernels ./arrow/compute`
    * `go test -race ./arrow/compute/internal/kernels ./arrow/compute`
    * amd64 cross-build for both packages
---
 arrow/compute/internal/kernels/Makefile            |   8 +-
 .../compute/internal/kernels/_lib/filter_uint32.cc |  75 ++++++++++++++++
 .../kernels/_lib/filter_uint32_avx2_amd64.s        |  84 +++++++++++++++++
 .../internal/kernels/filter_uint32_avx2_amd64.go   |  99 ++++++++++++++++++++
 .../internal/kernels/filter_uint32_avx2_amd64.s    |  81 +++++++++++++++++
 .../internal/kernels/filter_uint32_noasm.go        |  23 +++++
 arrow/compute/internal/kernels/vector_selection.go |   7 ++
 arrow/compute/vector_selection_bench_test.go       | 100 +++++++++++++++++++++
 arrow/compute/vector_selection_test.go             |  47 ++++++++++
 9 files changed, 523 insertions(+), 1 deletion(-)

diff --git a/arrow/compute/internal/kernels/Makefile 
b/arrow/compute/internal/kernels/Makefile
index 4e8ddd85..1edf1270 100644
--- a/arrow/compute/internal/kernels/Makefile
+++ b/arrow/compute/internal/kernels/Makefile
@@ -39,7 +39,7 @@ 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
+       scalar_comparison_avx2_amd64.s scalar_comparison_sse4_amd64.s 
filter_uint32_avx2_amd64.s
 
 #
 # ARROW-15336: DO NOT add the assembly target for Arm64 (ARM_SOURCES) until 
c2goasm added the Arm64 support.
@@ -70,6 +70,9 @@ _lib/scalar_comparison_avx2_amd64.s: _lib/scalar_comparison.cc
 _lib/scalar_comparison_sse4_amd64.s: _lib/scalar_comparison.cc
        $(CXX) -std=c++17 -S $(C_FLAGS) $(ASM_FLAGS_SSE4) $^ -o $@ ; 
$(PERL_FIXUP_ROTATE) $@
 
+_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/base_arithmetic_neon.s: _lib/base_arithmetic.cc
        $(CXX) -std=c++17 -S $(C_FLAGS_NEON) $^ -o $@ ; $(PERL_FIXUP_ROTATE) $@
 
@@ -106,6 +109,9 @@ scalar_comparison_avx2_amd64.s: 
_lib/scalar_comparison_avx2_amd64.s
 scalar_comparison_sse4_amd64.s: _lib/scalar_comparison_sse4_amd64.s
        $(C2GOASM) -a -f $^ $@
 
+filter_uint32_avx2_amd64.s: _lib/filter_uint32_avx2_amd64.s
+       $(C2GOASM) -a -f $^ $@
+
 clean:
        rm -f $(INTEL_SOURCES)
        rm -f $(addprefix _lib/,$(INTEL_SOURCES))
diff --git a/arrow/compute/internal/kernels/_lib/filter_uint32.cc 
b/arrow/compute/internal/kernels/_lib/filter_uint32.cc
new file mode 100644
index 00000000..2e72ec3d
--- /dev/null
+++ b/arrow/compute/internal/kernels/_lib/filter_uint32.cc
@@ -0,0 +1,75 @@
+// 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_uint32)(const uint32_t* values,
+                                           const uint8_t* filter,
+                                           uint32_t* output,
+                                           const uint8_t* tables,
+                                           const int64_t length) {
+    const uint8_t* shuffle_masks = tables;
+    const int32_t* store_masks = reinterpret_cast<const int32_t*>(tables + 
256);
+    const uint8_t* popcount = tables + 336;
+    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 uint32_t* input = values + i * 8;
+
+        if (mask == 0) {
+            continue;
+        }
+
+        if (mask == 0xff) {
+            const __m256i low = _mm256_loadu_si256(reinterpret_cast<const 
__m256i*>(input));
+            _mm256_storeu_si256(reinterpret_cast<__m256i*>(output + 
output_length), low);
+            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];
+        const __m128i low_values = _mm_loadu_si128(reinterpret_cast<const 
__m128i*>(input));
+        const __m128i high_values = _mm_loadu_si128(reinterpret_cast<const 
__m128i*>(input + 4));
+
+        if (low_count != 0) {
+            const __m128i shuffle = _mm_loadu_si128(
+                reinterpret_cast<const __m128i*>(shuffle_masks + low_mask * 
16));
+            const __m128i compacted = _mm_shuffle_epi8(low_values, shuffle);
+            const __m128i store_mask = _mm_loadu_si128(
+                reinterpret_cast<const __m128i*>(store_masks + low_count * 4));
+            _mm_maskstore_epi32(reinterpret_cast<int*>(output + 
output_length), store_mask, compacted);
+            output_length += low_count;
+        }
+
+        if (high_count != 0) {
+            const __m128i shuffle = _mm_loadu_si128(
+                reinterpret_cast<const __m128i*>(shuffle_masks + high_mask * 
16));
+            const __m128i compacted = _mm_shuffle_epi8(high_values, shuffle);
+            const __m128i store_mask = _mm_loadu_si128(
+                reinterpret_cast<const __m128i*>(store_masks + high_count * 
4));
+            _mm_maskstore_epi32(reinterpret_cast<int*>(output + 
output_length), store_mask, compacted);
+            output_length += high_count;
+        }
+    }
+
+}
diff --git a/arrow/compute/internal/kernels/_lib/filter_uint32_avx2_amd64.s 
b/arrow/compute/internal/kernels/_lib/filter_uint32_avx2_amd64.s
new file mode 100644
index 00000000..506e00cd
--- /dev/null
+++ b/arrow/compute/internal/kernels/_lib/filter_uint32_avx2_amd64.s
@@ -0,0 +1,84 @@
+       .intel_syntax noprefix
+       .file   "filter_uint32.cc"
+       .text
+       .globl  filter_uint32_avx2              # -- Begin function 
filter_uint32_avx2
+       .p2align        4
+       .type   filter_uint32_avx2,@function
+filter_uint32_avx2:                     # @filter_uint32_avx2
+# %bb.0:
+       lea     rax, [r8 + 7]
+       test    r8, r8
+       cmovns  rax, r8
+       cmp     r8, 8
+       jl      .LBB0_11
+# %bb.1:
+       push    rbp
+       push    r14
+       push    rbx
+       sar     rax, 3
+       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]
+       vmovdqu ymmword ptr [rdx + 4*r8], ymm0
+       add     r8, 8
+.LBB0_9:                                #   in Loop: Header=BB0_2 Depth=1
+       inc     r9
+       add     rdi, 32
+       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     r11d, r10d
+       and     r11d, 15
+       mov     r14d, r10d
+       shr     r14d, 4
+       movzx   ebx, byte ptr [rcx + r11 + 336]
+       movzx   r11d, byte ptr [rcx + r14 + 336]
+       vmovdqu xmm0, xmmword ptr [rdi + 16]
+       test    rbx, rbx
+       je      .LBB0_7
+# %bb.6:                                #   in Loop: Header=BB0_2 Depth=1
+       vmovdqu xmm1, xmmword ptr [rdi]
+       mov     ebp, r10d
+       shl     bpl, 4
+       movzx   r14d, bpl
+       vpshufb xmm1, xmm1, xmmword ptr [rcx + r14]
+       mov     r14d, ebx
+       shl     r14d, 4
+       vmovdqu xmm2, xmmword ptr [rcx + r14 + 256]
+       vpmaskmovd      xmmword ptr [rdx + 4*r8], xmm2, xmm1
+       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
+       and     r10d, -16
+       vpshufb xmm0, xmm0, xmmword ptr [rcx + r10]
+       mov     r10d, r11d
+       shl     r10d, 4
+       vmovdqu xmm1, xmmword ptr [rcx + r10 + 256]
+       vpmaskmovd      xmmword ptr [rdx + 4*r8], xmm1, xmm0
+       add     r8, r11
+       jmp     .LBB0_9
+.LBB0_10:
+       pop     rbx
+       pop     r14
+       pop     rbp
+.LBB0_11:
+       vzeroupper
+       ret
+.Lfunc_end0:
+       .size   filter_uint32_avx2, .Lfunc_end0-filter_uint32_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_uint32_avx2_amd64.go 
b/arrow/compute/internal/kernels/filter_uint32_avx2_amd64.go
new file mode 100644
index 00000000..4d7a283b
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint32_avx2_amd64.go
@@ -0,0 +1,99 @@
+// 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 filterUint32Tables = makeFilterUint32Tables()
+
+func makeFilterUint32Tables() (tables [352]byte) {
+       for mask := 0; mask < 16; mask++ {
+               pos := 0
+               for lane := 0; lane < 4; lane++ {
+                       if mask&(1<<uint(lane)) == 0 {
+                               continue
+                       }
+                       for byteInLane := 0; byteInLane < 4; byteInLane++ {
+                               tables[mask*16+pos] = byte(lane*4 + byteInLane)
+                               pos++
+                       }
+               }
+               for ; pos < 16; pos++ {
+                       tables[mask*16+pos] = 0x80
+               }
+       }
+
+       for count := 1; count <= 4; count++ {
+               for lane := 0; lane < count; lane++ {
+                       
binary.LittleEndian.PutUint32(tables[256+count*16+lane*4:], ^uint32(0))
+               }
+       }
+       for mask := 0; mask < 16; mask++ {
+               tables[336+mask] = byte(bits.OnesCount8(uint8(mask)))
+       }
+       return tables
+}
+
+//go:noescape
+func _filter_uint32_avx2(values, filter, output, tables unsafe.Pointer, length 
int64)
+
+func filterUint32Avx2(values []uint32, output []uint32, 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 {
+               return false
+       }
+
+       if len(output) == 0 {
+               return false
+       }
+       _filter_uint32_avx2(
+               unsafe.Pointer(&values[0]),
+               unsafe.Pointer(&filterData[filterByteOffset]),
+               unsafe.Pointer(&output[0]),
+               unsafe.Pointer(&filterUint32Tables[0]),
+               length,
+       )
+       return true
+}
diff --git a/arrow/compute/internal/kernels/filter_uint32_avx2_amd64.s 
b/arrow/compute/internal/kernels/filter_uint32_avx2_amd64.s
new file mode 100644
index 00000000..6d6c0fca
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint32_avx2_amd64.s
@@ -0,0 +1,81 @@
+//go:build go1.18 && amd64 && !noasm && !appengine
+// AUTO-GENERATED BY C2GOASM -- DO NOT EDIT
+
+TEXT ยท_filter_uint32_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
+       BYTE $0x55               // push    rbp
+       WORD $0x5641             // push    r14
+       BYTE $0x53               // push    rbx
+       LONG $0x03f8c148         // sar    rax, 3
+       WORD $0x3145; BYTE $0xc0 // xor    r8d, r8d
+       WORD $0x3145; BYTE $0xc9 // xor    r9d, r9d
+       JMP  LBB0_2
+
+LBB0_4:
+       LONG $0x076ffec5               // vmovdqu    ymm0, yword [rdi]
+       LONG $0x7f7ea1c4; WORD $0x8204 // vmovdqu    yword [rdx + 4*r8], ymm0
+       LONG $0x08c08349               // add    r8, 8
+
+LBB0_9:
+       WORD $0xff49; BYTE $0xc1 // inc    r9
+       LONG $0x20c78348         // add    rdi, 32
+       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 $0xd3                   // mov    r11d, r10d
+       LONG $0x0fe38341                           // and    r11d, 15
+       WORD $0x8945; BYTE $0xd6                   // mov    r14d, r10d
+       LONG $0x04eec141                           // shr    r14d, 4
+       QUAD $0x000150199cb60f42; BYTE $0x00       // movzx    ebx, byte [rcx + 
r11 + 336]
+       QUAD $0x000150319cb60f46; BYTE $0x00       // movzx    r11d, byte [rcx 
+ r14 + 336]
+       LONG $0x476ffac5; BYTE $0x10               // vmovdqu    xmm0, oword 
[rdi + 16]
+       WORD $0x8548; BYTE $0xdb                   // test    rbx, rbx
+       JE   LBB0_7
+       LONG $0x0f6ffac5                           // vmovdqu    xmm1, oword 
[rdi]
+       WORD $0x8944; BYTE $0xd5                   // mov    ebp, r10d
+       LONG $0x04e5c040                           // shl    bpl, 4
+       LONG $0xf5b60f44                           // movzx    r14d, bpl
+       LONG $0x0071a2c4; WORD $0x310c             // vpshufb    xmm1, xmm1, 
oword [rcx + r14]
+       WORD $0x8941; BYTE $0xde                   // mov    r14d, ebx
+       LONG $0x04e6c141                           // shl    r14d, 4
+       QUAD $0x010031946f7aa1c4; WORD $0x0000     // vmovdqu    xmm2, oword 
[rcx + r14 + 256]
+       LONG $0x8e69a2c4; WORD $0x820c             // vpmaskmovd    oword [rdx 
+ 4*r8], xmm2, xmm1
+       WORD $0x0149; BYTE $0xd8                   // add    r8, rbx
+
+LBB0_7:
+       WORD $0x854d; BYTE $0xdb               // test    r11, r11
+       JE   LBB0_9
+       LONG $0xf0e28341                       // and    r10d, -16
+       LONG $0x0079a2c4; WORD $0x1104         // vpshufb    xmm0, xmm0, oword 
[rcx + r10]
+       WORD $0x8945; BYTE $0xda               // mov    r10d, r11d
+       LONG $0x04e2c141                       // shl    r10d, 4
+       QUAD $0x0100118c6f7aa1c4; WORD $0x0000 // vmovdqu    xmm1, oword [rcx + 
r10 + 256]
+       LONG $0x8e71a2c4; WORD $0x8204         // vpmaskmovd    oword [rdx + 
4*r8], xmm1, xmm0
+       WORD $0x014d; BYTE $0xd8               // add    r8, r11
+       JMP  LBB0_9
+
+LBB0_10:
+       BYTE $0x5b   // pop    rbx
+       WORD $0x5e41 // pop    r14
+       BYTE $0x5d   // pop    rbp
+
+LBB0_11:
+       VZEROUPPER
+       RET
diff --git a/arrow/compute/internal/kernels/filter_uint32_noasm.go 
b/arrow/compute/internal/kernels/filter_uint32_noasm.go
new file mode 100644
index 00000000..10f9b616
--- /dev/null
+++ b/arrow/compute/internal/kernels/filter_uint32_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 filterUint32Avx2([]uint32, []uint32, []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 cbe0c220..68198628 100644
--- a/arrow/compute/internal/kernels/vector_selection.go
+++ b/arrow/compute/internal/kernels/vector_selection.go
@@ -482,6 +482,13 @@ func PrimitiveFilter(ctx *exec.KernelCtx, batch 
*exec.ExecSpan, out *exec.ExecRe
        allocateValidity := values.Nulls != 0 || filter.Nulls != 0
        bitWidth := values.Type.(arrow.FixedWidthDataType).BitWidth()
        preallocateData(ctx, outputLength, bitWidth, allocateValidity, out)
+       if bitWidth == 32 && values.Nulls == 0 && filter.Nulls == 0 {
+               valuesData := exec.GetSpanValues[uint32](values, 1)
+               outData := exec.GetSpanValues[uint32](out, 1)
+               if filterUint32Avx2(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
new file mode 100644
index 00000000..b5e8993e
--- /dev/null
+++ b/arrow/compute/vector_selection_bench_test.go
@@ -0,0 +1,100 @@
+// 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
+
+package compute_test
+
+import (
+       "context"
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/compute"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+)
+
+var benchmarkFilterOutputLength int
+
+func BenchmarkFilterInt32MixedMasks(b *testing.B) {
+       patterns := []struct {
+               name     string
+               selected func(int) bool
+       }{
+               {name: "alternating", selected: func(i int) bool { return i%2 
== 0 }},
+               {name: "random25", selected: func(i int) bool { return 
filterBenchmarkRandom(i, 25) }},
+               {name: "random50", selected: func(i int) bool { return 
filterBenchmarkRandom(i, 50) }},
+               {name: "random75", selected: func(i int) bool { return 
filterBenchmarkRandom(i, 75) }},
+               {name: "clustered50", selected: func(i int) bool { return 
(i/32)%2 == 0 }},
+               {name: "all-selected", selected: func(int) bool { return true 
}},
+               {name: "none-selected", selected: func(int) bool { return false 
}},
+       }
+
+       for _, size := range []int{1 << 10, 1 << 16, 1 << 20} {
+               size := size
+               for _, pattern := range patterns {
+                       pattern := pattern
+                       b.Run(fmt.Sprintf("size=%d/%s", size, pattern.name), 
func(b *testing.B) {
+                               values, filter := 
makeFilterInt32BenchmarkInput(b, size, pattern.selected)
+                               defer values.Release()
+                               defer filter.Release()
+
+                               b.ReportAllocs()
+                               b.SetBytes(int64(size * 4))
+                               b.ResetTimer()
+                               for i := 0; i < b.N; i++ {
+                                       result, err := 
compute.FilterArray(context.Background(), values, filter, 
*compute.DefaultFilterOptions())
+                                       if err != nil {
+                                               b.Fatal(err)
+                                       }
+                                       benchmarkFilterOutputLength = 
result.Len()
+                                       result.Release()
+                               }
+                       })
+               }
+       }
+}
+
+func makeFilterInt32BenchmarkInput(b *testing.B, size int, selected func(int) 
bool) (arrow.Array, arrow.Array) {
+       b.Helper()
+       mem := memory.DefaultAllocator
+
+       valuesBuilder := array.NewInt32Builder(mem)
+       valuesBuilder.Reserve(size)
+       for i := 0; i < size; i++ {
+               valuesBuilder.Append(int32(i))
+       }
+       values := valuesBuilder.NewInt32Array()
+       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
+       x = (x >> 22) ^ x
+       return int(x%100) < selectedPercent
+}
diff --git a/arrow/compute/vector_selection_test.go 
b/arrow/compute/vector_selection_test.go
index 08d41bae..0d9d9f04 100644
--- a/arrow/compute/vector_selection_test.go
+++ b/arrow/compute/vector_selection_test.go
@@ -2032,6 +2032,53 @@ func TestFilterKernels(t *testing.T) {
        suite.Run(t, new(FilterKernelWithTable))
 }
 
+func TestFilterInt32MixedMaskOffsets(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       const length = 128
+       for _, offset := range []int64{0, 3, 8} {
+               t.Run(fmt.Sprintf("offset=%d", offset), func(t *testing.T) {
+                       valuesBuilder := array.NewInt32Builder(mem)
+                       valuesBuilder.Reserve(int(offset) + length)
+                       for i := int64(0); i < offset+length; i++ {
+                               valuesBuilder.Append(int32(i))
+                       }
+                       valuesBase := valuesBuilder.NewInt32Array()
+                       valuesBuilder.Release()
+                       values := array.NewSlice(valuesBase, offset, 
offset+length)
+                       valuesBase.Release()
+                       defer values.Release()
+
+                       filterBuilder := array.NewBooleanBuilder(mem)
+                       filterBuilder.Reserve(int(offset) + length)
+                       for i := int64(0); i < offset+length; i++ {
+                               filterBuilder.Append(i%2 == 0)
+                       }
+                       filterBase := filterBuilder.NewBooleanArray()
+                       filterBuilder.Release()
+                       filter := array.NewSlice(filterBase, offset, 
offset+length)
+                       filterBase.Release()
+                       defer filter.Release()
+
+                       expectedBuilder := array.NewInt32Builder(mem)
+                       for i := offset; i < offset+length; i++ {
+                               if i%2 == 0 {
+                                       expectedBuilder.Append(int32(i))
+                               }
+                       }
+                       expected := expectedBuilder.NewInt32Array()
+                       expectedBuilder.Release()
+                       defer expected.Release()
+
+                       actual, err := 
compute.FilterArray(context.Background(), values, filter, 
*compute.DefaultFilterOptions())
+                       require.NoError(t, err)
+                       defer actual.Release()
+                       assertArraysEqual(t, expected, actual)
+               })
+       }
+}
+
 // Benchmark tests for Take operation with variable-length data
 // These benchmarks test the performance improvements from buffer 
pre-allocation
 // in VarBinaryImpl for string/binary data reorganization (e.g., partitioning).

Reply via email to