1

I made a generic overload for Enum.HasFlag that prevents boxing:

    public static unsafe bool HasFlag<T>(this T enumVal, T flag) where T : unmanaged, Enum {
        return sizeof(T) switch {
            1 => (*(byte*)&enumVal & *(byte*)&flag) == *(byte*)&flag,
            2 => (*(ushort*)&enumVal & *(ushort*)&flag) == *(ushort*)&flag,
            4 => (*(uint*)&enumVal & *(uint*)&flag) == *(uint*)&flag,
            8 => (*(ulong*)&enumVal & *(ulong*)&flag) == *(ulong*)&flag,
            _ => throw new ArgumentException("Unsupported base enum Type")
        };
    }

Yet the compiler still wants to use the default Enum.HasFlag instead and I have to explicitly define the generic type to force it to use the extension.

The extension method should take priority here as the parameters have the correct type and do not need implicit type casting compared to the original one so why is the compiler still using the wrong one?

HellGate
  • 708
  • 1
  • 5
  • 20
  • 7
    Instance methods are always preferred to extension methods in the specification. This is deliberate, to stop the case where adding a single `using` statement to a file causes code within that file to switch from binding to instance methods, to binding to an extension method introduced by the `using` – canton7 Jan 19 '22 at 13:33
  • 6
    Also, note that `Enum.HasFlag` is now a JIT intrinsic (and has been since .NET Core 2.1). Although the IL says to box, the JIT knows better and will do a cheap bitwise comparison. There is no need for your extension method in modern .NET. Compare the Debug and Release assembly [here](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKGIGYACMhgYQYG8aHunHiUGAWQAUAZQxQAlgDsA5qIAOAG0kYA8goySI03AwgBKDlx6nJAMwbCIAOgASeAGJLsssRJnzlqjVp24bACUYfAgANxgAUXxNAE9I6Q8YXAMDE1NuTmoMnKZyAE5hACIAOQgIIoMAbh50jIBfOsbqeqA=) – canton7 Jan 19 '22 at 13:42

1 Answers1

5

For details on the overload resolution process, see §12.6.4 of the specification. Right at the bottom of the general description of the process in §12.7.6.1, you can see:

Otherwise, an attempt is made to process E.I as an extension method invocation (§12.7.8.3). If this fails, E.I is an invalid member reference, and a binding-time error occurs.

If we take a look at §12.7.8.3:

if the normal processing of the invocation finds no applicable methods, an attempt is made to process the construct as an extension method invocation

This is pretty clear that an attempt is made to bind an extension method only if the overload resolution process fails to find an applicable instance method.

This is a deliberate decision. If this were not the case, adding a single using statement to the top of a file could change how methods are bound further down in the file -- spooky action at a distance, which the spec generally tries to avoid.


However, since .NET Core 2.1, Enum.HasFlag has been a JIT intrinsic (it was the poster-child for which the JIT intrinsics mechanism was introduced). This means that although the IL may say to box and call the Enum.HasFlag method, in reality the JIT knows that it can replace this with a single bitwise test.

For example, the code:

public void Native(StringSplitOptions o) {
    if (o.HasFlag(StringSplitOptions.RemoveEmptyEntries))
    {
        Console.WriteLine("Noo");   
    }
}

Is jitted to this assembly in Release:

C.Native(System.StringSplitOptions)
    L0000: test dl, 1
    L0003: je short L0017
    L0005: mov rcx, 0x1ac4adebda0
    L000f: mov rcx, [rcx]
    L0012: jmp 0x00007ffb2f6ff7f8
    L0017: ret

No sign of any method calls there (apart from the final Console.WriteLine at the end)!

The same code using your extension method is significantly worse:

public void Worse(StringSplitOptions o) {
    if (o.HasFlag<StringSplitOptions>(StringSplitOptions.RemoveEmptyEntries))
    {
        Console.WriteLine("Noo");   
    }
}

Gives:

C.Worse(System.StringSplitOptions)
    L0000: sub rsp, 0x28
    L0004: mov [rsp+0x24], edx
    L0008: mov dword ptr [rsp+0x20], 1
    L0010: mov ecx, [rsp+0x24]
    L0014: and ecx, [rsp+0x20]
    L0018: cmp ecx, [rsp+0x20]
    L001c: sete cl
    L001f: movzx ecx, cl
    L0022: test ecx, ecx
    L0024: je short L0038
    L0026: mov rcx, 0x1ac4adebda0
    L0030: mov rcx, [rcx]
    L0033: call 0x00007ffb2f6ff7f8
    L0038: nop
    L0039: add rsp, 0x28
    L003d: ret

We can see that this has inlined the content of your HasFlag method, which is:

Extensions.HasFlag[[System.StringSplitOptions, System.Private.CoreLib]](System.StringSplitOptions, System.StringSplitOptions)
    L0000: mov [rsp+8], ecx
    L0004: mov [rsp+0x10], edx
    L0008: mov eax, [rsp+8]
    L000c: and eax, [rsp+0x10]
    L0010: cmp eax, [rsp+0x10]
    L0014: sete al
    L0017: movzx eax, al
    L001a: ret

Since your question is tagged [.net-6.0], the best advice is to throw away your extension method and use the built-in Enum.HasFlag, as it's significantly faster than what you've written.

See this all on SharpLab.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • Very interesting. It makes sense that extensions are only considered after everything else but in my case I actually wanted it to take priority. I have not checked in release mode so good that it is not an issue there. Now I just need to remember to ignore the analyzer warning for this method – HellGate Jan 19 '22 at 14:10
  • Does this mean that the answers of the following question are outdated? [What is it that makes Enum.HasFlag so slow?](https://stackoverflow.com/questions/7368652/what-is-it-that-makes-enum-hasflag-so-slow) – Theodor Zoulias Jan 19 '22 at 16:40
  • 1
    @TheodorZoulias [svick's answer](https://stackoverflow.com/a/7368679/1086121) has an up-to-date note at the bottom, but the rest looks out of date, yes – canton7 Jan 19 '22 at 17:42