diff --git a/BUILD b/BUILD index 765546b987..6ebb89f484 100644 --- a/BUILD +++ b/BUILD @@ -317,6 +317,7 @@ cc_library( "hwy/contrib/algo/copy-inl.h", "hwy/contrib/algo/count-inl.h", "hwy/contrib/algo/find-inl.h", + "hwy/contrib/algo/is_sorted-inl.h", "hwy/contrib/algo/minmax-inl.h", "hwy/contrib/algo/transform-inl.h", ], diff --git a/CMakeLists.txt b/CMakeLists.txt index 44fb974aaf..31d9834b4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,6 +247,7 @@ list(APPEND HWY_CONTRIB_SOURCES hwy/contrib/algo/copy-inl.h hwy/contrib/algo/count-inl.h hwy/contrib/algo/find-inl.h + hwy/contrib/algo/is_sorted-inl.h hwy/contrib/algo/minmax-inl.h hwy/contrib/algo/transform-inl.h hwy/contrib/unroller/unroller-inl.h @@ -950,6 +951,7 @@ list(APPEND HWY_TEST_FILES hwy/contrib/algo/copy_test.cc hwy/contrib/algo/count_value_test.cc hwy/contrib/algo/find_test.cc + hwy/contrib/algo/is_sorted_test.cc hwy/contrib/algo/minmax_value_test.cc hwy/contrib/algo/transform_test.cc hwy/contrib/bit_pack/bit_pack_test.cc diff --git a/g3doc/op_wishlist.md b/g3doc/op_wishlist.md index c14eab3a2e..a53fe73acd 100644 --- a/g3doc/op_wishlist.md +++ b/g3doc/op_wishlist.md @@ -48,7 +48,6 @@ fmod, ilogb, lgamma, logb, modf, nextafter, nexttoward, scalbn, tgamma * EqualSpan * ReverseSpan * ShuffleSpan -* IsSorted * Reduce ### Range coder @@ -105,6 +104,7 @@ For SVE (svld1sb_u32)+WASM? Compiler can probably already fuse. ## Done +* ~~IsSorted~~ (algo) * ~~Signbit~~ * ~~ConvertF64<->I32~~ (math-inl) * ~~Copysign~~ (math) diff --git a/hwy.gni b/hwy.gni index 690f12cdc3..e78b26d432 100644 --- a/hwy.gni +++ b/hwy.gni @@ -47,6 +47,7 @@ hwy_contrib_public = [ "$_hwy/contrib/algo/copy-inl.h", "$_hwy/contrib/algo/count-inl.h", "$_hwy/contrib/algo/find-inl.h", + "$_hwy/contrib/algo/is_sorted-inl.h", "$_hwy/contrib/algo/minmax-inl.h", "$_hwy/contrib/algo/transform-inl.h", "$_hwy/contrib/dot/dot-inl.h", diff --git a/hwy/contrib/algo/is_sorted-inl.h b/hwy/contrib/algo/is_sorted-inl.h new file mode 100644 index 0000000000..61c127b0ed --- /dev/null +++ b/hwy/contrib/algo/is_sorted-inl.h @@ -0,0 +1,106 @@ +// Copyright 2026 Google LLC +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Per-target include guard +#if defined(HIGHWAY_HWY_CONTRIB_ALGO_IS_SORTED_INL_H_) == \ + defined(HWY_TARGET_TOGGLE) // NOLINT +#ifdef HIGHWAY_HWY_CONTRIB_ALGO_IS_SORTED_INL_H_ +#undef HIGHWAY_HWY_CONTRIB_ALGO_IS_SORTED_INL_H_ +#else +#define HIGHWAY_HWY_CONTRIB_ALGO_IS_SORTED_INL_H_ +#endif + +#include + +#include "hwy/highway.h" + +HWY_BEFORE_NAMESPACE(); +namespace hwy { +namespace HWY_NAMESPACE { + +namespace detail { + +// Default comparator for IsSorted: strict less-than, which results in a +// check for non-decreasing order. +struct IsSortedLess { + template + Mask operator()(D /*d*/, V a, V b) const { + return Lt(a, b); + } +}; + +} // namespace detail + +// Returns true if `in[0, count)` is sorted with respect to `comp`: there is +// no i in [0, count - 1) for which `comp(d, in[i + 1], in[i])` is true. +// `comp(d, a, b)` returns a mask which is true in lanes where `a` is ordered +// strictly before `b`, matching the comparator semantics of std::is_sorted. +// For example, passing a comparator that returns `Gt(a, b)` checks for +// non-increasing (descending) order. A range of fewer than two elements is +// trivially sorted. +template > +bool IsSorted(D d, const T* HWY_RESTRICT in, size_t count, const Comp& comp) { + if (count < 2) return true; + HWY_LANES_CONSTEXPR size_t N = Lanes(d); + + // There are count - 1 adjacent pairs. Each step loads a vector and its + // successors via an overlapping load offset by one element, so pairs that + // straddle two steps are also covered. + const size_t num_pairs = count - 1; + + size_t i = 0; + if (HWY_LIKELY(num_pairs >= 2 * N)) { + for (; i <= num_pairs - 2 * N; i += 2 * N) { + const Mask bad0 = comp(d, LoadU(d, in + i + 1), LoadU(d, in + i)); + const Mask bad1 = + comp(d, LoadU(d, in + i + N + 1), LoadU(d, in + i + N)); + if (HWY_UNLIKELY(!AllFalse(d, Or(bad0, bad1)))) return false; + } + } + + size_t remaining = num_pairs - i; + if (remaining >= N) { + const Mask bad = comp(d, LoadU(d, in + i + 1), LoadU(d, in + i)); + if (!AllFalse(d, bad)) return false; + i += N; + remaining -= N; + } + + HWY_DASSERT(remaining < N); + if (remaining != 0) { + // LoadN zero-pads the upper lanes; mask them out so that the result only + // depends on the `remaining` valid pairs, whatever `comp` may be. + const Mask valid = FirstN(d, remaining); + const Mask bad = comp(d, LoadN(d, in + i + 1, remaining), + LoadN(d, in + i, remaining)); + if (!AllFalse(d, And(valid, bad))) return false; + } + + return true; +} + +// Returns true if `in[0, count)` is sorted in non-decreasing order, i.e. +// `in[i] <= in[i + 1]` for all i (equivalent to std::is_sorted). +template > +bool IsSorted(D d, const T* HWY_RESTRICT in, size_t count) { + return IsSorted(d, in, count, detail::IsSortedLess()); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace hwy +HWY_AFTER_NAMESPACE(); + +#endif // HIGHWAY_HWY_CONTRIB_ALGO_IS_SORTED_INL_H_ diff --git a/hwy/contrib/algo/is_sorted_test.cc b/hwy/contrib/algo/is_sorted_test.cc new file mode 100644 index 0000000000..5c79529ce0 --- /dev/null +++ b/hwy/contrib/algo/is_sorted_test.cc @@ -0,0 +1,234 @@ +// Copyright 2026 Google LLC +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include // std::sort +#include + +#include "hwy/aligned_allocator.h" +#include "hwy/base.h" + +// clang-format off +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "hwy/contrib/algo/is_sorted_test.cc" +#include "hwy/foreach_target.h" // IWYU pragma: keep +#include "hwy/highway.h" +#include "hwy/contrib/algo/is_sorted-inl.h" +#include "hwy/tests/test_util-inl.h" +// clang-format on + +HWY_BEFORE_NAMESPACE(); +namespace hwy { +namespace HWY_NAMESPACE { +namespace { + +template +T Random(RandomState& rng) { + const int32_t bits = static_cast(Random32(&rng)) & 1023; + double val = (bits - 512) / 64.0; + if (!hwy::IsSigned() && val < 0.0) { + val = -val; + } + return ConvertScalarTo(val); +} + +// Scalar references matching std::is_sorted with the default and `greater` +// comparators, respectively. +template +bool ScalarIsSortedAsc(const T* in, size_t count) { + for (size_t i = 1; i < count; ++i) { + if (in[i] < in[i - 1]) return false; + } + return true; +} + +template +bool ScalarIsSortedDesc(const T* in, size_t count) { + for (size_t i = 1; i < count; ++i) { + if (in[i - 1] < in[i]) return false; + } + return true; +} + +// Comparator for the IsSorted overload: checks non-increasing order. +struct GreaterComp { + template + Mask operator()(D /*d*/, V a, V b) const { + return Gt(a, b); + } +}; + +template +struct ForeachCountAndMisalign { + template + HWY_NOINLINE void operator()(T /*unused*/, D d) const { + RandomState rng; + const size_t N = Lanes(d); + const size_t misalignments[3] = {0, N / 4, 3 * N / 5}; + + // Trivial and boundary cases are always covered, plus random lengths. + std::vector counts = {0, 1, 2, N, N + 1, 2 * N, 2 * N + 1}; + const size_t num_random = AdjustedReps(512); + counts.reserve(counts.size() + num_random); + for (size_t k = 0; k < num_random; ++k) { + counts.push_back(static_cast(rng()) % (16 * N + 1)); + } + + for (size_t count : counts) { + for (size_t m : misalignments) { + Test()(d, count, m, rng); + } + } + } +}; + +template > +void Check(D d, size_t count, size_t misalign, bool expected, bool actual, + const char* label) { + if (expected != actual) { + fprintf(stderr, + "%s count %d misalign %d [%s]: IsSorted expected %d got %d\n", + hwy::TypeName(T(), Lanes(d)).c_str(), static_cast(count), + static_cast(misalign), label, static_cast(expected), + static_cast(actual)); + HWY_ASSERT(false); + } +} + +struct TestIsSorted { + template + void operator()(D d, size_t count, size_t misalign, RandomState& rng) { + using T = TFromD; + AlignedFreeUniquePtr storage = + AllocateAligned(HWY_MAX(1, misalign + count)); + HWY_ASSERT(storage); + T* in = storage.get() + misalign; + for (size_t i = 0; i < count; ++i) { + in[i] = Random(rng); + } + + // Random data: usually unsorted, occasionally sorted by chance. + Check(d, count, misalign, ScalarIsSortedAsc(in, count), + IsSorted(d, in, count), "random"); + + // Sorted ascending: must report sorted. + std::sort(in, in + count, [](const T& a, const T& b) { return a < b; }); + Check(d, count, misalign, ScalarIsSortedAsc(in, count), + IsSorted(d, in, count), "sorted"); + + // All-equal: sorted (no strict descent), exercises tie handling. + for (size_t i = 0; i < count; ++i) { + in[i] = ConvertScalarTo(1.0); + } + Check(d, count, misalign, true, IsSorted(d, in, count), "all-equal"); + + // Single descent: must report unsorted. Deterministically cover the + // block-boundary and last-pair positions, plus one random position. + if (count >= 2) { + const size_t N = Lanes(d); + const size_t positions[8] = {0, + N - 1, + N, + 2 * N - 1, + 2 * N, + count / 2, + count - 2, + static_cast(rng()) % (count - 1)}; + for (size_t pos : positions) { + if (pos > count - 2) continue; + in[pos] = ConvertScalarTo(2.0); + Check(d, count, misalign, false, IsSorted(d, in, count), "descent"); + in[pos] = ConvertScalarTo(1.0); // restore all-equal + } + } + } +}; + +struct TestIsSortedComp { + template + void operator()(D d, size_t count, size_t misalign, RandomState& rng) { + using T = TFromD; + AlignedFreeUniquePtr storage = + AllocateAligned(HWY_MAX(1, misalign + count)); + HWY_ASSERT(storage); + T* in = storage.get() + misalign; + for (size_t i = 0; i < count; ++i) { + in[i] = Random(rng); + } + + Check(d, count, misalign, ScalarIsSortedDesc(in, count), + IsSorted(d, in, count, GreaterComp()), "random-desc"); + + // Sorted descending: must report sorted under the Gt comparator. + std::sort(in, in + count, [](const T& a, const T& b) { return b < a; }); + Check(d, count, misalign, ScalarIsSortedDesc(in, count), + IsSorted(d, in, count, GreaterComp()), "sorted-desc"); + + // All-equal: sorted under any strict comparator. + for (size_t i = 0; i < count; ++i) { + in[i] = ConvertScalarTo(1.0); + } + Check(d, count, misalign, true, IsSorted(d, in, count, GreaterComp()), + "all-equal-desc"); + + // Single ascent: must report unsorted under the Gt comparator. + // Deterministically cover block-boundary and last-pair positions. + if (count >= 2) { + const size_t N = Lanes(d); + const size_t positions[8] = {0, + N - 1, + N, + 2 * N - 1, + 2 * N, + count / 2, + count - 2, + static_cast(rng()) % (count - 1)}; + for (size_t pos : positions) { + if (pos > count - 2) continue; + in[pos] = ConvertScalarTo(0.0); + Check(d, count, misalign, false, IsSorted(d, in, count, GreaterComp()), + "ascent-desc"); + in[pos] = ConvertScalarTo(1.0); // restore all-equal + } + } + } +}; + +void TestAllIsSorted() { + ForAllTypes(ForPartialVectors>()); +} + +void TestAllIsSortedComp() { + ForAllTypes(ForPartialVectors>()); +} + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace hwy +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace hwy { +namespace { +HWY_BEFORE_TEST(IsSortedTest); +HWY_EXPORT_AND_TEST_P(IsSortedTest, TestAllIsSorted); +HWY_EXPORT_AND_TEST_P(IsSortedTest, TestAllIsSortedComp); +HWY_AFTER_TEST(); +} // namespace +} // namespace hwy +HWY_TEST_MAIN(); +#endif // HWY_ONCE diff --git a/hwy_tests.bzl b/hwy_tests.bzl index 8a3eefb48d..660e59a2dc 100644 --- a/hwy_tests.bzl +++ b/hwy_tests.bzl @@ -17,6 +17,11 @@ HWY_CONTRIB_TESTS = ( "find_test", [":algo"], ), + ( + "hwy/contrib/algo/", + "is_sorted_test", + [":algo"], + ), ( "hwy/contrib/algo/", "minmax_value_test", diff --git a/meson.build b/meson.build index 03f52aba2d..c8748bdb5f 100644 --- a/meson.build +++ b/meson.build @@ -148,6 +148,7 @@ hwy_contrib_headers = files( 'hwy/contrib/algo/copy-inl.h', 'hwy/contrib/algo/count-inl.h', 'hwy/contrib/algo/find-inl.h', + 'hwy/contrib/algo/is_sorted-inl.h', 'hwy/contrib/algo/minmax-inl.h', 'hwy/contrib/algo/transform-inl.h', 'hwy/contrib/unroller/unroller-inl.h', @@ -674,6 +675,7 @@ if tests_enabled 'hwy/contrib/algo/copy_test.cc', 'hwy/contrib/algo/count_value_test.cc', 'hwy/contrib/algo/find_test.cc', + 'hwy/contrib/algo/is_sorted_test.cc', 'hwy/contrib/algo/minmax_value_test.cc', 'hwy/contrib/algo/transform_test.cc', 'hwy/contrib/bit_pack/bit_pack_test.cc',