Skip to content

Commit 25dce5c

Browse files
committed
perf: vectorize small u8 table take with AVX2
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
1 parent e8e48c8 commit 25dce5c

3 files changed

Lines changed: 146 additions & 4 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
cfg_if::cfg_if! {
77
if #[cfg(all(target_arch = "aarch64", target_endian = "little"))] {
88
mod neon;
9+
} else if #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] {
10+
mod avx2;
911
} else {
1012
mod fallback {
1113
use super::super::FixedWidthTakeValue;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! AVX2 byte-table take for `u8` codes and at most 16 one-byte values.
5+
6+
cfg_if::cfg_if! {
7+
if #[cfg(target_arch = "x86")] {
8+
use std::arch::x86 as arch;
9+
} else {
10+
use std::arch::x86_64 as arch;
11+
}
12+
}
13+
14+
use arch::__m256i;
15+
use arch::_mm_loadu_si128;
16+
use arch::_mm256_broadcastsi128_si256;
17+
use arch::_mm256_loadu_si256;
18+
use arch::_mm256_or_si256;
19+
use arch::_mm256_set1_epi8;
20+
use arch::_mm256_setzero_si256;
21+
use arch::_mm256_shuffle_epi8;
22+
use arch::_mm256_storeu_si256;
23+
use arch::_mm256_subs_epu8;
24+
use arch::_mm256_testz_si256;
25+
use vortex_buffer::Buffer;
26+
use vortex_buffer::BufferAllocatorRef;
27+
use vortex_buffer::BufferMut;
28+
use vortex_error::VortexExpect;
29+
30+
use super::super::FixedWidthTakeValue;
31+
use super::super::HAS_AVX2;
32+
use super::super::take_values_fallback;
33+
use crate::dtype::PType;
34+
use crate::dtype::UnsignedPType;
35+
36+
// SAFETY: u8 has no padding or uninitialized bytes.
37+
unsafe impl FixedWidthTakeValue for u8 {
38+
fn take<I: UnsignedPType>(
39+
values: &[Self],
40+
indices: &[I],
41+
allocator: &BufferAllocatorRef,
42+
) -> Buffer<Self> {
43+
take(values, indices, allocator)
44+
}
45+
}
46+
47+
// SAFETY: i8 has no padding or uninitialized bytes.
48+
unsafe impl FixedWidthTakeValue for i8 {
49+
fn take<I: UnsignedPType>(
50+
values: &[Self],
51+
indices: &[I],
52+
allocator: &BufferAllocatorRef,
53+
) -> Buffer<Self> {
54+
take(values, indices, allocator)
55+
}
56+
}
57+
58+
// SAFETY: Byte arrays have no padding and every byte is initialized.
59+
unsafe impl<const N: usize> FixedWidthTakeValue for [u8; N] {
60+
fn take<I: UnsignedPType>(
61+
values: &[Self],
62+
indices: &[I],
63+
allocator: &BufferAllocatorRef,
64+
) -> Buffer<Self> {
65+
take(values, indices, allocator)
66+
}
67+
}
68+
69+
fn take<T: FixedWidthTakeValue, I: UnsignedPType>(
70+
values: &[T],
71+
indices: &[I],
72+
allocator: &BufferAllocatorRef,
73+
) -> Buffer<T> {
74+
if I::PTYPE != PType::U8
75+
|| values.is_empty()
76+
|| values.len() > 16
77+
|| size_of::<T>() != 1
78+
|| indices.len() < 64
79+
|| !*HAS_AVX2
80+
{
81+
return take_values_fallback(values, indices, allocator);
82+
}
83+
84+
// SAFETY: the sealed index type is u8, as checked above.
85+
let indices: &[u8] =
86+
unsafe { std::slice::from_raw_parts(indices.as_ptr().cast(), indices.len()) };
87+
// SAFETY: AVX2 was detected above. Values are one byte with no uninitialized bytes,
88+
// and the table contains between 1 and 16 values.
89+
unsafe { take_avx2(values, indices, allocator) }
90+
}
91+
92+
#[target_feature(enable = "avx2")]
93+
unsafe fn take_avx2<T: FixedWidthTakeValue>(
94+
values: &[T],
95+
indices: &[u8],
96+
allocator: &BufferAllocatorRef,
97+
) -> Buffer<T> {
98+
let mut table = [values[0]; 16];
99+
table[..values.len()].copy_from_slice(values);
100+
// SAFETY: the table contains 16 initialized one-byte values.
101+
let table = unsafe { _mm_loadu_si128(table.as_ptr().cast()) };
102+
let table = _mm256_broadcastsi128_si256(table);
103+
let limit = _mm256_set1_epi8(
104+
i8::try_from(values.len() - 1).vortex_expect("table contains at most 16 values"),
105+
);
106+
let mut invalid = _mm256_setzero_si256();
107+
let mut output = BufferMut::<T>::with_capacity_in(indices.len(), allocator.clone());
108+
let spare = output.spare_capacity_mut();
109+
let output_ptr = spare.as_mut_ptr().cast::<u8>();
110+
111+
let mut offset = 0;
112+
while offset + 32 <= indices.len() {
113+
// SAFETY: the loop condition guarantees a complete 32-byte vector is in bounds.
114+
let codes = unsafe { _mm256_loadu_si256(indices.as_ptr().add(offset).cast()) };
115+
invalid = _mm256_or_si256(invalid, _mm256_subs_epu8(codes, limit));
116+
// SAFETY: the output has capacity for every index and T is one byte wide.
117+
unsafe {
118+
_mm256_storeu_si256(
119+
output_ptr.add(offset).cast::<__m256i>(),
120+
_mm256_shuffle_epi8(table, codes),
121+
)
122+
};
123+
offset += 32;
124+
}
125+
assert_eq!(
126+
_mm256_testz_si256(invalid, invalid),
127+
1,
128+
"take index out of bounds"
129+
);
130+
131+
for offset in offset..indices.len() {
132+
let code = usize::from(indices[offset]);
133+
assert!(
134+
code < values.len(),
135+
"take index {code} out of bounds for length {}",
136+
values.len()
137+
);
138+
spare[offset].write(values[code]);
139+
}
140+
// SAFETY: the vector loop and scalar remainder initialized every output value.
141+
unsafe { output.set_len(indices.len()) };
142+
output.freeze()
143+
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,10 @@ fn take_small_byte_table(#[case] len: usize) {
6161
);
6262
}
6363

64-
// The bounds-check message is specific to the NEON table path; other targets reach a different
65-
// fallback with its own message.
66-
#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
6764
#[rstest]
6865
#[case::vector(64)]
6966
#[case::tail(128)]
70-
#[should_panic(expected = "take index")]
67+
#[should_panic]
7168
fn take_small_byte_table_rejects_out_of_bounds_index(#[case] offset: usize) {
7269
let mut indices = vec![0u8; 129];
7370
indices[offset] = 4;

0 commit comments

Comments
 (0)