Skip to content

Unnecessary sign-extension for LeadingZeroCount(UInt64) #119699

Description

@xtqqczze
public static class LeadingZeroCount {
    public static nint M1(ulong mask) {
        return BitOperations.LeadingZeroCount(mask);
    }
    
    public static nint M2(ulong mask) {
        return (nint)ulong.LeadingZeroCount(mask);
    }
}

public static class TrailingZeroCount {
    public static nint M1(ulong mask) {
        return BitOperations.TrailingZeroCount(mask);
    }
    
    public static nint M2(ulong mask) {
        return (nint)ulong.TrailingZeroCount(mask);
    }
}

public static class PopCount {
    public static nint M1(ulong mask) {
        return BitOperations.PopCount(mask);
    }
    
    public static nint M2(ulong mask) {
        return (nint)ulong.PopCount(mask);
    }
}
// coreclr trunk-20250917+116db00b333e27b70b6e97c93bcfb304ca4245ab

LeadingZeroCount:M1(ulong):nint (FullOpts):
       xor      eax, eax
       lzcnt    rax, rdi
       cdqe     
       ret      

LeadingZeroCount:M2(ulong):nint (FullOpts):
       xor      eax, eax
       lzcnt    rax, rdi
       ret      

TrailingZeroCount:M1(ulong):nint (FullOpts):
       xor      eax, eax
       tzcnt    rax, rdi
       cdqe     
       ret      

TrailingZeroCount:M2(ulong):nint (FullOpts):
       xor      eax, eax
       tzcnt    rax, rdi
       ret      

PopCount:M1(ulong):nint (FullOpts):
       xor      eax, eax
       popcnt   rax, rdi
       cdqe     
       ret      

PopCount:M2(ulong):nint (FullOpts):
       xor      eax, eax
       popcnt   rax, rdi
       ret             

godbolt.org

