Skip to content

Commit 21fa748

Browse files
committed
perf: vectorize small u8 table take with NEON
Use NEON TBL for u8-coded tables with at most 16 one-byte values. Signed-off-by: Joseph Isaacs <joseph-isaacs@users.noreply.github.com>
1 parent d49393c commit 21fa748

3 files changed

Lines changed: 110 additions & 0 deletions

File tree

‎vortex-array/src/arrays/fixed_width/take/mod.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ mod avx2;
66
mod records;
77
mod scalar;
88
mod slices;
9+
#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
10+
mod small_table;
911
#[cfg(test)]
1012
mod tests;
1113

@@ -36,6 +38,8 @@ use crate::arrays::piecewise_sequence::constant_unsigned_usize;
3638
use crate::arrays::piecewise_sequence::maybe_contiguous_slices;
3739
use crate::builtins::ArrayBuiltins;
3840
use crate::dtype::DType;
41+
#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
42+
use crate::dtype::PType;
3943
use crate::dtype::UnsignedPType;
4044
use crate::dtype::half::f16;
4145
use crate::match_each_unsigned_integer_ptype;
@@ -80,6 +84,16 @@ pub(crate) fn take_values<T: FixedWidthTakeValue, I: UnsignedPType>(
8084
values: &[T],
8185
indices: &[I],
8286
) -> Buffer<T> {
87+
#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
88+
if I::PTYPE == PType::U8 {
89+
// SAFETY: the ptype dispatcher guarantees that `I::PTYPE == U8` is the concrete `u8`
90+
// implementation, so these slices have identical layouts.
91+
let indices = unsafe { std::slice::from_raw_parts(indices.as_ptr().cast(), indices.len()) };
92+
if let Some(taken) = small_table::take(values, indices) {
93+
return taken;
94+
}
95+
}
96+
8397
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
8498
if *HAS_AVX2 {
8599
// SAFETY: AVX2 was detected above and `FixedWidthTakeValue` guarantees an initialized byte
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! NEON byte-table take for `u8` codes and at most 16 one-byte values.
5+
6+
use std::arch::aarch64::uint8x16_t;
7+
use std::arch::aarch64::vdupq_n_u8;
8+
use std::arch::aarch64::vld1q_u8;
9+
use std::arch::aarch64::vmaxq_u8;
10+
use std::arch::aarch64::vmaxvq_u8;
11+
use std::arch::aarch64::vqtbl1q_u8;
12+
use std::arch::aarch64::vst1q_u8;
13+
14+
use vortex_buffer::Buffer;
15+
use vortex_buffer::BufferMut;
16+
17+
use super::FixedWidthTakeValue;
18+
19+
pub(crate) fn take<T: FixedWidthTakeValue>(values: &[T], indices: &[u8]) -> Option<Buffer<T>> {
20+
if values.is_empty() || values.len() > 16 || size_of::<T>() != 1 || indices.len() < 64 {
21+
return None;
22+
}
23+
24+
let mut table = [0u8; 16];
25+
// SAFETY: one-byte values have the same representation as bytes, and the length is <= 16.
26+
unsafe {
27+
std::ptr::copy_nonoverlapping(
28+
values.as_ptr().cast::<u8>(),
29+
table.as_mut_ptr(),
30+
values.len(),
31+
);
32+
}
33+
34+
let mut output = BufferMut::<T>::with_capacity(indices.len());
35+
let output_ptr = output.spare_capacity_mut().as_mut_ptr().cast::<u8>();
36+
// SAFETY: AArch64 always provides NEON. Both pointers advance only by complete vectors.
37+
let (offset, max_code) = unsafe { take_vectors(&table, indices, output_ptr) };
38+
39+
for offset in offset..indices.len() {
40+
let code = usize::from(indices[offset]);
41+
assert!(
42+
code < values.len(),
43+
"take index {code} out of bounds for length {}",
44+
values.len()
45+
);
46+
// SAFETY: the code was checked and this reserved output position is uninitialized.
47+
unsafe {
48+
output_ptr
49+
.add(offset)
50+
.write(*values.as_ptr().add(code).cast::<u8>())
51+
};
52+
}
53+
assert!(
54+
usize::from(max_code) < values.len(),
55+
"take index {max_code} out of bounds for length {}",
56+
values.len()
57+
);
58+
// SAFETY: the vector loop and scalar remainder initialized every output value.
59+
unsafe { output.set_len(indices.len()) };
60+
Some(output.freeze())
61+
}
62+
63+
unsafe fn take_vectors(table: &[u8; 16], indices: &[u8], output: *mut u8) -> (usize, u8) {
64+
let table = unsafe { vld1q_u8(table.as_ptr()) };
65+
let mut max_codes: uint8x16_t = unsafe { vdupq_n_u8(0) };
66+
let mut offset = 0;
67+
while offset + 16 <= indices.len() {
68+
let codes = unsafe { vld1q_u8(indices.as_ptr().add(offset)) };
69+
max_codes = unsafe { vmaxq_u8(max_codes, codes) };
70+
unsafe { vst1q_u8(output.add(offset), vqtbl1q_u8(table, codes)) };
71+
offset += 16;
72+
}
73+
(offset, unsafe { vmaxvq_u8(max_codes) })
74+
}

‎vortex-array/src/arrays/fixed_width/take/tests.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,28 @@ fn take_eight_byte_values() {
3838
assert_eq!(taken.as_slice(), &[20, 30, 10]);
3939
}
4040

41+
#[test]
42+
fn take_small_byte_table() {
43+
let values = [10u8, 20, 30, 40];
44+
let indices = (0..128).map(|index| index % 4).collect::<Vec<u8>>();
45+
let expected = indices
46+
.iter()
47+
.map(|index| values[usize::from(*index)])
48+
.collect::<Vec<_>>();
49+
assert_eq!(
50+
take_values(&values, &indices).as_slice(),
51+
expected.as_slice()
52+
);
53+
}
54+
55+
#[test]
56+
#[should_panic(expected = "take index")]
57+
fn take_small_byte_table_rejects_out_of_bounds_index() {
58+
let mut indices = vec![0u8; 128];
59+
indices[64] = 4;
60+
drop(take_values(&[10u8, 20, 30, 40], &indices));
61+
}
62+
4163
#[rstest]
4264
#[case(1)]
4365
#[case(2)]

0 commit comments

Comments
 (0)