Fix saturating subtraction when the subtrahend is the type minimum#1380
Fix saturating subtraction when the subtrahend is the type minimum#1380vsaraikin wants to merge 1 commit into
Conversation
|
Thanks for spotting this. There's a bit of cruft in the patch (including some surprising formatting change), but the approach looks good. I leave it to you to fix CI before having a look ;-) |
9837081 to
50befe4
Compare
|
Thanks for the review! Fixed both points:
|
| { | ||
| if (std::is_signed<T>::value) | ||
| { | ||
| return sadd(self, -other); |
There was a problem hiding this comment.
why directly to commmon here? are there no intrinsics for it?
There was a problem hiding this comment.
indeed there's
_mm256_subs_epi16
_mm256_subs_epu16
_mm256_subs_epi8
_mm256_subs_epu8
There was a problem hiding this comment.
You're right, I've since dropped the common detour and implemented it inline here, mirroring the sadd right above
And yeah, as Serge noted, AVX2 has subs_epi8/epi16, those stay on the native path in avx2::ssub. This overload only handles the 32/64-bit case, which has no saturating-sub intrinsic
There was a problem hiding this comment.
I mean you can also emulate saturating using sub + select no? Would that be faster that a scalar loop?
| { | ||
| if (std::is_signed<T>::value) | ||
| { | ||
| return sadd(self, -other); |
There was a problem hiding this comment.
it comes with AVX512BW. Yet falling back to avx for [u]int[8[16]_t is probably better.
There was a problem hiding this comment.
happy to make the 8/16 path fall back to avx instead if you'd prefer
| } | ||
| else if ((rhs > 0) && (lhs < std::numeric_limits<T>::lowest() + rhs)) | ||
| { | ||
| return std::numeric_limits<T>::lowest(); |
There was a problem hiding this comment.
matching the sadd just above it, which uses max() / lowest()
lowest() (not min()) is deliberate so it's also correct for floating-point T, where min() is the smallest positive value
for integers they're identical
50befe4 to
bdfcf69
Compare
| { | ||
| if (std::is_signed<T>::value) | ||
| { | ||
| return sadd(self, -other); |
There was a problem hiding this comment.
it comes with AVX512BW. Yet falling back to avx for [u]int[8[16]_t is probably better.
bdfcf69 to
584ffb7
Compare
ssub was implemented as sadd(x, -rhs), but -rhs is not representable when rhs == numeric_limits<T>::min(), so e.g. ssub(1, INT_MIN) wrapped to a large negative value instead of saturating to INT_MAX. Compute the saturating difference directly: - scalar overload and the common batch kernel (used by SSE2 for 32/64-bit integers, which have no native saturating-subtract intrinsic); - AVX and AVX512F integer ssub, which had their own copies of the sadd(x, -rhs) pattern, now mirror their local sadd implementations. For 8/16-bit integers the saturating-sub intrinsic requires AVX512BW; in the AVX512F fallback, delegate those widths to AVX2 on each 256-bit half.
584ffb7 to
871b0ce
Compare
|
hey, the MSVC crash was the test blowing the stack in the big templated test case (moved the new checks to a standalone one) + added the AVX512F -> AVX2 fallback for 8/16-bit |
Bug
Saturating subtraction
ssubwraps instead of saturating when the subtrahend is the type minimum:Root cause
Both the scalar overload (
arch/xsimd_scalar.hpp) and the common batch kernel (arch/common/xsimd_common_arithmetic.hpp) implement signedssub(lhs, rhs)assadd(lhs, -rhs). Whenrhs == numeric_limits<T>::min(),-rhsis not representable (it wraps back tomin(), and is signed-overflow UB in the scalar case), so the saturating add is fed the wrong operand and the result wraps.The common kernel matters beyond the scalar API: SSE/SSE2 only provide native saturating subtraction for 8- and 16-bit lanes, so
batch<int32_t>/batch<int64_t>ssubdispatch to this common implementation on x86.Fix
Compute the saturating difference directly, mirroring the existing signed
sadd:rhsand compare againstmax() + rhs/lowest() + rhs(both safe from overflow because of the sign), elselhs - rhs.select(other >= 0, max(min + other, self), min(max + other, self)) - other, the direct analogue of the signedsaddkernel above it.The native NEON/SSE 8/16-bit paths (
vqsubq_*,_mm_subs_*) are unaffected — only the scalar overload and the common fallback change.Testing
Extended
test_ssubintest/test_xsimd_api.cppwith the min-subtrahend cases (ssub(0, min)andssub(1, min)must saturate tomaxfor signed types). The previous test only coveredssub(122, 121), so this edge was untested.Verified exhaustively out of tree: an all-pairs
int8scalar sweep is now 0/65536 wrong (was failing for everyrhs == -128), and the forced common-kernelbatch<int32_t>cases withINT_MINnow returnINT_MAX, on Apple Silicon (arm64).