HasFlag против битовых операций

Закрою для себя раз и навсегда споры о том, что быстрее — метод HasFlag у перечислимого типа помеченного атрибутом [Flags] или ручные битовые операции. Такие вопросы периодически возникают и споры могут быть жаркими.

Немного истории вопроса — ранее, в первых версиях .NET Framework, метод HasFlag был существенно медленнее чем ручные битовые операции, просто по причине того, что Enum это класс (хоть и производный от ValueType, но тем не менее), при этом экземпляр конкретного перечислимого типа, является структурой. Метод HasFlag определен именно в базовом классе (а не в наследуемой от него структуре). Далее, чтобы можно было вызвать метод класса Enum, делается упаковка (boxing) конкретного экземпляра перечислимого типа, и делается вызов нужного метода, в нашем случае это HasFlag. Кому интересно, можно почитать дискуссию на эту тему тут https://stackoverflow.com/questions/7368652/what-is-it-that-makes-enum-hasflag-so-slow

Справедливости ради, и сейчас, в IL коде происходит то же самое (boxing), но не спешим расстраиваться, — ситуация, как это обычно бывает, намного интереснее. Для начала напишем простенький бенчмарк. Например такой:

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;

namespace vperf;

[Flags]
public enum Options : byte
{
    Option1 = 1,
    Option2 = 2,
    Option3 = 4,
}

[DisassemblyDiagnoser(printInstructionAddresses: true, syntax: DisassemblySyntax.Intel)]
public class BenchmarkHasFlag
{
    private readonly Options opt = Options.Option2;

    [Benchmark(Baseline = true)]
    public bool Plain() => (opt & Options.Option3) == Options.Option3;

    [Benchmark]
    public bool HasFlag() => opt.HasFlag(Options.Option3);
}

Перед запуском посмотрим в какой IL код компилируются оба метода. Сначала Plain:

  .method public hidebysig instance bool
    Plain() cil managed
  {
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string)
      = (
        01 00 14 00 00 00 2e 2f 68 6f 6d 65 2f 65 67 72 // ......./home/egr
        2f 63 6f 64 65 2f 76 70 65 72 66 2f 76 70 65 72 // /code/vperf/vper
        66 2f 42 65 6e 63 68 6d 61 72 6b 48 61 73 46 6c // f/BenchmarkHasFl
        61 67 2e 63 73 01 00 54 02 08 42 61 73 65 6c 69 // ag.cs..T..Baseli
        6e 65 01                                        // ne.
      )
      // int32(20) // 0x00000014
      // string('/home/egr/code/vperf/vperf/BenchmarkHasFlag.cs')
      // property bool 'Baseline' = bool(true)
    .maxstack 8

    // [21 28 - 21 70]
    IL_0000: ldarg.0      // this
    IL_0001: ldfld        valuetype vperf.Options vperf.BenchmarkHasFlag::'opt'
    IL_0006: ldc.i4.4
    IL_0007: and
    IL_0008: ldc.i4.4
    IL_0009: ceq
    IL_000b: ret

  } // end of method BenchmarkHasFlag::Plain

Далее HasFlag:

  .method public hidebysig instance bool
    HasFlag() cil managed
  {
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string)
      = (
        01 00 17 00 00 00 2e 2f 68 6f 6d 65 2f 65 67 72 // ......./home/egr
        2f 63 6f 64 65 2f 76 70 65 72 66 2f 76 70 65 72 // /code/vperf/vper
        66 2f 42 65 6e 63 68 6d 61 72 6b 48 61 73 46 6c // f/BenchmarkHasFl
        61 67 2e 63 73 00 00                            // ag.cs..
      )
      // int32(23) // 0x00000017
      // string('/home/egr/code/vperf/vperf/BenchmarkHasFlag.cs')
    .maxstack 8

    // [24 30 - 24 58]
    IL_0000: ldarg.0      // this
    IL_0001: ldfld        valuetype vperf.Options vperf.BenchmarkHasFlag::'opt'
    IL_0006: box          vperf.Options
    IL_000b: ldc.i4.4
    IL_000c: box          vperf.Options
    IL_0011: call         instance bool [System.Runtime]System.Enum::HasFlag(class [System.Runtime]System.Enum)
    IL_0016: ret

  } // end of method BenchmarkHasFlag::HasFlag

С виду у HasFlag все плохо. Если в первом методе мы загружаем поле класса в стек, потом значение нужного флага, делаем побитовую операцию and и сравниваем результат, то во втором мы также загружаем поле класса, делаем его упаковку (box), чтобы иметь возможность вызвать метод HasFlag определенный в базовом классе Enum (помним что Enum это класс, а Options это структура) и далее вызываем собственно нужный метод. Ужас — при боксинге по определению мы выделяем память (где — отдельный вопрос), что ужасно плохо для такой элементарной вещи.

запускаем бенчмарк.

BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Manjaro Linux
Intel Core i9-9900K CPU 3.60GHz (Coffee Lake), 1 CPU, 16 logical and 8 physical cores
.NET SDK 7.0.403
  [Host]     : .NET 6.0.24 (6.0.2423.51814), X64 RyuJIT AVX2
  DefaultJob : .NET 6.0.24 (6.0.2423.51814), X64 RyuJIT AVX2
MethodMeanErrorStdDevMedianRatioRatioSDCode Size
Plain0.0000 ns0.0000 ns0.0000 ns0.0 ns??11 B
HasFlag0.0002 ns0.0006 ns0.0005 ns0.0 ns??13 B

Хмм … Что мы видим? Результат одинаковый! Т.е. кто-то, где-то мухлюет! А этот кто-то собственно наш JIT компилятор. У BenchmarkDotNet есть очень классная опция записывать реальный ассемблерный код, который выполняется на процессоре, т.е. результат работы JIT компилятора. Давайте его посмотрим:

.NET 6.0.24 (6.0.2423.51814), X64 RyuJIT AVX2

; vperf.BenchmarkHasFlag.Plain()
       7F8B31DA3B10 test      byte ptr [rdi+8],4
       7F8B31DA3B14 setne     al
       7F8B31DA3B17 movzx     eax,al
       7F8B31DA3B1A ret
; Total bytes of code 11

.NET 6.0.24 (6.0.2423.51814), X64 RyuJIT AVX2

; vperf.BenchmarkHasFlag.HasFlag()
       7FB1BC573CD0 movzx     eax,byte ptr [rdi+8]
       7FB1BC573CD4 test      al,4
       7FB1BC573CD6 setne     al
       7FB1BC573CD9 movzx     eax,al
       7FB1BC573CDC ret
; Total bytes of code 13

а мы видим что код одинаковый! Т.е. по большому счету, нам без разницы что использовать и мы можем выбирать что проще в чтении и понимании, т.е. в нашем случае это использование метода HasFlag. Мораль сей истории — не спешите пользоваться низкоуровневыми (и плохо читаемыми) вещами, попробуйте использовать библиотечные вещи и доверьтесь JIT компилятору, он в .NET весьма продвинутый.

2023-12-05 07:48:20 UTC csharp microsoft noweb performance programming