struct CharTraits { // Highway doesn't support direct `char` usage in vector types, presumably // because it can vary in signed-ness. `memchr` does not care at all; choose // signed because hwy docs say it's slightly more efficient on older x86_64 // CPUs. If there's any better argument for unsigned, that should probably be // preferred. using CharType = int8_t; using VectorTag = portable_simd::FullVector<CharType>;
};
struct WcharTraits { using CharType = int32_t;
static_assert(sizeof(CharType) == sizeof(wchar_t)); using VectorTag = portable_simd::FullVector<CharType>;
};
// All functions here share a 'driver' implementation, with a `Traits` object // to encapsulate their differences. template <typename Traits> struct CharAgnosticMemchrTraits { using CharType = Traits::CharType; using VectorTag = Traits::VectorTag;
// As of C11, calling `memchr` with a length greater than the size of the // buffer pointed to is well-defined _if_ the needle can be found in that // buffer. `wmemchr` provides no such guarantee, but treat it similarly // anyway for simplicity.
constexpr staticbool kIsMaxLengthGuaranteed = false;
// Advance `ptr` in the direction of this memchr.
PSIMD_FLATTEN staticconst CharType* advance_ptr(const CharType* p, size_t n = 1) {
constexpr VectorTag d; return p + n * d.MaxLanes();
}
// Performs adjustment on `p` before starting the algorithm. Memchr starts at `p`.
PSIMD_FLATTEN staticconst CharType* initial_adjust_ptr(const CharType* p, size_t) { return p; }
// Given a scalar mask indicating which vector element(s) matched our target // character, return the offset from the vector pointer of the closest // match.
PSIMD_FLATTEN static size_t lane_offset_of_first(ScalarMaskForD<VectorTag> mask) { return stdc_trailing_zeros(mask);
}
// Returns the pointer to the closest element in `val` equal to `ch`, // provided `val` was loaded from `ptr - lanes_to_skip`. If `count` is // non-empty, this will ignore any elements in the highest `count` lanes of // `val`.
PSIMD_FLATTEN static optional<constvoid*> ptr_of_first(const CharType* ptr,
hn::VFromD<VectorTag> val, CharType ch,
optional<size_t> count = {},
optional<size_t> lanes_to_skip = {}) {
constexpr VectorTag d; constauto all_ch = Set(d, ch); const size_t raw_eq_mask = BitsFromMask(d, all_ch == val);
size_t eq_mask = raw_eq_mask >> lanes_to_skip.unwrap_or(0);
// Since the max length is never guaranteed, 'known safe' isn't possible.
__attribute__((diagnose_if(true, "This is unsafe, and should never be called", "error"))) staticauto
align_ptr_to_vec_known_safe(auto...) {
__builtin_unreachable();
}
};
// Use `struct`s instead of `using`s so error messages and stack traces are // prettier. struct MemchrTraits : CharAgnosticMemchrTraits<CharTraits> {}; struct WmemchrTraits : CharAgnosticMemchrTraits<WcharTraits> {};
struct MemrchrTraits : CharTraits { // memrchr always walks to its maximum length, so we can treat it as a // guarantee.
constexpr staticbool kIsMaxLengthGuaranteed = true;
// Advance `ptr` in the direction of this memchr.
PSIMD_FLATTEN staticconst CharType* advance_ptr(const CharType* p, size_t n = 1) {
constexpr VectorTag d; return p - n * d.MaxLanes();
}
// Performs adjustment on `p` before starting the algorithm. // Memrchr starts at the end and immediately calls alignment functions that // expect the pointer to be adjusted backwards by one vector width.
PSIMD_FLATTEN staticconst CharType* initial_adjust_ptr(const CharType* p, size_t count) {
constexpr VectorTag d; return p + count - d.MaxLanes();
}
// Given a scalar mask indicating which vector element(s) matched our target // character, return the offset from the vector pointer of the farthest // match.
PSIMD_FLATTEN static size_t lane_offset_of_first(ScalarMaskForD<VectorTag> mask) {
constexpr VectorTag d; const size_t leading_zeros = stdc_leading_zeros(mask); const size_t distance_of_one = leading_zeros + 1; const size_t offset_from_start = d.MaxLanes() - distance_of_one; return offset_from_start;
}
// Returns the pointer to the farthest element in `val` equal to `ch`, // provided `val` was loaded from `ptr - lanes_to_skip`. If `count` is // non-empty, this will ignore any elements in the lowest `count` lanes of `val`.
PSIMD_FLATTEN static optional<constvoid*> ptr_of_first(const CharType* ptr,
hn::VFromD<VectorTag> val, CharType ch,
optional<size_t> count = {},
optional<size_t> lanes_to_skip = {}) {
constexpr VectorTag d; constauto all_ch = Set(d, ch);
// NOTE: The size of this type is important; this code ends up simpler if we // can rely on this mask only ever containing bits that correspond with // vector lanes.
ScalarMaskForD<VectorTag> eq_mask = BitsFromMask(d, all_ch == val);
// TODO(gbiv): This was written expecting that `count` and `lanes_to_skip` // would both trivially `has_value` or not, so there'll no be actual // branches here. It could be nice to either: // - have an assertion of that here (though attempts like // `if (!__builtin_constant_p(has_value())) warning_func();` have failed, // despite no branches existing in the LLVM IR related to the optionals), // or // - template this on some kind of `const_optional` type so we can `if // constexpr` these branches if (lanes_to_skip) {
eq_mask <<= *lanes_to_skip;
}
constauto result_from_final_vec = [ch](const CharType* ptr, constauto vec_val, size_t count,
optional<size_t> lanes_to_skip = {}) -> constvoid* { // If `count == 0`, we loaded `vec_val` when we shouldn't have. This is a // correctness issue, since `ptr` might've been at the start of a new page.
PSIMD_DCHECK(count != 0); return Traits::ptr_of_first(ptr, vec_val, ch, optional<size_t>{count}, lanes_to_skip)
.unwrap_or(nullptr);
};
if (count <= d.MaxLanes()) { // Unlikely because it seems rare that people would depend on 0-sized // memrchrs being a very fast case. if (count == 0) [[unlikely]] { return nullptr;
}
// We know for certain that we need 2 or fewer loads to service this request. constauto [ptr, maybe_result] = Traits::align_ptr_to_vec(
s,
[&](constauto val, optional<size_t> bytes_to_skip, optional<size_t>)
PSIMD_FLATTEN -> optional<constvoid*> { // If we loaded `ptr` directly, one vector op is all this will // take. if (!bytes_to_skip.has_value()) { return optional{result_from_final_vec(s, val, count)};
}
// If pointers are properly aligned, we should never load a partial // CharType.
PSIMD_DCHECK(*bytes_to_skip % sizeof(CharType) == 0);
const size_t lanes_to_skip = *bytes_to_skip / sizeof(CharType); // Reiterating from `align_forward_to_vec`, this is expected to // be inlined such that `lanes_to_skip.has_value()` always // trivially folds to a constant. if (const optional<constvoid*> x = Traits::ptr_of_first(
s, val, ch, optional<size_t>{count}, optional<size_t>{lanes_to_skip})) { return x;
}
auto align_func = [&](auto val, optional<size_t> bytes_to_skip, optional<size_t> overlap_bytes)
PSIMD_FLATTEN -> optional<constvoid*> { // If pointers are properly aligned, we should never load a partial // CharType, since vector alignments are all multiples of `char` // and `wchar_t`.
optional<size_t> lanes_to_skip;
optional<size_t> overlap_lanes;
size_t lanes_consumed = d.MaxLanes(); if (bytes_to_skip) {
PSIMD_DCHECK(*bytes_to_skip % sizeof(CharType) == 0);
lanes_to_skip = optional<size_t>{*bytes_to_skip / sizeof(CharType)};
lanes_consumed -= *lanes_to_skip;
} else {
PSIMD_DCHECK(overlap_bytes.has_value());
PSIMD_DCHECK(*overlap_bytes % sizeof(CharType) == 0);
overlap_lanes = optional<size_t>{*overlap_bytes / sizeof(CharType)};
lanes_consumed -= *overlap_lanes;
}
if (const optional<constvoid*> x =
Traits::ptr_of_first(s, val, ch, /*count=*/{}, lanes_to_skip)) { // No need to bounds-check, due to `count`'s size. return optional<constvoid*>{*x};
}
count -= lanes_consumed; return {};
};
GenericAlignResult<VectorTag, optional<constvoid*>> first_align_result; if constexpr (Traits::kIsMaxLengthGuaranteed) {
first_align_result = Traits::align_ptr_to_vec_known_safe(s, align_func);
} else {
first_align_result = Traits::align_ptr_to_vec(s, align_func);
} auto [ptr, maybe_result] = first_align_result; if (maybe_result) { return *maybe_result;
}
// Can't do fancy unrolling tricks if we can't read ahead. if constexpr (!kReadAheadToPageBoundaryIsOK) { while (count >= d.MaxLanes()) { if (const optional<constvoid*> x = check_ptr_and_advance()) { return *x;
}
count -= d.MaxLanes();
}
// The simplest implementation from here would be: // // while (true) { // // check for char, return if found // ++ptr; // } // // `perf` says that x86_64 CPUs stall on 'check for char, return if found' // branches really badly, so it's a better balance if we can work in batches. // Work in batches until heuristics say that running down an unrolled // loop is likely to be better.
size_t full_vector_loads_remaining = count / d.MaxLanes();
constexpr size_t unrolled_loop_size = 4;
if (full_vector_loads_remaining >= unrolled_loop_size) { constauto needle = Set(d, ch); // NOTE: "3 checks per loop," was chosen based on experimentation on Brya, // which ships with chips like the 2024 Intel Core 3 100U. 2 loads was as // much as 1.1x slower on very long inputs. There was no obvious // improvement in doing 4 per loop. // // That said, do 4 checks per loop if the size isn't a guaranteed // upper-bound. It needs to be _some_ power of two, since the batches below // can't safely cross a page boundary.
constexpr size_t checks_per_loop = Traits::kIsMaxLengthGuaranteed ? 3 : 4;
if constexpr (!Traits::kIsMaxLengthGuaranteed) {
constexpr size_t misaligned_mask = vector_align(d) * checks_per_loop - 1; // Bring us to a correct multiple-of-vector-size alignment... #pragma unroll for (size_t i = 0; i < checks_per_loop - 1; ++i) { bool is_misaligned = reinterpret_cast<uintptr_t>(ptr) & misaligned_mask; if (!is_misaligned) { break;
} if (const optional<constvoid*> x = check_ptr_and_advance()) { return *x;
}
full_vector_loads_remaining -= 1;
}
PSIMD_DCHECK(!(reinterpret_cast<uintptr_t>(ptr) & misaligned_mask));
} while (full_vector_loads_remaining >= unrolled_loop_size) { // So highway may represent masks as _either_: // - a vector which you can convert to a scalar through MaskFromVec(), or // - a scalar. // // It does not allow `operator|` on masks. // // This implementation was written assuming: // - they're vectors (thus converting mask -> vector is free), and // - this loop's hot path involves looping (so operations on that path // should be minimized). // // When that no longer holds, it should be trivial to refactor a bit.
static_assert(sizeof(hn::MFromD<VectorTag>) == sizeof(hn::VFromD<VectorTag>));
hn::MFromD<VectorTag> equal_results[checks_per_loop]; #pragma unroll for (size_t i = 0; i < checks_per_loop; ++i) { constauto v = Load(d, Traits::advance_ptr(ptr, i));
equal_results[i] = v == needle;
}
auto merged_eq = Or(equal_results[0], equal_results[1]); #pragma unroll for (size_t i = 2; i < checks_per_loop; ++i) {
merged_eq = Or(merged_eq, equal_results[i]);
}
#pragma unroll for (size_t i = 0; i < checks_per_loop - 1; ++i) { if (const size_t m = BitsFromMask(d, equal_results[i])) { return ptr + Traits::lane_offset_of_first(m);
}
ptr = Traits::advance_ptr(ptr, 1);
}
// If earlier masks were empty, `eq_bits` only contains bits relevant to // the last vector. return ptr + Traits::lane_offset_of_first(eq_bits);
}
}
switch (full_vector_loads_remaining) { case3: if (const optional<constvoid*> x = check_ptr_and_advance()) { return *x;
}
[[fallthrough]]; case2: if (const optional<constvoid*> x = check_ptr_and_advance()) { return *x;
}
[[fallthrough]]; case1: if (const optional<constvoid*> x = check_ptr_and_advance()) { return *x;
}
[[fallthrough]]; case0: if (const size_t residual_count = count % d.MaxLanes()) { return result_from_final_vec(ptr, Load(d, ptr), residual_count);
} return nullptr; default:
__builtin_unreachable();
}
}
} // namespace
} // namespace portable_simd
PSIMD_LIBC_FUNCTION(void*, memchr, constvoid* ptr, int ch, size_t count) { using portable_simd::MemchrTraits; returnconst_cast<void*>(portable_simd::memchr_vectorized<MemchrTraits>( reinterpret_cast<const MemchrTraits::CharType*>(ptr), ch, count));
}
PSIMD_LIBC_FUNCTION(void*, memrchr, constvoid* ptr, int ch, size_t count) { using portable_simd::MemrchrTraits; returnconst_cast<void*>(portable_simd::memchr_vectorized<MemrchrTraits>( reinterpret_cast<const MemrchrTraits::CharType*>(ptr), ch, count));
}
PSIMD_LIBC_FUNCTION(size_t, strnlen, constchar* ptr, size_t count) { // strnlen is just `memchr(ptr, '\0', count)` with an extra step. constauto* s = static_cast<constchar*>(PSIMD_LIBC_FUNCTION_NAME(memchr)(ptr, '\0', count)); if (!s) { return count;
} returnstatic_cast<size_t>(s - ptr);
}
// We might have received a misaligned pointer. Support that with a slow case // if needed. It's expected that the 99% case will be properly-aligned, so no // meaningful effort is put into making the misaligned case fast. if (reinterpret_cast<uintptr_t>(ptr) % alignof(wchar_t)) [[unlikely]] { return __wmemchr_misaligned(ptr, ch, count);
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.