GenTree::SupportsSettingZeroFlag() returns true for any GT_HWINTRINSIC whose instruction has the Writes_ZF bit, which includes bsf/bsr. But BSF/BSR set ZF from the source being zero, not from the result, so lowering drops the EQ/NE ... 0 compare and emits a SETCC/Jcc reading the wrong flag. Silent wrong code on any x64 CPU without LZCNT/BMI1, where corelib falls back to X86Base.BitScanReverse/BitScanForward.
Minimal Repro
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
class Program
{
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
static bool Log2IsZero(uint x) => BitOperations.Log2(x) == 0;
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
static bool Log2NotZero(uint x) => BitOperations.Log2(x) != 0;
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
static bool TzcIsZero(uint x) => BitOperations.TrailingZeroCount(x) == 0;
static int Main()
{
Console.WriteLine("Log2IsZero(1) = {0} expected True", Log2IsZero(1));
Console.WriteLine("Log2NotZero(1) = {0} expected False", Log2NotZero(1));
Console.WriteLine("TzcIsZero(1) = {0} expected True", TzcIsZero(1));
bool ok = Log2IsZero(1) && !Log2NotZero(1) && TzcIsZero(1);
Console.WriteLine(ok ? "PASS" : "FAIL");
return ok ? 100 : 101;
}
}
set DOTNET_EnableAVX2=0
corerun out\s3bsf.dll
DOTNET_EnableAVX2=0 disables LZCNT/TZCNT so that corelib's X86Base.BitScanReverse/BitScanForward fallbacks are taken; no switch is needed on real pre-Haswell x64 hardware.
Expected
Log2IsZero(1) = True expected True
Log2NotZero(1) = False expected False
TzcIsZero(1) = True expected True
PASS
Actual
Log2IsZero(1) = False expected True
Log2NotZero(1) = True expected False
TzcIsZero(1) = False expected True
FAIL
; Program:Log2IsZero(uint):bool (FullOpts)
or ecx, 1
bsr eax, ecx
sete al ; <-- reads ZF set by BSR == "ecx was zero", NOT "eax == 0"
ret
; Program:TzcIsZero(uint):bool (FullOpts)
test ecx, ecx
bsf eax, ecx
sete al ; <-- reads ZF set by BSF, not a compare of eax
ret
Notes
- Root cause: the
GT_HWINTRINSIC clause in GenTree::SupportsSettingZeroFlag() (gentree.cpp) keys off emitter::DoesWriteZeroFlag, i.e. the Writes_ZF bit in instrsxarch.h, which means "ZF is architecturally defined", not "ZF reflects result == 0".
emitter::AreFlagsSetToZeroCmp (used by genCanAvoidEmittingCompareAgainstZero) has the same unsound test, so INS_bsf/INS_bsr must be excluded in both places (or Writes_ZF split in two).
- Note the neighbouring comment explaining why
GT_ROL/GT_ROR are excluded — bsf/bsr are the same class of problem via the HW-intrinsic path.
- FullOpts only (
DOTNET_JITMinOpts=1 is correct); main @ b44cd904110a27d96ea83621e94332d55150d482 and .NET 10.0.12 both repro, so not a recent regression.
GenTree::SupportsSettingZeroFlag()returnstruefor anyGT_HWINTRINSICwhose instruction has theWrites_ZFbit, which includesbsf/bsr. ButBSF/BSRset ZF from the source being zero, not from the result, so lowering drops theEQ/NE ... 0compare and emits aSETCC/Jccreading the wrong flag. Silent wrong code on any x64 CPU without LZCNT/BMI1, where corelib falls back toX86Base.BitScanReverse/BitScanForward.Minimal Repro
DOTNET_EnableAVX2=0disables LZCNT/TZCNT so that corelib'sX86Base.BitScanReverse/BitScanForwardfallbacks are taken; no switch is needed on real pre-Haswell x64 hardware.Expected
Actual
Notes
GT_HWINTRINSICclause inGenTree::SupportsSettingZeroFlag()(gentree.cpp) keys offemitter::DoesWriteZeroFlag, i.e. theWrites_ZFbit ininstrsxarch.h, which means "ZF is architecturally defined", not "ZF reflectsresult == 0".emitter::AreFlagsSetToZeroCmp(used bygenCanAvoidEmittingCompareAgainstZero) has the same unsound test, soINS_bsf/INS_bsrmust be excluded in both places (orWrites_ZFsplit in two).GT_ROL/GT_RORare excluded —bsf/bsrare the same class of problem via the HW-intrinsic path.DOTNET_JITMinOpts=1is correct);main@b44cd904110a27d96ea83621e94332d55150d482and .NET 10.0.12 both repro, so not a recent regression.