Activity

  1. added
    needs-area-labelAn area label is needed to ensure this gets routed to the appropriate area owners
    on Sep 14, 2025
  2. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    and removed
    needs-area-labelAn area label is needed to ensure this gets routed to the appropriate area owners
    on Sep 14, 2025
  3. dotnet-policy-service commented on Sep 14, 2025

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  4. pinkfloydx33 commented on Sep 15, 2025

    @pinkfloydx33
  5. added this to the Future milestone on Sep 15, 2025
  6. added
    help wanted[up-for-grabs] Good issue for external contributors
    and removed
    untriagedNew issue has not been triaged by the area owner
    on Sep 15, 2025
  7. EgorBo commented on Sep 15, 2025

    @EgorBo
    Member

    Presumably should be trivial to fix, thanks for filing

  8. xtqqczze commented on Sep 16, 2025

    @xtqqczze
    ContributorAuthor

    Confusingly the intrinsic is now named NI_AVX2_X64_LeadingZeroCount, despite LZCNT not being related to AVX2.

  9. tannergooding commented on Sep 16, 2025

    @tannergooding
    Member

    While the intrinsic isn't technically part of the AVX2 CPUID bit, it is logically part of the AVX2 family of instructions alongside BMI1, BMI2, F16C, and FMA. That is, the ISA bits were all introduced simultaneously and have not appeared in hardware independently. -- Formally, Intel defines POPCNT is part of SSE4.2 and LZCNT is part of BMI1, they exist as separate CPUID bits for compat with AMD which had them in their own earlier ABM instruction set.

    The JIT took several simplifications last release to "merge" various logical ISA groupings together. This was done to significantly simplify the testing, support, and general codegen complexity matrices. Such logical groupings are always disabled or enabled together and this flows with the new "unified" versioning scheme that is intended to exist moving forward under AVX10.

    In .NET 10, we have:

    • CMOV+CX8+SSE+SSE2 - baseline, logically x86-64-v1
    • SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT - logically x86-64-v2
    • AVX - kept because enough real world AVX only hardware existed, making it significant enough to support
    • AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE - logically x86-64-v3
    • AVX512F+BW+CD+DQ+VL - logically x86-64-v4

    We then define some "pseudo-versions" for AVX512v2 (IFMA+VBMI) and AVX512v3 (BITALG+VBMI2+VNNI+VPOPCNTDQ), which also represent the real world logical groupings. After that, we just have AVX10v1, AVX10v2, and will continue versioning this way in the future.

    In .NET 11 we have raised the baseline to x86-64-v2 and so NI_SSE42_* no longer exists, it is simply part of NI_X86Base_*

  10. xtqqczze commented on Sep 16, 2025

    @xtqqczze
    ContributorAuthor

    @tannergooding The naming here does feel a bit misleading, especially considering the existence of vector leading-zero count instructions.

  11. xtqqczze commented on Sep 16, 2025

    @xtqqczze
    ContributorAuthor

    Look like the sign-extension for the M2 case was elided starting from .NET 8.

  12. tannergooding commented on Sep 16, 2025

    @tannergooding
    Member

    It's no different than other scalar vs vector form instructions that exist in a given ISA. We differentiate where important/relevant

    We can always do so here in the future if it becomes important. For now, it was the change that allowed simplification of the JIT without also being a massively in depth refactoring

  13. Lotendan commented on Nov 15, 2025

    @Lotendan
    Contributor

    I have looked into this and compiled the first LeadingZeroCount class.

    This is the corresponding GenTree:

    [000004] -----------                         *  RETURN    long  
    [000003] -----------                         \--*  CAST      long <- int
    [000002] ---------U-                            \--*  CAST      int <- ulong
    [000001] -----------                               \--*  HWINTRINSIC long   0 LeadingZeroCount
    [000000] -----------                                  \--*  LCL_VAR   long   V00 arg0

    Obviously this is the node that generates a cdqe:

    CAST      long <- int

    Help appreciated here: not too sure about what is best?

    1. we morph the double cast into a single long <- ulong cast without sign extension IFFF we know that the intermediate type (int here) is always positive (as is the case here with LeadingZeroCount)?
      A bit similar to what is done here:
      https://github.com/dotnet/runtime/blob/main/src/coreclr/jit/morph.cpp#L369

    2. or maybe we should skip emitting the cdqe straight at code generation time, and hardcode lzcnt/popcnt/tzcnt instructions here?
      https://github.com/dotnet/runtime/blob/main/src/coreclr/jit/emitxarch.cpp#L1036

    3. any other suggestion?

    Also tagging @saucecontrol because this looks relatively similar to what we did with popcnt in my other PR.

  14. saucecontrol commented on Nov 15, 2025

    @saucecontrol
    Member

    Option 1 sounds reasonable to me. You'd need to be able to prove not only that the value is non-negative but also that the value would fit in the intermediate type. The range assertions on PopCount and pals do that, so you should be good.

  15. Lotendan commented on Nov 20, 2025

    @Lotendan
    Contributor

    I have looked into it this evening and there are a couple of things I'm unsure of:

    I'm not sure to understand why the first CAST is marked as U unsigned although the return type from LeadingZeroCount is long.
    It is important because IntegralRange::ForCastOutput propagates the "non-negative" flag through a chain of casts:

    /* static */ IntegralRange IntegralRange::ForCastOutput(GenTreeCast* cast, Compiler* compiler)

    Also, any idea about why this condition is limited to TYP_INT?

    if ((fromType == TYP_INT) && fromUnsigned)

    If I understand properly, propagating the unsigned in the second cast would allow to use zero-extension instead of sign-extension.

    Thanks

  16. saucecontrol commented on Nov 20, 2025

    @saucecontrol
    Member

    The type you see on each node in the IR is what's called the JIT type, which is defined in the 3rd column of this table:

    DEF_TP(BYTE ,"byte" , TYP_INT, 1, 1, 4, 1, 1, VTR_INT, availableIntRegs, RBM_INT_CALLEE_SAVED, RBM_INT_CALLEE_TRASH, VTF_INT)
    DEF_TP(UBYTE ,"ubyte" , TYP_INT, 1, 1, 4, 1, 1, VTR_INT, availableIntRegs, RBM_INT_CALLEE_SAVED, RBM_INT_CALLEE_TRASH, VTF_INT|VTF_UNS)
    DEF_TP(SHORT ,"short" , TYP_INT, 2, 2, 4, 1, 2, VTR_INT, availableIntRegs, RBM_INT_CALLEE_SAVED, RBM_INT_CALLEE_TRASH, VTF_INT)
    DEF_TP(USHORT ,"ushort" , TYP_INT, 2, 2, 4, 1, 2, VTR_INT, availableIntRegs, RBM_INT_CALLEE_SAVED, RBM_INT_CALLEE_TRASH, VTF_INT|VTF_UNS)
    DEF_TP(INT ,"int" , TYP_INT, 4, 4, 4, 1, 4, VTR_INT, availableIntRegs, RBM_INT_CALLEE_SAVED, RBM_INT_CALLEE_TRASH, VTF_INT|VTF_I32)
    DEF_TP(UINT ,"uint" , TYP_INT, 4, 4, 4, 1, 4, VTR_INT, availableIntRegs, RBM_INT_CALLEE_SAVED, RBM_INT_CALLEE_TRASH, VTF_INT|VTF_UNS|VTF_I32) // Only used in GT_CAST nodes
    DEF_TP(LONG ,"long" , TYP_LONG, 8,EPS,EPS, 2, 8, VTR_INT, availableIntRegs, RBM_INT_CALLEE_SAVED, RBM_INT_CALLEE_TRASH, VTF_INT|VTF_I64)
    DEF_TP(ULONG ,"ulong" , TYP_LONG, 8,EPS,EPS, 2, 8, VTR_INT, availableIntRegs, RBM_INT_CALLEE_SAVED, RBM_INT_CALLEE_TRASH, VTF_INT|VTF_UNS|VTF_I64) // Only used in GT_CAST nodes

    You'll see that both long and ulong have a JIT type of TYP_LONG. Likewise, all primitive types 4 bytes or smaller have a JIT type of TYP_INT.

    For HWIntrinsic nodes like LeadingZeroCount, the real data type (ulong in this case) is stored in SimdBaseType, while the node itself is the JIT type (long in this case, because it returns a scalar value).

    Whether a cast is sign extending or not is determined by either the from type of the cast (for small types, like ubyte and ushort) or the unsigned flag on the node (for uint and ulong), but the node itself will have the JIT type.

    Hope that clears it up 😄

    I think you're looking in the right place, because it looks to me like we're ignoring the fact that your cast operand brought its own narrow range (0 to 127) assertion in, and it's leaving with a wider range (int.MinValue to int.MaxValue).

  17. xtqqczze commented on May 28, 2026

    @xtqqczze
    ContributorAuthor

    Looks like this might be addressed by #128658

  18. tannergooding commented on May 28, 2026

    @tannergooding
    Member

    There's a few different range check things I'm working on atm, multiple have the chance to address this issue. We'll see which land or not.

  19. added 4 commits that reference this issue on Aug 17, 2026
    0b2d7d2
    0265071
    2e765d5
    13ab4a6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIhelp wanted[up-for-grabs] Good issue for external contributors

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions