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.cppandtest/packetmath.cppparts 1 and 2 pass on Apple M1 and under x86_64 SSE/AVX2 Rosetta. Packet parts 13 and 15 (half andbfloat16) pass on M1 and AVX2 Rosetta.- A focused scalar/array smoke test passes with
-ffast-mathand DAZ enabled; the parent fails. The changed lines passgit 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 |