Skip to content

Fix defects in __simd_find_first_of() - #2834

Open
SergeyKopienko wants to merge 30 commits into
mainfrom
dev/skopienko/fix_simd_find_first_of
Open

SergeyKopienko wants to merge 30 commits into
mainfrom
dev/skopienko/fix_simd_find_first_of

Conversation

@SergeyKopienko

@SergeyKopienko SergeyKopienko commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Found in #2832; the tests in main did not catch it.

Fixed defects in __simd_find_first_of()

  1. Only the first element of the first sequence was compared (__n1 < __n2): the __simd_or() functor captured __first once, before the loop. find_first_of([1, 2], [9, 8, 7, 2]) returned last.
  2. Swapped predicate arguments (__n1 < __n2): [alg.find.first.of] requires pred(*i, *j). With std::less, find_first_of([3, 9], [4, 4, 4, 4]) returned last.
  3. Not the leftmost match (__n1 >= __n2): find_first_of([5, 3, 7, 9], [3, 5]) returned index 1 instead of 0.

Design (review follow-up)

The first sequence is scanned in blocks of 256 bytes doubling up to 16 KB, and the scan stops at the first block with a match. Each block is searched with the two original strategies, choosing by whether the block or the second sequence is longer. As a result:

  • an early match costs O(block * n2) instead of O(n1 * n2);
  • a late match or no match keeps full SIMD throughput;
  • the block stays in L1 and does not depend on n2;
  • narrow difference types, e.g. counting_iterator<std::int8_t>, do not overflow.

Tests

  • find_first_of.pass.cpp covers each defect and now fails on main:
    • distinct values in the first sequence;
    • rotating match positions (the first and the last block);
    • a leftmost match that is later in the second sequence;
    • an asymmetric predicate;
    • a CallId parameter that keeps the SYCL kernel names unique.
  • New implementation_details/simd_find_first_of.pass.cpp checks narrow difference types on the longest representable first sequence.

… in __simd_find_first_of()

The __n1 < __n2 branch created the functor passed to __simd_or() once, before the loop over the
first sequence, capturing __first by value. The capture kept pointing at the initial position for
the whole loop, so every iteration compared the elements of the second sequence against the *first*
element of the first sequence again. The brick could therefore only return two values: __first, if
any element of the second sequence matches *__first, and __last otherwise - a match at any other
position was never reported.

Example (unseq, equality): find_first_of([1, 2], [9, 8, 7, 2]) returned index 2 (== __last)
instead of index 1.

The functor is now created inside the loop, directly at the __simd_or() call, so it holds the
element of the current iteration; being created per iteration it also no longer needs to be
mutable. The defect appeared together with the lambda in 954ce1a - before that commit the
functor (__internal::__equal_value_by_pred) was constructed inside the loop.
…simd_find_first_of()

[alg.find.first.of] specifies the result as the first iterator i of the first sequence for which
pred(*i, *j) is true for some iterator j of the second sequence, so the element of the first
sequence has to be passed as the first argument. The __n1 < __n2 branch called the predicate the
other way round, pred(*j, *i).

With a symmetric predicate - the common case, e.g. equality - this is invisible, which is why it
went unnoticed. Any asymmetric predicate gives a wrong result, and the result also depends on the
sizes of the sequences, because the __n1 >= __n2 branch of the same brick passes the arguments in
the correct order.

Example (unseq, pred = std::less): find_first_of([3, 9], [4, 4, 4, 4]) returned index 2
(== __last) instead of index 0, because 4 < 3 is false while 3 < 4 is true.

The swapped order comes from the original PSTL code (821780f): that branch used
__internal::__equal_value_by_pred, whose operator() already invoked _M_pred(__arg, _M_value), and
the lambda introduced in 954ce1a kept the order.
…_of()

The __n1 >= __n2 branch scans the whole first sequence once per element of the second sequence and
returned the first match it found. That is the leftmost match of *some* element of the second
sequence - in fact of the earliest one which matches at all - while the standard asks for the
earliest position in the *first* sequence over all the elements of the second one.

Example (unseq, equality): find_first_of([5, 3, 7, 9], [3, 5]) returned index 1, the match of
s[0] == 3, instead of index 0, where a[0] == 5 matches s[1].

The loop now keeps the best position found so far and passes it to __simd_first() as the upper
bound of the next search, so a later scan can only improve the result. Bounding the search this way
is also cheaper than the original code even in the worst case: the scanned prefix shrinks
monotonically instead of covering the whole first sequence __n2 times, and the loop stops as soon
as position 0 is reached. When nothing is found the bound stays at __n1 and __last is returned.

The defect is as old as the initial PSTL snapshot 821780f.

This comment was marked as outdated.

@SergeyKopienko SergeyKopienko changed the title Fix errors in __simd_find_first_of() Fix deffects in __simd_find_first_of() Sep 22, 2026
Comment thread test/parallel_api/algorithm/alg.nonmodifying/find_first_of.pass.cpp Outdated
Comment thread include/oneapi/dpl/pstl/unseq_backend_simd.h Outdated
Comment thread include/oneapi/dpl/pstl/unseq_backend_simd.h Outdated
Comment thread include/oneapi/dpl/pstl/unseq_backend_simd.h Outdated
Comment thread include/oneapi/dpl/pstl/unseq_backend_simd.h Outdated
SergeyKopienko and others added 4 commits September 22, 2026 13:02
Co-authored-by: Dmitriy Sobolev <Dmitriy.Sobolev@intel.com>
…nd_first_of() & introduce local variables for internal lambdas
…ew cases, consider updating the existing ones. Having a match not in the beginning is a basic scenario - it should be covered in the core tests - remove extra comments
Comment thread include/oneapi/dpl/pstl/unseq_backend_simd.h Outdated
Co-authored-by: Dmitriy Sobolev <Dmitriy.Sobolev@intel.com>

This comment was marked as outdated.

This comment was marked as outdated.

@SergeyKopienko

Copy link
Copy Markdown
Contributor Author

Just for note - minimal fix required the state from commit f370b122

This comment was marked as outdated.

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The implementation addresses all described defects with focused regression and boundary coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@danhoeflinger danhoeflinger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants