Skip to content

JIT: (bug) x64: BitOperations.Log2/TrailingZeroCount compared against 0 produces wrong results when the BSR/BSF fallback is used #133783

Description

@EgorBo

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions