I wrote a search function, similar to std::find
, that uses SIMD instructions. Since I am new to SIMD, I would appreciate comments on other SIMD instructions I have missed that would be useful for this use case, possible corner cases I have overlooked, and best practices in using SIMD and C++ in general.
#pragma once
#include <immintrin.h>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <memory>
#include <type_traits>
namespace simd {
using vec256i = __m256i;
using vec256f = __m256;
using vec256d = __m256d;
template <std::size_t WIDTH>
[[nodiscard]] inline __attribute__((always_inline)) vec256i compare(const vec256i& lhs, const void* const rhs) {
static_assert(WIDTH == 1 || WIDTH == 2 || WIDTH == 4 || WIDTH == 8);
if constexpr (WIDTH == 1) {
return _mm256_cmpeq_epi8(lhs, _mm256_loadu_epi8(rhs));
} else if (WIDTH == 2) {
return _mm256_cmpeq_epi16(lhs, _mm256_loadu_epi16(rhs));
} else if (WIDTH == 4) {
return _mm256_cmpeq_epi32(lhs, _mm256_load_epi32(rhs));
} else if (WIDTH == 8) {
return _mm256_cmpeq_epi64(lhs, _mm256_load_epi64(rhs));
} else {
return vec256i{0};
}
}
template <std::size_t WIDTH>
[[nodiscard]] inline __attribute__((always_inline)) std::uint32_t compress_mask(const vec256i& mask) {
static_assert(WIDTH == 1 || WIDTH == 2 || WIDTH == 4 || WIDTH == 8);
if constexpr (WIDTH == 1 || WIDTH == 2) {
return _mm256_movemask_epi8(mask);
} else if (WIDTH == 4) {
return _mm256_movemask_ps(reinterpret_cast<vec256f>(mask));
} else if (WIDTH == 8) {
return _mm256_movemask_pd(reinterpret_cast<vec256d>(mask));
} else {
return 0;
}
}
template <std::size_t WIDTH>
[[nodiscard]] inline __attribute__((always_inline)) std::uint32_t ctz_ui128(const std::uint32_t& mask4,
const std::uint32_t& mask3,
const std::uint32_t& mask2,
const std::uint32_t& mask1) {
static_assert(WIDTH == 1 || WIDTH == 2);
if (mask1 != 0) {
return __builtin_ctz(mask1) / WIDTH;
} else if (mask2 != 0) {
return (__builtin_ctz(mask2) + 32) / WIDTH;
} else if (mask3 != 0) {
return (__builtin_ctz(mask3) + 64) / WIDTH;
} else if (mask4 != 0) {
return (__builtin_ctz(mask4) + 96) / WIDTH;
} else {
return 128;
}
}
template <std::size_t WIDTH>
const void* find(const void* const current_void, const void* const value_void);
template <>
[[nodiscard]] inline __attribute__((always_inline)) const void* find<1>(const void* const current,
const void* const value) {
const auto* const current_i8{reinterpret_cast<const std::uint8_t*>(current)};
const auto* const value_i8{reinterpret_cast<const std::uint8_t*>(value)};
const auto value_vec{_mm256_set1_epi8(*value_i8)};
const auto mask1{compare<1>(value_vec, current_i8)};
const auto mask2{compare<1>(value_vec, current_i8 + 32)};
const auto mask3{compare<1>(value_vec, current_i8 + 64)};
const auto mask4{compare<1>(value_vec, current_i8 + 96)};
const auto mask12{_mm256_or_si256(mask1, mask2)};
const auto mask34{_mm256_or_si256(mask3, mask4)};
const auto mask1234{_mm256_or_si256(mask12, mask34)};
if (_mm256_testz_si256(mask1234, mask1234) == 0) {
const auto first_occurrence{ctz_ui128<1>(compress_mask<1>(mask4), compress_mask<1>(mask3), compress_mask<1>(mask2),
compress_mask<1>(mask1))};
return reinterpret_cast<const void*>(current_i8 + first_occurrence);
}
return nullptr;
}
template <>
[[nodiscard]] inline __attribute__((always_inline)) const void* find<2>(const void* const current,
const void* const value) {
const auto* const current_i16{reinterpret_cast<const std::uint16_t*>(current)};
const auto* const value_i16{reinterpret_cast<const std::uint16_t*>(value)};
const auto value_vec{_mm256_set1_epi16(*value_i16)};
const auto mask1{compare<2>(value_vec, current_i16)};
const auto mask2{compare<2>(value_vec, current_i16 + 16)};
const auto mask3{compare<2>(value_vec, current_i16 + 32)};
const auto mask4{compare<2>(value_vec, current_i16 + 48)};
const auto mask12{_mm256_or_si256(mask1, mask2)};
const auto mask34{_mm256_or_si256(mask3, mask4)};
const auto mask1234{_mm256_or_si256(mask12, mask34)};
if (_mm256_testz_si256(mask1234, mask1234) == 0) {
const auto first_occurrence{ctz_ui128<2>(compress_mask<2>(mask4), compress_mask<2>(mask3), compress_mask<2>(mask2),
compress_mask<2>(mask1))};
return reinterpret_cast<const void*>(current_i16 + first_occurrence);
}
return nullptr;
}
template <>
[[nodiscard]] inline __attribute__((always_inline)) const void* find<4>(const void* const current,
const void* const value) {
const auto* const current_i32{reinterpret_cast<const std::uint32_t*>(current)};
const auto* const value_i32{reinterpret_cast<const std::uint32_t*>(value)};
const auto value_vec{_mm256_set1_epi32(*value_i32)};
const auto mask1{compare<4>(value_vec, current_i32)};
const auto mask2{compare<4>(value_vec, current_i32 + 8)};
const auto mask3{compare<4>(value_vec, current_i32 + 16)};
const auto mask4{compare<4>(value_vec, current_i32 + 24)};
const auto mask12{_mm256_or_si256(mask1, mask2)};
const auto mask34{_mm256_or_si256(mask3, mask4)};
const auto mask1234{_mm256_or_si256(mask12, mask34)};
if (_mm256_testz_si256(mask1234, mask1234) == 0) {
const auto compressed_mask{(compress_mask<4>(mask4) << 24) + (compress_mask<4>(mask3) << 16) +
(compress_mask<4>(mask2) << 8) + compress_mask<4>(mask1)};
return reinterpret_cast<const void*>(current_i32 + __builtin_ctz(compressed_mask));
}
return nullptr;
}
template <>
[[nodiscard]] inline __attribute__((always_inline)) const void* find<8>(const void* const current,
const void* const value) {
const auto* const current_i64{reinterpret_cast<const std::uint64_t*>(current)};
const auto* const value_i64{reinterpret_cast<const std::uint64_t*>(value)};
const auto value_vec{_mm256_set1_epi64x(*value_i64)};
const auto mask1{compare<8>(value_vec, current_i64)};
const auto mask2{compare<8>(value_vec, current_i64 + 4)};
const auto mask3{compare<8>(value_vec, current_i64 + 8)};
const auto mask4{compare<8>(value_vec, current_i64 + 12)};
const auto mask12{_mm256_or_si256(mask1, mask2)};
const auto mask34{_mm256_or_si256(mask3, mask4)};
const auto mask1234{_mm256_or_si256(mask12, mask34)};
if (_mm256_testz_si256(mask1234, mask1234) == 0) {
const auto compressed_mask{(compress_mask<8>(mask4) << 12) + (compress_mask<8>(mask3) << 8) +
(compress_mask<8>(mask2) << 4) + compress_mask<8>(mask1)};
return reinterpret_cast<const void*>(current_i64 + __builtin_ctz(compressed_mask));
}
return nullptr;
}
template <std::contiguous_iterator ITERATOR_T>
[[nodiscard]] ITERATOR_T find(ITERATOR_T begin_it, ITERATOR_T end_it,
const typename std::iterator_traits<ITERATOR_T>::value_type& value) {
using value_t = typename std::iterator_traits<ITERATOR_T>::value_type;
static_assert(std::is_scalar_v<value_t>);
constexpr auto WIDTH{sizeof(value_t)};
const value_t* const begin{std::to_address(begin_it)};
const value_t* const end{std::to_address(end_it)};
const value_t* current{std::to_address(begin_it)};
// Scalar comparison until the pointer is aligned to 32 bytes.
while (current != end && reinterpret_cast<std::uintptr_t>(current) % 32 != 0) {
if (*current == value) {
return begin_it + (current - begin);
}
++current;
}
// SIMD comparison.
while (current + (128 / WIDTH) <= end) {
const auto* const current_void{reinterpret_cast<const void*>(current)};
const auto* const value_void{reinterpret_cast<const void*>(&value)};
if (const auto* const found{find<WIDTH>(current_void, value_void)}; found != nullptr) {
return begin_it + (reinterpret_cast<const value_t*>(found) - begin);
}
current += 128 / WIDTH;
}
// Scalar tail comparison.
while (current != end) {
if (*current == value) {
return begin_it + (current - begin);
}
++current;
}
return end_it;
}
} // namespace simd
Example usage:
#include <cassert>
#include <algorithm>
#include <numeric>
#include <vector>
#include "find.hpp"
int main() {
std::vector<int> elements(1'000'000, 0);
std::iota(elements.begin(), elements.end(), 0);
const auto simd_it{simd::find(elements.cbegin(), elements.cend(), 1337)};
const auto std_it{std::find(elements.cbegin(), elements.cend(), 1337)};
assert(simd_it == std_it);
}