Enum.Equals - Análise de desempenho

Imagem de capa Enum.Equals - Análise de desempenho

Enum.Equals - Análise de desempenho

Imagine que temos este mesmo enum:

public  enum Color 
{
    Red = 0,
    Green = 1,
}

Nada realmente extravagante, mas para nós é o suficiente. Temos várias maneiras de comparar se duas instâncias de um enum são iguais. Mas antes de mergulhar em alguma explicação, mostrarei os resultados antecipadamente com o código de referência:

public  class  Benchmark 
{
     private  readonly Color _colorRed = Color.Red; 
     private  readonly Color _colorGreen = Color.Green;
     
     [Benchmark(Baseline = true)] 
     public bool ObjectEquals() => Equals(_colorRed, _colorGreen);
     
     [Benchmark] 
     public bool EnumEquals() => Enum.Equals(_colorRed, _colorGreen);
     
     [Benchmark] 
     public bool InstanceEquals() => _colorRed.Equals(_colorGreen);
     
     [Benchmark] 
     public bool ComparisonOperator() => _colorRed == _colorGreen;
}

Temos 4 opções para comparar:

  • object.Equals
  • Enum.Equals
  • Chamar Equals de no método de instância
  • Use o operador de comparação ==

Agora traga os resultados:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1348 (21H1/May2021Update)
Intel Core i7-7820HQ CPU 2.90GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.101
  [Host]     : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
  DefaultJob : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
|              Método|      Quer dizer|      Erro |    StdDev |    Mediana|  Razão| RatioSD |
|------------------- |---------------:|----------:|----------:|----------:|------:|--------:|
|       ObjectEquals |      9.1618 ns | 0.2113 ns | 0.1765 ns | 9.1957 ns | 1.000 |    0.00 |
|         EnumEquals |      9.1438 ns | 0.1075 ns | 0.0839 ns | 9.1245 ns | 0.997 |    0.02 |
|     InstanceEquals |      9.9292 ns | 0.2370 ns | 0.5858 ns | 9.7626 ns | 1.086 |    0.10 |
| ComparisonOperator |      0.0445 ns | 0.0353 ns | 0.0571 ns | 0.0250 ns | 0.008 |    0.01 |

Que pato! Basicamente apenas o operador de comparação leva apenas 1/100 do tempo que as outras variações!

Agora vamos descobrir o porquê!

Boxing and unboxing

Agora as duas primeiras abordagens têm o mesmo tempo de execução (considerando a taxa de erro). Por que é que? Bem EnumEquals e ObjectEquals são os mesmos. Até nosso IDE nos dará uma dica aqui:

Podemos ver que o qualificador Enum é redundante. Mas por que isso. Bom vamos lá:

Quando passamos o mouse sobre Enum.Equals, bem como Equals, obtemos a dica de que isso é invocado do objeto. Então, o que object.Equals faz aqui? É bem simples:

public static bool Equals(object? objA, object? objB) 
{
     if (objA == objB) 
     {
         return  true;
     }
     if (objA == null || objB == null)
     {
         return  false;
     }
     return objA.Equals(objB);
}

Segure na primeira linha parece nossa última abordagem, que é a mais rápida! Então eu manipulei o teste porque meus enums não são os mesmos e, portanto, o tempo de execução é muito maior. Não. Mesmo se fizéssemos isso:

[Benchmark] 
public bool InstanceEqualsWhenTheSame() => _colorRed.Equals(_colorRed);

Teríamos isso:

|                     Método|     Quer dizer|      Erro |    StdDev |
|-------------------------- |--------------:|----------:|----------:|
| InstanceEqualsWhenTheSame |      9.821 ns | 0.1284 ns | 0.1138 ns |

O mesmo tempo de execução! Então vemos que todas as abordagens com Equals são iguais e apenas a última é de alguma forma diferente. Vamos dar uma olhada no código IL.

Esse Equals(_colorRed, _colorGreen); assim como este _colorRed.Equals(_colorGreen); irá traduzir aproximadamente para o mesmo código IL:

IL_0005: ldloc.0
IL_0006: box C/Color
IL_000b: ldloc.1
IL_000c: box C/Color
IL_0011: call bool [System.Private.CoreLib]System.Object::Equals(object, object)
IL_0016: pop

O exemplo mostrado acima é de Equals(_colorRed, _colorGreen);

Agora IL_0006: box C/Color e IL_000c: box C/Color são interessantes! Temos que encaixotar nossos enums!

Como fica nosso caso ==?

IL_002b: ldloc.0
IL_002c: ldloc.1
IL_002d: ceq
IL_002f: stloc.2

Sim você vê certo. Não há boxe. Apenas uma simples comparação de igualdade ceq. A propósito, se você quiser brincar sozinho: sharplab.io

"Boxing é o processo de conversão de um tipo de valor para o objeto de tipo ou para qualquer tipo de interface implementado por esse tipo de valor. Quando o Common Language Runtime (CLR) enquadra um tipo de valor, ele encapsula o valor dentro de uma instância de System.Object e o armazena no heap gerenciado. O unboxing extrai o tipo de valor do objeto. O boxe é implícito; unboxing é explícito."

Da documentação oficial da Microsoft

Boxing um tipo de valor em um tipo de referência custa um pouco de tempo. E é exatamente por isso que é tão mais caro. Se você quiser saber mais sobre boxing/ unboxing, sugiro que leia o link acima.

O repositório pode ser encontrado: aqui