Estou apenas revisando o capítulo 4 do C # no Depth, que trata de tipos anuláveis, e estou adicionando uma seção sobre o uso do operador "as", que permite escrever:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Eu pensei que isso era realmente legal e que poderia melhorar o desempenho em relação ao equivalente em C # 1, usando "is" seguido de uma conversão - afinal, dessa forma, só precisamos solicitar uma verificação dinâmica de tipo uma vez e depois uma simples verificação de valor .
No entanto, parece não ser esse o caso. Incluí um exemplo de aplicativo de teste abaixo, que basicamente soma todos os números inteiros em uma matriz de objetos - mas a matriz contém muitas referências nulas e referências de string, bem como números inteiros em caixa. A referência mede o código que você teria que usar no C # 1, o código usando o operador "as" e apenas para dar um pontapé na solução LINQ. Para minha surpresa, o código C # 1 é 20 vezes mais rápido nesse caso - e até o código LINQ (que eu esperava ser mais lento, considerando os iteradores envolvidos) supera o código "como".
A implementação do .NET isinst
para tipos anuláveis é realmente muito lenta? É o adicionalunbox.any
que causa o problema? Existe outra explicação para isso? No momento, parece que vou ter que incluir um aviso contra o uso em situações sensíveis ao desempenho ...
Resultados:
Elenco: 10000000: 121
Como: 10000000: 2211
LINQ: 10000000: 2143
Código:
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i+1] = "";
values[i+2] = 1;
}
FindSumWithCast(values);
FindSumWithAs(values);
FindSumWithLinq(values);
}
static void FindSumWithCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int) o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
}
as
em tipos anuláveis. Interessante, pois não pode ser usado em outros tipos de valor. Na verdade, mais surpreendente.as
tente converter para um tipo e, se falhar, retornará nulo. Não é possível definir os tipos de valor para nuloRespostas:
Claramente, o código da máquina que o compilador JIT pode gerar para o primeiro caso é muito mais eficiente. Uma regra que realmente ajuda é que um objeto só pode ser retirado da caixa de seleção para uma variável que possui o mesmo tipo que o valor da caixa. Isso permite que o compilador JIT gere um código muito eficiente; nenhuma conversão de valor precisa ser considerada.
O é teste operador é fácil, basta verificar se o objeto não é nulo e é do tipo esperado, leva apenas algumas instruções de código de máquina. A conversão também é fácil, o compilador JIT conhece a localização dos bits de valor no objeto e os usa diretamente. Nenhuma cópia ou conversão ocorre, todo o código da máquina está embutido e leva apenas cerca de uma dúzia de instruções. Isso precisava ser realmente eficiente no .NET 1.0 quando o boxe era comum.
Transmitindo para int? requer muito mais trabalho. A representação do valor do número inteiro em caixa não é compatível com o layout de memória de
Nullable<int>
. É necessária uma conversão e o código é complicado devido a possíveis tipos de enum em caixa. O compilador JIT gera uma chamada para uma função auxiliar do CLR denominada JIT_Unbox_Nullable para concluir o trabalho. Esta é uma função de uso geral para qualquer tipo de valor, muito código para verificar tipos. E o valor é copiado. Difícil estimar o custo, pois esse código está bloqueado no mscorwks.dll, mas é provável que centenas de instruções de código de máquina.O método de extensão Linq OfType () também usa o operador is e o cast. No entanto, isso é uma conversão para um tipo genérico. O compilador JIT gera uma chamada para uma função auxiliar, JIT_Unbox (), que pode executar uma conversão para um tipo de valor arbitrário. Eu não tenho uma grande explicação por que é tão lento quanto o elenco
Nullable<int>
, já que menos trabalho deve ser necessário. Suspeito que o ngen.exe possa causar problemas aqui.fonte
Parece-me que
isinst
é realmente muito lento em tipos anuláveis. No métodoFindSumWithCast
eu mudeipara
o que também diminui significativamente a execução. A única diferença em IL que posso ver é que
é alterado para
fonte
isinst
é seguido por um teste de nulidade e, em seguida, condicionalmente umunbox.any
. No caso anulável, há um incondicionalunbox.any
.isinst
eunbox.any
é mais lento em tipos anuláveis.Isso originalmente começou como um comentário à excelente resposta de Hans Passant, mas demorou muito, então eu quero adicionar alguns bits aqui:
Primeiro, o
as
operador C # emitirá umaisinst
instrução IL (ois
operador também). (Outra instrução interessante écastclass
emitida quando você faz uma conversão direta e o compilador sabe que a verificação de tempo de execução não pode ser omitida.)Aqui está o que
isinst
faz ( ECMA 335, Partição III, 4.6 ):Mais importante:
Portanto, o assassino de desempenho não é
isinst
, neste caso, mas o adicionalunbox.any
. Isso não ficou claro na resposta de Hans, pois ele olhou apenas para o código JIT. Em geral, o compilador C # emitirá umunbox.any
depois de umisinst T?
(mas o omitirá caso você o façaisinst T
, quandoT
for um tipo de referência).Por que ele faz isso?
isinst T?
nunca teve o efeito que seria óbvio, ou seja, você volta aT?
. Em vez disso, todas essas instruções garantem que você tenha um"boxed T"
que possa ser retirado da caixa de correioT?
. Para se ter uma realT?
, ainda precisamos unbox nosso"boxed T"
paraT?
, razão pela qual o compilador emite umunbox.any
depoisisinst
. Se você pensar bem, isso faz sentido, porque o "formato da caixa" paraT?
é apenas um"boxed T"
e fazercastclass
eisinst
executar a unbox seria inconsistente.Fazendo backup da descoberta de Hans com algumas informações do padrão , aqui está:
(ECMA 335, partição III, 4.33):
unbox.any
(ECMA 335, partição III, 4.32):
unbox
fonte
Curiosamente, repassei o feedback sobre o suporte do operador,
dynamic
sendo uma ordem de magnitude mais lenta paraNullable<T>
(semelhante a este teste inicial ) - suspeito por motivos muito semelhantes.Tenho que amar
Nullable<T>
. Outra coisa divertida é que, embora o JIT identifique (e remova)null
para estruturas não anuláveis, ele o impede deNullable<T>
:fonte
null
para estruturas não anuláveis"? Você quer dizer que ele substituinull
por um valor padrão ou algo durante o tempo de execução?T
etc). Os requisitos da pilha etc dependem dos argumentos (quantidade de espaço da pilha para um local etc.), portanto, você obtém um JIT para qualquer permutação exclusiva que envolva um tipo de valor. No entanto, as referências são do mesmo tamanho, portanto compartilhe um JIT. Ao executar o JIT por tipo de valor, ele pode verificar alguns cenários óbvios e tenta extrair código inacessível devido a coisas como nulos impossíveis. Não é perfeito, note. Além disso, eu estou ignorando AOT para o acima.count
variável. A adiçãoConsole.Write(count.ToString()+" ");
após o testewatch.Stop();
nos dois casos diminui a velocidade dos outros testes em uma ordem de magnitude, mas o teste nulo irrestrito não é alterado. Observe que também há alterações quando você testa os casos quandonull
é aprovada, confirmando que o código original não está realmente realizando a verificação nula e o incremento para os outros testes. LINQPadEste é o resultado de FindSumWithAsAndHas acima:
Este é o resultado de FindSumWithCast:
Constatações:
Usando
as
, ele testa primeiro se um objeto é uma instância do Int32; sob o capô que está usandoisinst Int32
(que é semelhante ao código escrito à mão: if (o é int)). Eas
, usando , ele também unboxing incondicionalmente o objeto. E é um verdadeiro matador de desempenho chamar uma propriedade (ainda é uma função oculta), IL_0027Usando elenco, você testa primeiro se o objeto é um
int
if (o is int)
; sob o capô que está usandoisinst Int32
. Se for uma instância de int, você poderá desmarcar com segurança o valor IL_002DSimplificando, este é o pseudo-código do uso da
as
abordagem:E este é o pseudo-código do uso da abordagem de conversão:
Portanto, a
(int)a[i]
abordagem de elenco ( bem, a sintaxe se parece com um elenco, mas na verdade é unboxing, elenco e unboxing compartilham a mesma sintaxe, da próxima vez que ser pedante com a terminologia correta) é muito mais rápida, você só precisa desempacotar um valor quando um objeto é decididamente umint
. Não se pode dizer a mesma coisa usando umaas
abordagem.fonte
Para manter essa resposta atualizada, vale a pena mencionar que a maior parte da discussão nesta página agora é discutida agora com C # 7.1 e .NET 4.7 que suporta uma sintaxe fina que também produz o melhor código IL.
O exemplo original do OP ...
torna-se simplesmente ...
Eu descobri que um uso comum para a nova sintaxe é quando você está escrevendo um tipo de valor .NET (ou seja,
struct
em C # ) que implementaIEquatable<MyStruct>
(como a maioria deveria). Depois de implementar oEquals(MyStruct other)
método fortemente tipado , agora você pode redirecionar normalmente aEquals(Object obj)
substituição não tipada (herdada deObject
) para ele da seguinte maneira:Apêndice: O código IL de
Release
construção para as duas primeiras funções de exemplo mostradas acima nesta resposta (respectivamente) são fornecidos aqui. Embora o código IL para a nova sintaxe seja de fato 1 byte menor, ele geralmente ganha muito ao fazer zero chamadas (vs. dois) e evitar a operação quando possível.unbox
Para testes adicionais que substanciam minha observação sobre o desempenho da nova sintaxe C # 7 que ultrapassa as opções disponíveis anteriormente, consulte aqui (em particular, exemplo 'D').
fonte
Criação de perfil adicional:
Resultado:
O que podemos deduzir dessas figuras?
fonte
Não tenho tempo para experimentá-lo, mas você pode querer ter:
Como
Você está criando um novo objeto a cada vez, o que não explica completamente o problema, mas pode contribuir.
fonte
int?
usando a pilhaunbox.any
. Suspeito que esse seja o problema - meu palpite é que o IL artesanal pode superar as duas opções aqui ... embora também seja possível que o JIT seja otimizado para reconhecer o caso is / cast e verificar apenas uma vez.Eu tentei o tipo exato de verificação de construção
typeof(int) == item.GetType()
, que executa tão rápido quanto aitem is int
versão e sempre retorna o número (ênfase: mesmo que você tenha escritoNullable<int>
a no array, você precisará usá-lotypeof(int)
). Você também precisa de umanull != item
verificação adicional aqui.Contudo
typeof(int?) == item.GetType()
permanece rápido (em contraste comitem is int?
), mas sempre retorna falso.O typeof-construct é, aos meus olhos, o caminho mais rápido para a verificação exata do tipo, pois usa o RuntimeTypeHandle. Como os tipos exatos nesse caso não coincidem com anuláveis, meu palpite é que é
is/as
necessário realizar um levantamento adicional aqui para garantir que seja de fato uma instância de um tipo anulável .E honestamente: o que faz o seu
is Nullable<xxx> plus HasValue
você compra? Nada. Você sempre pode ir diretamente para o tipo subjacente (valor) (neste caso). Você obtém o valor ou "não, não uma instância do tipo que estava solicitando". Mesmo se você escreveu(int?)null
na matriz, a verificação de tipo retornará false.fonte
int?
- se você colocar uma caixaint?
colocar valor em ele acaba como uma caixa int ou umanull
referência.Saídas:
[EDIT: 19/06/2010]
Nota: O teste anterior foi realizado no VS, depuração da configuração, usando o VS2009, usando o Core i7 (máquina de desenvolvimento da empresa).
O seguinte foi feito na minha máquina usando o Core 2 Duo, usando o VS2010
fonte