Qual é mais rápido, ou?Nullable<T>.ValueNullable<T>.GetValueOrDefault()

Antes de responder a essa pergunta, minha resposta padrão para “qual cavalo é mais rápido?” se aplica. Leia isso primeiro.

.
.
.

Bem-vindo de volta. Mas, novamente, antes de responder à pergunta, preciso salientar que a diferença de desempenho potencial entre esses dois mecanismos para obter o valor não anulável de um tipo de valor anulável é uma consequência do fato de queesses dois mecanismos não são semanticamente equivalentes. O primeiro só pode ser legalmente chamado se você tiver certeza de que o valor anulável não é nulo; dito de outra forma, chamar sem saber que é verdade é umaexceção desossada. Este último pode ser chamado em qualquer valor anulável. Uma olhada em uma versão simplificada do código-fonte ilustra a diferença.Value HasValue

struct Nullable<T> where T : struct
{
  private bool hasValue;
  private T value;
  public Nullable(T value)
  {
    this.hasValue = true;
    this.value = value;
  }
  public bool HasValue { get { return this.hasValue; } }
  public T Value
  {
    get
    {
      if (!this.HasValue) throw something;
      return this.value;
    }
  }
  public T GetValueOrDefault() 
  {
    return this.value; 
  }
  ... and then all the other conversion gear and so on ...
}

A primeira coisa a notar é que a capacidade de um tipo de valor anulável de representar um inteiro “nulo” ou decimal ou o que quer que sejanão é mágica. (Os tipos de valor anulável são mágicos de outras maneiras; por exemplo, não há como escrever sua própria struct que tenha o estranho comportamento de boxe de um tipo de valor anulável; um int? caixas para um int ou null, nunca para um int?. Mas não vamos nos preocupar com essas características mágicas hoje.) Um tipo de valor anulável nada mais é do que uma instância do tipo de valor mais aditando se é nulo ou não.bool

Se uma variável do tipo de valor anulável for inicializada com o construtor padrão, o campo será seu valor padrão e o campo será. Se ele for inicializado com o construtor declarado, é claro que o campo é verdadeiro e o campo é qualquer valor legal, incluindo o valor padrão da possibilidade. Assim, a implementação do necessit não verificar o pavilhão; se o sinalizador for verdadeiro, o campo está definido corretamente e, se for falso, ele será definido como o valor padrão de.hasValuefalsevaluedefault(T)hasValuevalueTGetValueOrDefault()valueT

Olhando para o código, deve ficar claro que quase certamente não é mais rápido do que porque, obviamente, o primeiro faz exatamente o mesmo trabalho que o segundo no caso de sucesso, além do trabalho adicional da verificação da bandeira. Além disso, por ser tão simples de morte cerebral, é altamente provável que o jitter execute uma otimização de inline.ValueGetValueOrDefault()GetValueOrDefault()

Uma otimização inlining é onde o jitter elimina uma instrução desnecessária de “call” e “return” simplesmente gerando o código do corpo do método “inline” no chamador. Essa é uma ótima otimização porque isso pode tornar o código menor e mais rápido em alguns casos, embora dificulte a depuração porque o depurador não tem uma boa maneira de gerar pontos de interrupção dentro do método embutido.

Como o jitter escolhe inline ou não é um detalhe de implementação, mas é razoável supor que é menos provável que ele execute uma otimização inlining no código que contém mais de um “bloco básico” e tem um lançamento nele.

Um “bloco básico” é uma região de código onde você sabe que o código será executado do topo do bloco para a parte inferior sem quaisquer ramificações “normais” dentro ou fora do meio do bloco. (Um bloco básico pode, é claro, ter exceções descartadas dele.) Muitos compiladores de otimização usam “blocos básicos” como uma abstração porque abstrai os detalhes desnecessários do que o bloco realmente faz e o trata apenas como um nó em um gráfico de controle de fluxo.

Também deve ficar claro que, embora a diferença_relativa_de desempenho possa ser grande, a diferença_absoluta_é pequena. Uma chamada, busca de campo, salto condicional e retorno no caso típico faz a diferença, e essas coisas são apenas nanossegundos.

Agora, é claro que isso não quer dizer que você deva mudar todas as suas chamadas por razões de desempenho. Leia meu discurso novamente se você tiver vontade de fazer isso! Não vá alterando o código de trabalho, depurado e testado para obter um benefício de desempenho que é (1) altamente improvável que seja um gargalo real e (2) altamente improvável que seja seu pior problema de desempenho.ValueGetValueOrDefault()

E além disso, usinghas a propriedade agradável de que, se você cometeu um erro e buscou o valor de um nulo, você receberá uma exceção que informa onde seu bug está! Código que chama a atenção para suas falhas é uma coisa boa.Value

Por último, constato que temos aqui um daqueles raros casos em que as orientações de concepção dos quadros foram deliberadamente distorcidas. Temos um método “Get” é realmente mais rápido do que um getter de propriedade, e o getter de propriedade lança! Normalmente, você espera o oposto: o método “Get” geralmente é o que é lento e pode lançar, e a propriedade é a que é rápida e nunca lança. Embora isso seja um pouco infeliz, lembre-se, as diretrizes de design são nossos_servos_, não nossos_mestres_, e sãodiretrizes, não_regras_.


Da próxima vez no FAIC:Como o compilador C# usa seu conhecimento dos fatos discutidos hoje a seu favor? Tenha um ótimo Natal a todos; vamos retomar esse assunto em uma semana.


Autor: Eric Lippert

Artigo Original