blob: 188e962c34151724e36912a955c74346f0afa76d [file] [log] [blame]
// Test framework for <simd> -*- C++ -*-
// Copyright The GNU Toolchain Authors.
//
// This file is part of the GNU ISO C++ Library. This library is free
// software; you can redistribute it and/or modify it under the
// terms of the GNU General Public License as published by the
// Free Software Foundation; either version 3, or (at your option)
// any later version.
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// Under Section 7 of GPL version 3, you are granted additional
// permissions described in the GCC Runtime Library Exception, version
// 3.1, as published by the Free Software Foundation.
// You should have received a copy of the GNU General Public License and
// a copy of the GCC Runtime Library Exception along with this program;
// see the files COPYING3 and COPYING.RUNTIME respectively. If not, see
// <http://www.gnu.org/licenses/>.
#ifndef SIMD_TEST_SETUP_H
#define SIMD_TEST_SETUP_H
#include <bits/simd_details.h>
#include <string_view>
namespace test
{
struct precondition_failure
{
std::string_view file;
int line;
std::string_view expr;
std::string_view msg;
};
#undef __glibcxx_simd_precondition
#define __glibcxx_simd_precondition(expr, msg, ...) \
do { \
if (__builtin_expect(!bool(expr), false)) \
throw test::precondition_failure{__FILE__, __LINE__, #expr, msg}; \
} while(false)
}
#undef _GLIBCXX_SIMD_NOEXCEPT
#define _GLIBCXX_SIMD_NOEXCEPT noexcept(false)
#include <simd>
#include <source_location>
#include <iostream>
#include <concepts>
#include <cfenv>
#include <vector>
#include <cstdint>
#include <climits>
// global objects
static std::vector<void(*)()> test_functions = {};
static std::int64_t passed_tests = 0;
static std::int64_t failed_tests = 0;
static std::string_view test_name = "unknown";
// ------------------------------------------------
namespace simd = std::simd;
template <typename T>
struct is_character_type
: std::bool_constant<false>
{};
template <typename T>
inline constexpr bool is_character_type_v = is_character_type<T>::value;
template <typename T>
struct is_character_type<const T>
: is_character_type<T>
{};
template <typename T>
struct is_character_type<T&>
: is_character_type<T>
{};
template <> struct is_character_type<char> : std::bool_constant<true> {};
template <> struct is_character_type<wchar_t> : std::bool_constant<true> {};
template <> struct is_character_type<char8_t> : std::bool_constant<true> {};
template <> struct is_character_type<char16_t> : std::bool_constant<true> {};
template <> struct is_character_type<char32_t> : std::bool_constant<true> {};
std::ostream& operator<<(std::ostream& s, std::byte b)
{ return s << std::hex << static_cast<unsigned>(b) << std::dec; }
template <typename T, typename Abi>
std::ostream& operator<<(std::ostream& s, std::simd::basic_vec<T, Abi> const& v)
{
if constexpr (std::is_arithmetic_v<T>)
{
using U = std::conditional_t<
sizeof(T) == 1, int, std::conditional_t<
is_character_type_v<T>,
std::simd::_UInt<sizeof(T)>, T>>;
s << '[' << U(v[0]);
for (int i = 1; i < v.size(); ++i)
s << ", " << U(v[i]);
}
else
{
s << '[' << v[0];
for (int i = 1; i < v.size(); ++i)
s << ", " << v[i];
}
return s << ']';
}
template <std::size_t B, typename Abi>
std::ostream& operator<<(std::ostream& s, std::simd::basic_mask<B, Abi> const& v)
{
s << '<';
for (int i = 0; i < v.size(); ++i)
s << int(v[i]);
return s << '>';
}
template <std::simd::__vec_builtin V>
std::ostream& operator<<(std::ostream& s, V v)
{ return s << std::simd::vec<std::simd::__vec_value_type<V>, std::simd::__width_of<V>>(v); }
template <typename T, typename U>
std::ostream& operator<<(std::ostream& s, const std::pair<T, U>& x)
{ return s << '{' << x.first << ", " << x.second << '}'; }
template <typename T>
concept is_string_type
= is_character_type_v<std::ranges::range_value_t<T>>
&& std::is_convertible_v<T, std::basic_string_view<std::ranges::range_value_t<T>>>;
template <std::ranges::range R>
requires (!is_string_type<R>)
std::ostream& operator<<(std::ostream& s, R&& x)
{
s << '[';
auto it = std::ranges::begin(x);
if (it != std::ranges::end(x))
{
s << *it;
while (++it != std::ranges::end(x))
s << ',' << *it;
}
return s << ']';
}
struct additional_info
{
const bool failed = false;
additional_info
operator()(auto const& value0, auto const&... more)
{
if (failed)
[&] {
std::cout << " " << value0;
((std::cout << ' ' << more), ...);
std::cout << std::endl;
}();
return *this;
}
};
struct log_novalue {};
template <typename T>
struct unwrap_value_types
{ using type = T; };
template <typename T>
requires requires { typename T::value_type; }
struct unwrap_value_types<T>
{ using type = typename unwrap_value_types<typename T::value_type>::type; };
template <typename T>
using value_type_t = typename unwrap_value_types<std::remove_cvref_t<T>>::type;
template <typename T>
struct as_unsigned;
template <typename T>
using as_unsigned_t = typename as_unsigned<T>::type;
template <typename T>
requires (sizeof(T) == sizeof(unsigned char))
struct as_unsigned<T>
{ using type = unsigned char; };
template <typename T>
requires (sizeof(T) == sizeof(unsigned short))
struct as_unsigned<T>
{ using type = unsigned short; };
template <typename T>
requires (sizeof(T) == sizeof(unsigned int))
struct as_unsigned<T>
{ using type = unsigned int; };
template <typename T>
requires (sizeof(T) == sizeof(unsigned long long))
struct as_unsigned<T>
{ using type = unsigned long long; };
template <typename T, typename Abi>
struct as_unsigned<std::simd::basic_vec<T, Abi>>
{ using type = std::simd::rebind_t<as_unsigned_t<T>, std::simd::basic_vec<T, Abi>>; };
template <typename T0, typename T1>
constexpr T0
ulp_distance_signed(T0 val0, const T1& ref1)
{
if constexpr (std::is_floating_point_v<T1>)
return ulp_distance_signed(val0, std::simd::rebind_t<T1, T0>(ref1));
else if constexpr (std::is_floating_point_v<value_type_t<T0>>)
{
int fp_exceptions = 0;
if !consteval
{
fp_exceptions = std::fetestexcept(FE_ALL_EXCEPT);
}
using std::isnan;
using std::abs;
using T = value_type_t<T0>;
using L = std::numeric_limits<T>;
constexpr T0 signexp_mask = -L::infinity();
T0 ref0(ref1);
T1 val1(val0);
const auto subnormal = fabs(ref1) < L::min();
using I = as_unsigned_t<T1>;
const T1 eps1 = select(subnormal, L::denorm_min(),
L::epsilon() * std::bit_cast<T0>(
std::bit_cast<I>(ref1)
& std::bit_cast<I>(signexp_mask)));
const T0 ulp = select(val0 == ref0 || (isnan(val0) && isnan(ref0)),
T0(), T0((ref1 - val1) / eps1));
if !consteval
{
std::feclearexcept(FE_ALL_EXCEPT ^ fp_exceptions);
}
return ulp;
}
else
return ref1 - val0;
}
template <typename T0, typename T1>
constexpr T0
ulp_distance(const T0& val, const T1& ref)
{
auto ulp = ulp_distance_signed(val, ref);
using T = value_type_t<decltype(ulp)>;
if constexpr (std::is_unsigned_v<T>)
return ulp;
else
{
using std::abs;
return fabs(ulp);
}
}
template <typename T>
constexpr bool
bit_equal(const T& a, const T& b)
{
using std::simd::_UInt;
if constexpr (sizeof(T) <= sizeof(0ull))
return std::bit_cast<_UInt<sizeof(T)>>(a) == std::bit_cast<_UInt<sizeof(T)>>(b);
else if constexpr (std::simd::__simd_vec_or_mask_type<T>)
{
using TT = typename T::value_type;
if constexpr (std::is_integral_v<TT>)
return all_of(a == b);
else
{
constexpr size_t uint_size = std::min(size_t(8), sizeof(TT));
struct B
{
alignas(T) simd::rebind_t<_UInt<uint_size>,
simd::resize_t<T::size() * sizeof(TT) / uint_size, T>> data;
};
if constexpr (sizeof(B) == sizeof(a))
return all_of(std::bit_cast<B>(a).data == std::bit_cast<B>(b).data);
else
{
auto [a0, a1] = chunk<std::bit_ceil(unsigned(T::size())) / 2>(a);
auto [b0, b1] = chunk<std::bit_ceil(unsigned(T::size())) / 2>(b);
return bit_equal(a0, b0) && bit_equal(a1, b1);
}
}
}
else
static_assert(false);
}
// treat as equal if either:
// - operator== yields true
// - or for floats, a and b are NaNs
template <typename V>
constexpr bool
equal_with_nan_and_inf_fixup(const V& a, const V& b)
{
auto eq = a == b;
if (std::simd::all_of(eq))
return true;
else if constexpr (std::simd::__simd_vec_type<V>)
{
using M = typename V::mask_type;
using T = typename V::value_type;
if constexpr (std::is_floating_point_v<T>)
{ // fix up nan == nan results
eq |= a._M_isnan() && b._M_isnan();
}
else
return false;
return std::simd::all_of(eq);
}
else if constexpr (std::is_floating_point_v<V>)
return std::isnan(a) && std::isnan(b);
else
return false;
}
struct constexpr_verifier
{
struct ignore_the_rest
{
constexpr ignore_the_rest
operator()(auto const&, auto const&...)
{ return *this; }
};
bool okay = true;
constexpr ignore_the_rest
verify_precondition_failure(std::string_view expected_msg, auto&& f) &
{
try
{
f();
okay = false;
}
catch (const test::precondition_failure& failure)
{
okay = okay && failure.msg == expected_msg;
}
catch (...)
{
okay = false;
}
return {};
}
constexpr ignore_the_rest
verify(const auto& k) &
{
okay = okay && std::simd::all_of(k);
return {};
}
constexpr ignore_the_rest
verify_equal(const auto& v, const auto& ref) &
{
using V = decltype(std::simd::select(v == ref, v, ref));
okay = okay && equal_with_nan_and_inf_fixup<V>(v, ref);
return {};
}
constexpr ignore_the_rest
verify_bit_equal(const auto& v, const auto& ref) &
{
using V = decltype(std::simd::select(v == ref, v, ref));
okay = okay && bit_equal<V>(v, ref);
return {};
}
template <typename T, typename U>
constexpr ignore_the_rest
verify_equal(const std::pair<T, U>& x, const std::pair<T, U>& y) &
{
verify_equal(x.first, y.first);
verify_equal(x.second, y.second);
return {};
}
constexpr ignore_the_rest
verify_not_equal(const auto& v, const auto& ref) &
{
okay = okay && std::simd::all_of(v != ref);
return {};
}
constexpr ignore_the_rest
verify_equal_to_ulp(const auto& x, const auto& y, float allowed_distance) &
{
okay = okay && std::simd::all_of(ulp_distance(x, y) <= allowed_distance);
return {};
}
constexpr_verifier() = default;
constexpr_verifier(const constexpr_verifier&) = delete;
constexpr_verifier(constexpr_verifier&&) = delete;
};
template <int... is>
[[nodiscard]]
consteval bool
constexpr_test(auto&& fun, auto&&... args)
{
constexpr_verifier t;
try
{
fun.template operator()<is...>(t, args...);
}
catch(const test::precondition_failure& fail)
{
return false;
}
return t.okay;
}
template <typename T>
T
make_value_unknown(const T& x)
{ return *std::start_lifetime_as<T>(&x); }
template <typename T>
concept pair_specialization
= std::same_as<std::remove_cvref_t<T>, std::pair<typename std::remove_cvref_t<T>::first_type,
typename std::remove_cvref_t<T>::second_type>>;
struct runtime_verifier
{
const std::string_view test_kind;
template <typename X, typename Y>
additional_info
log_failure(const X& x, const Y& y, std::source_location loc, std::string_view s)
{
++failed_tests;
std::cout << loc.file_name() << ':' << loc.line() << ':' << loc.column() << ": in "
<< test_kind << " test of '" << test_name
<< "' " << s << " failed";
if constexpr (!std::is_same_v<X, log_novalue>)
{
std::cout << ":\n result: " << std::boolalpha;
if constexpr (is_character_type_v<X>)
std::cout << int(x);
else
std::cout << x;
if constexpr (!std::is_same_v<decltype(y), const log_novalue&>)
{
std::cout << "\n expected: ";
if constexpr (is_character_type_v<Y>)
std::cout << int(y);
else
std::cout << y;
}
}
std::cout << std::endl;
return additional_info {true};
}
[[gnu::always_inline]]
additional_info
verify_precondition_failure(std::string_view expected_msg, auto&& f,
std::source_location loc = std::source_location::current()) &
{
try
{
f();
++failed_tests;
return log_failure(log_novalue(), log_novalue(), loc, "precondition failure not detected");
}
catch (const test::precondition_failure& failure)
{
if (failure.msg != expected_msg)
{
++failed_tests;
return log_failure(failure.msg, expected_msg, loc, "unexpected exception");
}
else
{
++passed_tests;
return {};
}
}
catch (...)
{
++failed_tests;
return log_failure(log_novalue(), log_novalue(), loc, "unexpected exception");
}
}
[[gnu::always_inline]]
additional_info
verify(auto&& k, std::source_location loc = std::source_location::current())
{
if (std::simd::all_of(k))
{
++passed_tests;
return {};
}
else
return log_failure(log_novalue(), log_novalue(), loc, "verify");
}
[[gnu::always_inline]]
additional_info
verify_equal(auto&& x, auto&& y,
std::source_location loc = std::source_location::current())
{
bool ok;
if constexpr (pair_specialization<decltype(x)> && pair_specialization<decltype(y)>)
ok = std::simd::all_of(x.first == y.first) && std::simd::all_of(x.second == y.second);
else
ok = equal_with_nan_and_inf_fixup<decltype(std::simd::select(x == y, x, y))>(x, y);
if (ok)
{
++passed_tests;
return {};
}
else
return log_failure(x, y, loc, "verify_equal");
}
[[gnu::always_inline]]
additional_info
verify_bit_equal(auto&& x, auto&& y,
std::source_location loc = std::source_location::current())
{
using V = decltype(std::simd::select(x == y, x, y));
if (bit_equal<V>(x, y))
{
++passed_tests;
return {};
}
else
return log_failure(x, y, loc, "verify_bit_equal");
}
[[gnu::always_inline]]
additional_info
verify_not_equal(auto&& x, auto&& y,
std::source_location loc = std::source_location::current())
{
if (std::simd::all_of(x != y))
{
++passed_tests;
return {};
}
else
return log_failure(x, y, loc, "verify_not_equal");
}
// ulp_distance_signed can raise FP exceptions and thus must be conditionally executed
[[gnu::always_inline]]
additional_info
verify_equal_to_ulp(auto&& x, auto&& y, float allowed_distance,
std::source_location loc = std::source_location::current())
{
const bool success = std::simd::all_of(ulp_distance(x, y) <= allowed_distance);
if (success)
{
++passed_tests;
return {};
}
else
return log_failure(x, y, loc, "verify_equal_to_ulp")
("distance:", ulp_distance_signed(x, y),
"\n allowed:", allowed_distance);
}
};
template <int... is>
[[gnu::noinline, gnu::noipa]]
void
runtime_test(auto&& fun, auto&&... args)
{
runtime_verifier t {"runtime"};
fun.template operator()<is...>(t, make_value_unknown(args)...);
}
template <typename T>
concept constant_value = requires {
typename std::integral_constant<std::remove_cvref_t<decltype(T::value)>, T::value>;
};
template <typename T>
[[gnu::always_inline]] inline bool
is_const_known(const T& x)
{ return constant_value<T> || __builtin_constant_p(x); }
template <typename T, typename Abi>
[[gnu::always_inline]] inline bool
is_const_known(const std::simd::basic_vec<T, Abi>& x)
{ return __is_const_known(x); }
template <std::size_t B, typename Abi>
[[gnu::always_inline]] inline bool
is_const_known(const std::simd::basic_mask<B, Abi>& x)
{ return __is_const_known(x); }
template <std::ranges::sized_range R>
[[gnu::always_inline]] inline bool
is_const_known(const R& arr)
{
constexpr std::size_t N = std::ranges::size(arr);
constexpr auto [...is] = std::_IotaArray<N>;
return (is_const_known(arr[is]) && ...);
}
template <int... is>
[[gnu::always_inline, gnu::flatten]]
inline void
constprop_test(auto&& fun, auto... args)
{
runtime_verifier t{"constprop"};
#ifndef __clang__
t.verify((is_const_known(args) && ...))("=> Some argument(s) failed to constant-propagate.");
#endif
fun.template operator()<is...>(t, args...);
}
/**
* The value of the largest element in test_iota<V, Init>.
*/
template <typename V, int Init = 0, int Max = V::size() + Init - 1>
constexpr value_type_t<V> test_iota_max
= sizeof(value_type_t<V>) < sizeof(int)
? std::min(int(std::numeric_limits<value_type_t<V>>::max()),
Max < 0 ? std::min(V::size() + Init - 1,
int(std::numeric_limits<value_type_t<V>>::max()) + Max)
: Max)
: V::size() + Init - 1;
template <typename T, typename Abi, int Init, int Max>
requires std::is_enum_v<T>
constexpr T test_iota_max<simd::basic_vec<T, Abi>, Init, Max>
= static_cast<T>(test_iota_max<simd::basic_vec<std::underlying_type_t<T>, Abi>, Init, Max>);
/**
* Starts iota sequence at Init.
*
* With `Max == 0`: Wrap-around on overflow
* With `Max < 0`: Subtract from numeric_limits::max (to leave room for arithmetic ops)
* Otherwise: [Init..Max, Init..Max, ...] (inclusive)
*
* Use simd::__iota if a non-monotonic sequence is a bug.
*/
template <typename V, int Init = 0, int MaxArg = int(test_iota_max<V, Init>)>
constexpr V test_iota = V([](int i) {
constexpr int Max = MaxArg < 0 ? int(test_iota_max<V, Init, MaxArg>) : MaxArg;
static_assert(Max == 0 || Max > Init || V::size() == 1);
i += Init;
if constexpr (Max > Init)
{
while (i > Max)
i -= Max - Init + 1;
}
using T = value_type_t<V>;
return static_cast<T>(i);
});
/**
* A data-parallel object initialized with {values..., values..., ...}
*/
template <typename V, auto... values>
constexpr V init_vec = [] {
using T = typename V::value_type;
constexpr std::array<T, sizeof...(values)> arr = {T(values)...};
return V([&](size_t i) { return arr[i % arr.size()]; });
}();
template <typename V>
struct Tests;
template <typename T>
concept array_specialization
= std::same_as<T, std::array<typename T::value_type, std::tuple_size_v<T>>>;
template <typename Args = void, typename Fun = void>
struct add_test
{
alignas(std::bit_floor(sizeof(Args))) Args args;
Fun fun;
};
struct dummy_test
{
static constexpr std::array<int, 0> args = {};
static constexpr auto fun = [](auto&, auto...) {};
};
template <auto test_ref, int... is, std::size_t... arg_idx>
void
invoke_test_impl(std::index_sequence<arg_idx...>)
{
constexpr auto fun = test_ref->fun;
[[maybe_unused]] constexpr auto args = test_ref->args;
#ifdef EXPENSIVE_TESTS
constprop_test<is...>(fun, std::get<arg_idx>(args)...);
constexpr bool passed = constexpr_test<is...>(fun, std::get<arg_idx>(args)...);
if (passed)
++passed_tests;
else
{
++failed_tests;
std::cout << "=> constexpr test of '" << test_name << "' failed.\n";
}
#endif
runtime_test<is...>(fun, std::get<arg_idx>(args)...);
}
template <auto test_ref, int... is>
void
invoke_test(std::string_view name)
{
test_name = name;
constexpr auto args = test_ref->args;
using A = std::remove_const_t<decltype(args)>;
if constexpr (array_specialization<A>)
{ // call for each element
template for (constexpr std::size_t I : std::_IotaArray<args.size()>)
{
std::string tmp_name = std::string(name) + '|' + std::to_string(I);
test_name = tmp_name;
((std::cout << "Testing '" << test_name) << ... << (' ' + std::to_string(is)))
<< ' ' << args[I] << "'\n";
invoke_test_impl<test_ref, is...>(std::index_sequence<I>());
}
}
else
{
((std::cout << "Testing '" << test_name) << ... << (' ' + std::to_string(is))) << "'\n";
invoke_test_impl<test_ref, is...>(std::make_index_sequence<std::tuple_size_v<A>>());
}
}
#define ADD_TEST(name, ...) \
template <int> \
static constexpr auto name##_tmpl = dummy_test {}; \
\
const int init_##name = [] { \
test_functions.push_back([] { invoke_test<&name##_tmpl<0>>(#name); }); \
return 0; \
}(); \
\
template <int Tmp> \
requires (Tmp == 0) __VA_OPT__(&& (__VA_ARGS__)) \
static constexpr auto name##_tmpl<Tmp> = add_test
#define ADD_TEST_N(name, N, ...) \
template <int> \
static constexpr auto name##_tmpl = dummy_test {}; \
\
static void \
name() \
{ \
template for (constexpr int i : std::_IotaArray<N, int>) \
invoke_test<&name##_tmpl<0>, i>(#name); \
} \
\
const int init_##name = [] { \
test_functions.push_back(name); \
return 0; \
}(); \
\
template <int Tmp> \
requires (Tmp == 0) __VA_OPT__(&& (__VA_ARGS__)) \
static constexpr auto name##_tmpl<Tmp> = add_test
void create_tests();
int main()
{
create_tests();
try
{
for (auto f : test_functions)
f();
}
catch(const test::precondition_failure& fail)
{
std::cout << fail.file << ':' << fail.line << ": Error: precondition '" << fail.expr
<< "' does not hold: " << fail.msg << '\n';
return EXIT_FAILURE;
}
std::cout << "Passed tests: " << passed_tests << "\nFailed tests: " << failed_tests << '\n';
return failed_tests != 0 ? EXIT_FAILURE : EXIT_SUCCESS;
}
#endif // SIMD_TEST_SETUP_H