Skip to content

Commit 257c0e2

Browse files
DiamonDinoiaserge-sans-paille
authored andcommitted
feat: common double -> int64 fast_cast
batch_cast<int64_t>(batch<double>) fell back to a scalar loop through a stack buffer on every architecture without a native instruction. Split trunc(x) into hi * 2^32 + lo and read each half out of the mantissa with the 1.5 * 2^52 magic constant: both halves are small enough that the add is exact, so the result is independent of the rounding mode. Selected on sse2, sse4.1, avx, avx2, avx512f-without-DQ, vsx, vxe and wasm. avx512dq keeps vcvttpd2qq, neon64, sve, rvv and emulated keep their own. ns/element on a 128 KB L2-resident loop (Xeon w5-3435X, gcc 13.3 -O3, min of 300 reps, interleaved): sse2 2.387 -> 1.896 1.26x sse4.1 2.332 -> 0.636 3.67x avx 1.221 -> 0.501 2.44x avx2 1.221 -> 0.289 4.22x avx512f 0.697 -> 0.206 3.38x The unchanged int64 -> double control arm moves by at most 13%. test_cast_all_lanes covers float -> int32 and double -> int64 with a different value in every lane; the other cast tests are splats that only inspect lane 0. Assisted-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 04f3d21 commit 257c0e2

2 files changed

Lines changed: 93 additions & 2 deletions

File tree

include/xsimd/arch/common/xsimd_common_details.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,26 @@ namespace xsimd
251251
return bitwise_cast<int64_t>(self);
252252
}
253253

254+
// Common double -> int64_t cast. Adding 1.5 * 2^52 puts an integer
255+
// |v| < 2^51 exactly in the mantissa, so subtracting the bit patterns
256+
// reads it back out. trunc first, then split into halves that small.
257+
// An architecture reaching this overload needs a non-common trunc:
258+
// the common one goes through to_int and would recurse into here.
259+
template <class A>
260+
XSIMD_INLINE batch<int64_t, A> fast_cast(batch<double, A> const& x, batch<int64_t, A> const&, requires_arch<common>) noexcept
261+
{
262+
using batch_type = batch<double, A>;
263+
batch_type magic(0x1.8p52);
264+
batch<int64_t, A> magic_i = bitwise_cast<int64_t>(magic);
265+
batch_type t = trunc(x);
266+
batch_type hi = trunc(t * batch_type(0x1p-32)); // |hi| <= 2^31
267+
batch_type lo = t - hi * batch_type(0x1p32); // exact, |lo| < 2^32
268+
detail::reassociation_barrier(lo, "keep lo below 2^32 before the magic add");
269+
batch<int64_t, A> hi_i = bitwise_cast<int64_t>(hi + magic) - magic_i;
270+
batch<int64_t, A> lo_i = bitwise_cast<int64_t>(lo + magic) - magic_i;
271+
return (hi_i << 32) + lo_i;
272+
}
273+
254274
// Provide a common uint32_t -> float cast only if we have a
255275
// non-common int32_t -> float fast_cast
256276
template <class A, class = decltype(fast_cast(std::declval<batch<int32_t, A> const&>(), std::declval<batch<float, A> const&>(), A {}))>

test/test_batch_cast.cpp

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
#include "test_utils.hpp"
1616

17+
#include <random>
18+
1719
#if !XSIMD_WITH_NEON || XSIMD_WITH_NEON64
1820
namespace detail
1921
{
@@ -152,6 +154,12 @@ struct batch_cast_test
152154
};
153155
}
154156

157+
void test_cast_all_lanes() const
158+
{
159+
test_cast_all_lanes_impl<float_batch, int32_batch>("batch cast float -> int32");
160+
test_cast_all_lanes_impl<double_batch, int64_batch>("batch cast double -> int64");
161+
}
162+
155163
void test_bool_cast() const
156164
{
157165
test_bool_cast_impl<float_batch, int32_batch>("batch bool cast float -> int32");
@@ -345,6 +353,56 @@ struct batch_cast_test
345353
}
346354
}
347355

