Core: Preserve float and double sign under DAZ/FTZ

Classify float and double subnormals by their binary encodings so sign stays correct with DAZ/FTZ. Real packets with an integer packet classify zero from bits; NaN payloads are preserved and either signed zero returns +0. The bfloat16 packet gap is tracked separately in #3182 (closed).

Behavior change: Vectorized sign(-NaN) previously returned a positive NaN. It now preserves the negative sign bit, matching the scalar path; this is user-visible.

Scalar-path check under Rosetta

x86_64 SSE, 16,384 float coefficients Parent 087757ad (ns/coefficient) Updated (ns/coefficient)
Ordinary inputs 0.20 0.19
Subnormal inputs 0.20 0.19
One subnormal per 64 0.20 0.19

These are medians of nine timings per invocation, repeated in an A/B/B/A order, using the same ArrayXf::sign() loop with Apple clang 21, C++14, -O3 -DNDEBUG -msse2. The x86_64 binary ran under Rosetta on an Apple M1, so these numbers check the scalar-path regression rather than native x86 performance. The first MR revision measured about 1.74 ns/coefficient on the same loop because it called an out-of-line zero helper.

Native x86_64 performance (prior revision 983f0b75)

The integer-classification change in 4056d8d3 has not yet been benchmarked natively; these figures apply to the prior revision.

Medians for ArrayXf/ArrayXd::sign() on 16,384 coefficients, measured in A/B/B/A order on a 13th Gen Intel Core i7-13700HX (ns/coefficient; parent → 983f0b75):

Path Compiler Parent 983f0b75
SSE2 float (scalar path) gcc-13 0.92 0.21
SSE2 float (scalar path) clang-20 0.15 0.16
SSE2 double gcc-13, clang-20 0.24 0.44
AVX2 float clang-20 0.061 0.068
AVX2 float gcc-13 0.067 0.095
AVX2 double clang-20 0.125 0.135
AVX2 double gcc-13 0.13 0.19

Reference issue

Related to TensorFlow PR #127954, which needs the fix in Eigen.

Validation

  • The new scalar test fails against the parent; the packet test fails against the parent on AVX2. Both pass on this branch.
  • test/numext.cpp and test/packetmath.cpp parts 1 and 2 pass on Apple M1 and under x86_64 SSE/AVX2 Rosetta. Packet parts 13 and 15 (half and bfloat16) pass on M1 and AVX2 Rosetta.
  • A focused scalar/array smoke test passes with -ffast-math and DAZ enabled; the parent fails. The changed lines pass git diff --check; clang-format 17 is unavailable locally.
  • Google Benchmark/CMake and CUDA/SYCL runtimes are unavailable locally; the committed benchmark and device backends were not run.

Update: Following @chuckyschluz's review, the packet zero check is fixed in place, scalar double is covered, and the x86 packet regression enables DAZ. The scalar path now classifies zero inline to avoid the call overhead. bfloat16 packets will be handled under #3182 (closed).

Update (4b4803e): Following Rasmus Munk Larsen's AVX-only review, Packet4d::sign() now classifies both SSE halves by integer encodings when AVX2 is unavailable. The existing DAZ regression and the new runtime-input fast-math NaN regression both fail at 4056d8d and pass with this fix. AVX-only packetmath part 2 and fast-math checks pass with GCC 8.5 on native x86-64 and Apple clang 21 under Rosetta; fast-math also passes on SSE2/AVX2 under Rosetta and native ARM64. Changed lines pass clang-format 17.0.6, check_style.py, and git diff --check; smoke pipeline 2891117014 is green at this head.

The AVX-only integer classification costs about 0.30 ns per double for cache-resident ordinary/mixed arrays on the tested CPU. AVX2 retains its existing implementation.

AVX-only double benchmark at 4b4803ea

bench_cwise_math.cpp, ArrayXd::sign(), Google Benchmark 1.9.5, GCC 8.5, C++14, -O3 -DNDEBUG -mavx -mno-avx2, on an INTEL(R) XEON(R) GOLD 6526Y. CPU affinity was fixed to core 0; clocks were not locked. Measurements used two A/B pairs, nine repetitions per invocation, minimum time 0.02 s. Values below are CPU ns/coefficient: the range of the two invocation medians and the maximum within-invocation standard deviation. The 262,144-coefficient cases were noisy, so no performance conclusion is drawn for that size.

Coefficients Inputs 4056d8d3 median range 4b4803ea median range Max SD (before / after)
1024 ordinary 0.342–0.343 0.639 0.002 / 0.003
16384 ordinary 0.341 0.637 0.001 / <0.001
262144 ordinary 0.661–0.665 0.724–0.729 0.124 / 0.106
1024 subnormal 0.340 0.635–0.636 0.002 / 0.003
16384 subnormal 0.340–0.416 0.637 0.001 / <0.001
262144 subnormal 0.655–0.659 0.731–0.736 0.135 / 0.105
1024 one subnormal per 64 0.340 0.634–0.635 0.003 / 0.003
16384 one subnormal per 64 0.340–0.341 0.637 0.001 / <0.001
262144 one subnormal per 64 0.650–0.659 0.716–0.735 0.126 / 0.109
Edited by Saicharan Ramineni

Merge request reports

Loading
Loading