// strspn exclusively uses 16B vectors, since: // 1. The 'large' implementation is written assuming 16B vectors, and would // need to be rewritten for larger vector types. // 2. Larger vectors for the 'small' path only really help as we traverse deep // into the haystack; it seems likely that most 'hits' will be early on in // the haystack. // // Moreover, "it's simpler this way, and probably more than sufficiently fast." using ElemTy = int8_t; using FullVec = hn::Full128<ElemTy>; using FullVecTy = hn::VFromD<FullVec>; using FullVecM = hn::MFromD<FullVec>;
constexpr static size_t kMaxSmallSize = 4;
// How many bytes to scan before with a naive n^2 algorithm before building a // bitset, assuming the keep/reject set has more than kMaxSmallSize elements. // // The main disadvantage of the bitset is that it takes quite a while to // construct; the goal is to balance the extra throughput the bet confers with // this. // // To help inform one's intuition, on arm64 (mustang), a fully n^2 // implementation (no bitset fallback) on Bionic's benchmarks is: // - Up to 3.5x slower than bitset fallback for strspn_medium (10 byte 'keep' // set, 131KB to scan). // - Up to *9x* slower for strcspn_rare (31 byte 'reject' set, 131KB to scan). // // On the other hand, this n^2 approach is >3x faster for both of the above // cases in the smallest instance (8 bytes before a match).
constexpr static size_t kMaxBytesBeforeBitset = 128;
// Runs `strspn`-like functions, where `f` provides the implementation. Assumes // `haystack` is aligned properly. // // The signature of `f` should be compatible with: // optional<size_t> f(FullVecTy v, optional<size_t> ignore_first = {}); // // where: // - `v` is a vector of chars to scan, // - `ignore_first` is the number of chars in `v` to ignore (starting at the // 0th lane) // - The return is an empty optional if the algo should keep scanning; // otherwise, it's the lane number of the char the algo should stop at in // `v` (less `ignore_first`, if provided). template <typename Fn>
PSIMD_FLATTEN size_t strspn_driver_aligned(const ElemTy* haystack, Fn f) {
constexpr FullVec d;
PSIMD_DCHECK(hn::IsAligned(d, haystack));
// Traits that allow for incrementally finding needles/non-needles for strspn // and strcspn. These build up a mask with 'true' elements meaning "this // element doesn't end the function's logic," and 'false' meaning "this does." struct SharedTraits { // Returns the index of the element that strspn/strcspn should stop at, or // empty if none was found. Ignores the first `ignore_first` chars.
PSIMD_FLATTEN static optional<size_t> find_first_false(FullVecM eq_mask,
optional<size_t> ignore_first) {
constexpr FullVec d; if (ignore_first) {
eq_mask = Or(eq_mask, FirstN(d, *ignore_first));
} if (AllTrue(d, eq_mask)) { return {};
}
size_t index = FindKnownFirstTrue(d, Not(eq_mask)); if (ignore_first) {
index -= *ignore_first;
} return optional<size_t>{index};
}
};
struct TryDispatchResult { // If this is nonempty, it's the result of the strcspn or strspn call.
optional<size_t> result;
// If `result` has a value, these have no well-defined value. Otherwise, // `haystack_scan_offset` is the offset of the `haystack` pointer to start // scanning from to discover the result. // // `haystack + haystack_scan_offset` is guaranteed to be aligned to 16B.
size_t haystack_scan_offset; // This is the `strlen` of the keep/reject set.
size_t set_len;
// Returns the offset from `haystack` to use if scanning `haystack` further // is necessary.
PSIMD_FLATTEN size_t haystack_offset() const {
constexpr FullVec d; return d.MaxBytes() - haystack_ignore_.unwrap_or(0);
}
// Returns a `TryDispatchResult::of_result` if any matches were found // incrementally. Otherwise, returns an empty optional.
PSIMD_FLATTEN optional<TryDispatchResult> try_to_result() const { if (const optional<size_t> result = Traits::find_first_false(mask_, haystack_ignore_)) { return optional{TryDispatchResult::of_result(*result)};
} return {};
}
private:
FullVecTy loaded_haystack_; // Subtle: This is an optional since there's a decent amount of // special-casing and optimization that can be done if this is statically // guaranteed to be 0. There's an entire class of usages where that's the // case, so represent it as empty for ease of LLVM analysis.
optional<size_t> haystack_ignore_;
FullVecM mask_;
// Tries to determine the result of `strspn` or `strcspn` if `str` is small. template <typename Traits>
PSIMD_FLATTEN TryDispatchResult try_dispatch_to_small_strspnish(const ElemTy* haystack, const ElemTy* str) {
constexpr FullVec d;
if (!str[0]) [[unlikely]] { if (Traits::kIsRejectSet) { return TryDispatchResult::of_result(
__builtin_strlen(reinterpret_cast<constchar*>(haystack)));
} return TryDispatchResult::of_result(0);
}
FullVecTy checks[kMaxSmallSize];
checks[0] = Set(d, str[0]); #pragma unroll for (size_t i = 1; i < kMaxSmallSize; ++i) {
checks[i] = checks[0];
}
bool is_small = false; #pragma unroll for (size_t i = 0; i < kMaxSmallSize; ++i) { if (str[i] == 0) { // If this is strcspn with a single elem in `str`, we can trivially defer to // `strchrnul`, which we have optimized implementations of for the 64-bit // architectures. // // (Note that this branch is expected to fold away after unrolling.) if (i == 1 && Traits::kIsRejectSet) { constauto* res_ptr = reinterpret_cast<const ElemTy*>(
strchrnul(reinterpret_cast<constchar*>(haystack), str[0])); return TryDispatchResult::of_result(res_ptr - haystack);
}
// NOTE: This is not written as `return strspn_small_fn(haystack, // checks)` because LLVM's optimizations are more likely to turn that // into N inlined function calls to `strspn_small_fn`. // // Instead, have precisely one unrolled callsite, and N jumps to that, // which should be much more compact and closer to what we 'actually' // want here.
is_small = true; break;
}
checks[i] = Set(d, str[i]);
}
if (is_small) [[likely]] { return TryDispatchResult::of_result(strspnish_small<Traits, kMaxSmallSize>(haystack, checks));
}
// It's possible that, even though this set of needles is large, the amount // we'll need to scan into `haystack` is small. // // Do an n^2 loop for the first handful of vectors into `haystack`, since // falling back to bitset building, while algorithmically optimal (& optimal // in practice for very large inputs), has a high fixed cost. auto incremental_result =
IncrementalStrspnishResult<Traits>::maybe_unaligned(haystack, checks[0]); #pragma unroll for (size_t i = 1; i < kMaxSmallSize; ++i) {
incremental_result.push_set_elem(checks[i]);
}
struct VecBitSet {
PSIMD_FLATTEN static VecBitSet from_large_string(const ElemTy* keep, size_t keep_len, bool include_nul) {
constexpr FullVec d; // On Brya (mobile Intel from ~2022), four approaches were tested. Listed // in order of observed performance on Bionic's benchmarks (earlier is // better performance): // // 1. this one // 2. a direct bitset stored in `uint8_t[256 / 8]` // 3. maintaining a bitset directly in 128B vector registers // 4. a bitset from `uint8_t[256]` (which matches `strspn/strcspn`'s // current implementation), which was later 'compressed' into two // vectors.
uint64_t lo_lo = 0;
uint64_t lo_hi = 0;
uint64_t hi_lo = 0;
uint64_t hi_hi = 0;
auto push_bits_into = [](uint8_t c, uint64_t& hi, uint64_t& lo) PSIMD_FLATTEN {
PSIMD_DCHECK(c <= 0x7F); // It's expected that storing to `hi` or `lo` will be unpredictable, so // store to both. // // `bit` represents the bit to set, `is_hi` is either all 1s or 0s, // depending on the value of `c & 0x40`. const uint64_t bit = 1ULL << (c & 0x3F); const uint64_t is_hi = -static_cast<uint64_t>(c >> 6);
hi |= bit & is_hi;
lo |= bit & ~is_hi;
};
auto push_char = [&](uint8_t c) PSIMD_FLATTEN { if (c >= 0x80) [[unlikely]] {
push_bits_into(c ^ 0x80, hi_hi, hi_lo);
} else {
push_bits_into(c, lo_hi, lo_lo);
}
};
size_t i = 0; #pragma unroll for (; i < kMaxSmallSize; ++i) {
PSIMD_DCHECK(keep[i]);
push_char(static_cast<uint8_t>(keep[i]));
}
for (; i < keep_len; ++i) {
PSIMD_DCHECK(keep[i]);
push_char(static_cast<uint8_t>(keep[i]));
}
PSIMD_DCHECK(!keep[i]); if (include_nul) {
push_char(0);
}
constexpr hn::Full128<uint64_t> d64;
constauto lo = Dup128VecFromValues(d64, lo_lo, lo_hi); constauto hi = Dup128VecFromValues(d64, hi_lo, hi_hi); return VecBitSet{BitCast(d, hi), BitCast(d, lo)};
}
PSIMD_FLATTEN void flip_all_bits() { // Technically this is pointless if `!hi_relevant_`, but the extra check is // likely more expensive than just flipping the bits unconditionally.
hi_ = Not(hi_);
lo_ = Not(lo_);
hi_flipped_ = !hi_flipped_;
}
FullVecM eq_mask = test_membership(v, lo_); // If `hi_relevant_` is set, then we have chars outside of the standard // ASCII range. Technically permitted, but should be incredibly rare. if (hi_relevant_) [[unlikely]] { const FullVecTy hi_bits = Set(d, static_cast<ElemTy>(0x80));
eq_mask = Or(eq_mask, test_membership(v ^ hi_bits, hi_));
} elseif (hi_flipped_) {
eq_mask = Or(eq_mask, v < Zero(d));
} if (ignore_first) {
eq_mask = Or(eq_mask, FirstN(d, *ignore_first));
}
if (AllTrue(d, eq_mask)) { return {};
}
size_t index = FindKnownFirstTrue(d, Not(eq_mask)); if (ignore_first) {
index -= *ignore_first;
} return optional<size_t>{index};
}
// Does membership tests for each lane in v, returning a mask where lane i is // set if v[i] is in the bitset represented by lookups. // // All lanes of v must be in [0, 127]. Anything outside of that is zeroed in // the mask.
PSIMD_FLATTEN static FullVecM test_membership(FullVecTy v, FullVecTy lookups) {
constexpr FullVec d;
// These are 128-bit vectors. Bit [i] indicates whether [i] is set.
static_assert(sizeof(FullVecTy) == 16, "Expected 128-bit vectors");
// This is a standard bit-set. Bit `i` will be in lane (i / 8) of the // lookup vector, at bit (i % 8).
// ...To that end, move the bitset's lane `(i / 8)` into the lanes // corresponding to `v`. constauto index_parts = ShiftRight<3>(v); constauto lookup_masks = TableLookupBytesOr0(lookups, index_parts);
// ...And then figure out the bit in each `v[i]` that needs to be set. constauto mask_shift_amounts = v & Set(d, 7); auto nth_bits = [&](auto v) PSIMD_FLATTEN { // The instruction sequence to emulate a bytewise `1 << v[i]` in SSE and // AVX are pretty massive; just look up from a table instead. if constexpr (kTargetIsX86OrX86_64) {
alignas(d.MaxBytes()) staticconst int8_t bits[d.MaxLanes()] = { 1u << 0, 1u << 1, 1u << 2, 1u << 3, 1u << 4, 1u << 5, 1u << 6, (int8_t)(1u << 7),
}; constauto loaded = Load(d, bits); return TableLookupBytes(loaded, v);
} else { return Set(d, 1) << v;
}
}; constauto shifted_masks = nth_bits(mask_shift_amounts);
// ...Finally, it's as simple as `&`. return (lookup_masks & shifted_masks) != Zero(d);
}
FullVecTy hi_;
FullVecTy lo_; // `hi_relevant_` tracks whether `hi_` is just all unset (or all set, in the // case of `hi_flipped_`). // // Testing this is trivial, should be consistently `false`, and lets us skip // a decent number of instructions if it is `false`. constbool hi_relevant_; // If `!hi_relevant_`, this indicates whether `hi` should be treated as all // `true` values. bool hi_flipped_;
};
// Mark this `noinline` because it's the uncommon case, and it adds a lot of // register pressure to its caller on x86_64. // // Visually inspecting `objdump`, `noinline` allows `strspn` to operate without // touching the stack unless it _has_ to call this function. With // `PSIMD_FLATTEN`, we end up unconditionally saving/restoring 3 registers for // no reason.
__attribute__((noinline)) size_t strspn_large(const ElemTy* haystack, const ElemTy* keep,
size_t keep_len, size_t start_offset) { constauto bitset = VecBitSet::from_large_string(keep, keep_len, /*include_nul=*/false); return start_offset +
strspn_driver_aligned(haystack + start_offset,
[&](FullVecTy v, optional<size_t> ignore_first = {}) PSIMD_FLATTEN { return bitset.index_of_first_unset(v, ignore_first);
});
}
// Mark this `noinline` because it's the uncommon case, and it adds a lot of // register pressure to its caller on x86_64. // // Visually inspecting `objdump`, `noinline` allows `strcspn` to operate // without touching the stack unless it _has_ to call this function. With // `PSIMD_FLATTEN`, we end up unconditionally saving/restoring 4 registers for // no reason.
__attribute__((noinline)) size_t strcspn_large(const ElemTy* haystack, const ElemTy* reject,
size_t reject_len, size_t start_offset) { // `include_nul` since we're going to run `strspn`, but with membership flipped. auto bitset = VecBitSet::from_large_string(reject, reject_len, /*include_nul=*/true);
bitset.flip_all_bits(); return start_offset +
strspn_driver_aligned(haystack + start_offset,
[&](FullVecTy v, optional<size_t> ignore_first = {}) PSIMD_FLATTEN { return bitset.index_of_first_unset(v, ignore_first);
});
}
} // namespace
} // namespace portable_simd
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.