356+
// A float -> same-width-int cast can recombine per-lane data, so every lane has to
357+
// carry a different value; the other cast tests are splats and only look at lane 0.
358+
template <class B_in, class B_out>
359+
void test_cast_all_lanes_impl(const std::string& name) const
360+
{
361+
using T_in = typename B_in::value_type;
362+
using T_out = typename B_out::value_type;
363+
constexpr int digits = std::numeric_limits<T_out>::digits; // 31 or 63
364+
const T_in beyond = std::ldexp(T_in(1), digits); // one past the top of T_out
365+
const T_in top = std::nextafter(beyond, T_in(0)); // the largest T_in that fits
366+
// The ends of the range, then every power of two the cast can reach probed on
367+
// both sides: those are where a carry between halves and the end of the
368+
// mantissa live, and a random draw never lands on one of them.
369+
std::vector<T_in> values = { T_in(0), -T_in(0), -beyond, top, -top };
370+
for (int e = 0; e < digits; ++e)
371+
{
372+
const T_in p = std::ldexp(T_in(1), e);
373+
for (const T_in v : { p - T_in(1.5), p - T_in(1), p - T_in(.5), std::nextafter(p, T_in(0)),
374+
p, std::nextafter(p, beyond), p + T_in(.5), p + T_in(1) })
375+
{
376+
values.push_back(v);
377+
values.push_back(-v);
378+
}
379+
}
380+
// Eight random values in every binade the cast can reach, both signs. The
381+
// engine is default seeded, so a failure reproduces.
382+
std::default_random_engine generator;
383+
std::uniform_real_distribution<T_in> mantissa(T_in(1), T_in(2));
384+
for (int e = -1; e < digits; ++e)
385+
for (int k = 0; k < 8; ++k)
386+
{
387+
const T_in v = std::ldexp(mantissa(generator), e);
388+
values.push_back(v);
389+
values.push_back(-v);
390+
}
391+
constexpr size_t n = B_in::size;
392+
T_in buffer[n];
393+
for (size_t i = 0; i < values.size(); i += n)
394+
{
395+
for (size_t l = 0; l < n; ++l)
396+
buffer[l] = values[(i + l) % values.size()];
397+
B_out res = xsimd::batch_cast<T_out>(B_in::load_unaligned(buffer));
398+
for (size_t l = 0; l < n; ++l)
399+
{
400+
INFO(name, ", lane ", l, " holding ", buffer[l]);
401+
CHECK_SCALAR_EQ(static_cast<T_out>(buffer[l]), res.get(l));
402+
}
403+
}
404+
}
405+
348406
template <class B_in, class B_out>
349407
void test_bool_cast_impl(const std::string& name) const
350408
{
@@ -380,6 +438,11 @@ TEST_CASE_TEMPLATE("[xsimd cast tests]", B, CONVERSION_TYPES)
380438
{
381439
Test.test_cast();
382440
}
441+
442+
SUBCASE("cast all lanes")
443+
{
444+
Test.test_cast_all_lanes();
445+
}
383446
}
384447
#endif
385448
#if 0 && XSIMD_X86_INSTR_SET > D_X86_AVX_VERSION
@@ -396,19 +459,27 @@ TYPED_TEST(batch_cast_test, cast_sizeshift2)
396459
}
397460
#endif
398461

462+
// neon32 has no batch<double>, and detail::uses_fast_cast_v lives inside the guard
463+
// that excludes it, so the whole block needs the same guard.
464+
#if !XSIMD_WITH_NEON || XSIMD_WITH_NEON64
399465
// sve and rvv used to declare fast_cast in detail_sve / detail_rvv, where the
400466
// dispatcher in kernel::detail cannot see it, so every conversion silently fell back
401-
// to the scalar loop. Assert on those two arches as well as on sse2.
402-
#if XSIMD_WITH_SSE2 || XSIMD_WITH_SVE || XSIMD_WITH_RVV
467+
// to the scalar loop. int32 <-> float has no common overload, so both asserts below
468+
// fail if an architecture declares its fast_cast outside kernel::detail again.
403469
TEST_CASE_TEMPLATE("[xsimd cast tests]", B, CONVERSION_TYPES)
404470
{
405471
SUBCASE("use fastcast")
406472
{
407473
using A = xsimd::default_arch;
474+
#if XSIMD_WITH_SSE2 || XSIMD_WITH_SVE || XSIMD_WITH_RVV
408475
static_assert(detail::uses_fast_cast_v<A, int32_t, float>,
409476
"expected int32 to float conversion to use fast_cast");
410477
static_assert(detail::uses_fast_cast_v<A, float, int32_t>,
411478
"expected float to int32 conversion to use fast_cast");
479+
#endif
480+
// the common overload answers double -> int64_t on every architecture
481+
static_assert(detail::uses_fast_cast_v<A, double, int64_t>,
482+
"expected double to int64 conversion to use fast_cast");
412483
}
413484
}
414485
#endif

0 commit comments

Comments
 (0)