Surpresa de desempenho com os tipos "as" e nulos

330

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 isinstpara 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);
    }
}
Jon Skeet
fonte
8
Por que não olhar para o código jitted? Até o depurador VS pode mostrá-lo.
Anton Tykhyy 17/10/09
2
Só estou curioso, você testou com o CLR 4.0 também?
Dirk Vollmar
1
@ Anton: Bom ponto. Fará em algum momento (embora isso não esteja no VS no momento :) @divo: Sim, e é pior o tempo todo. Mas isso está na versão beta, então pode haver muito código de depuração lá.
21710 Jon Skeet
1
Hoje eu aprendi que você pode usar asem tipos anuláveis. Interessante, pois não pode ser usado em outros tipos de valor. Na verdade, mais surpreendente.
leppie
3
@Lepp, faz todo o sentido não funcionar em tipos de valor. Pense bem, astente converter para um tipo e, se falhar, retornará nulo. Não é possível definir os tipos de valor para nulo
Earlz

Respostas:

209

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.

Hans Passant
fonte
16
Ok, estou convencido. Eu acho que estou acostumado a pensar em "é" como potencialmente caro, devido às possibilidades de subir em uma hierarquia de herança - mas no caso de um tipo de valor, não há possibilidade de uma hierarquia, portanto pode ser uma comparação bit a bit simples . Ainda acho que o código JIT para o caso anulável pode ser otimizado pelo JIT com muito mais força do que é.
precisa
26

Parece-me que isinsté realmente muito lento em tipos anuláveis. No método FindSumWithCasteu mudei

if (o is int)

para

if (o is int?)

o que também diminui significativamente a execução. A única diferença em IL que posso ver é que

isinst     [mscorlib]System.Int32

é alterado para

isinst     valuetype [mscorlib]System.Nullable`1<int32>
Dirk Vollmar
fonte
1
É mais do que isso; no caso "cast", isinsté seguido por um teste de nulidade e, em seguida, condicionalmente um unbox.any. No caso anulável, há um incondicional unbox.any .
21710 Jon Skeet
Sim, resulta em ambos isinst e unbox.anyé mais lento em tipos anuláveis.
Dirk Vollmar
@ Jon: Você pode revisar minha resposta sobre por que o elenco é necessário. (Eu sei que isso é antigo, mas eu acabei de descobrir isso q e pensei em fornecer meu 2c do que sei sobre o CLR).
Johannes Rudolph
22

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 asoperador C # emitirá uma isinstinstrução IL (o isoperador também). (Outra instrução interessante é castclassemitida 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 isinstfaz ( ECMA 335, Partição III, 4.6 ):

Formato: isinst typeTok

typeTok é um símbolo de metadados (um typeref, typedefou typespec), indicando a classe desejada.

Se typeTok for um tipo de valor não anulável ou um tipo de parâmetro genérico, ele será interpretado como typeTok "em caixa" .

Se typeTok for um tipo anulável Nullable<T>, será interpretado como "in a box"T

Mais importante:

Se o tipo real (não o tipo rastreado do verificador) de obj for atribuível pelo verificador ao tipo typeTok, será isinstbem - sucedido e obj (como resultado ) será retornado inalterado enquanto a verificação rastreia seu tipo como typeTok . Diferentemente das coerções (§1.6) e conversões (§3.27), isinstnunca altera o tipo real de um objeto e preserva a identidade do objeto (consulte a Partição I).

Portanto, o assassino de desempenho não é isinst, neste caso, mas o adicional unbox.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á um unbox.anydepois de um isinst T?(mas o omitirá caso você o faça isinst T, quando Tfor um tipo de referência).

Por que ele faz isso? isinst T?nunca teve o efeito que seria óbvio, ou seja, você volta a T?. Em vez disso, todas essas instruções garantem que você tenha um "boxed T"que possa ser retirado da caixa de correio T?. Para se ter uma real T?, ainda precisamos unbox nosso "boxed T"para T?, razão pela qual o compilador emite um unbox.anydepois isinst. Se você pensar bem, isso faz sentido, porque o "formato da caixa" para T?é apenas um "boxed T"e fazer castclasse isinstexecutar 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

Quando aplicada à forma em caixa de um tipo de valor, a unbox.anyinstrução extrai o valor contido em obj (do tipo O). (É equivalente a unboxseguido por ldobj.) Quando aplicada a um tipo de referência, a unbox.anyinstrução tem o mesmo efeito que castclasstypeTok.

(ECMA 335, partição III, 4.32): unbox

Normalmente, unboxsimplesmente calcula o endereço do tipo de valor que já está presente dentro do objeto em caixa. Essa abordagem não é possível ao desmarcar os tipos de valor anulável. Como os Nullable<T>valores são convertidos em caixa Tsdurante a operação de caixa, uma implementação geralmente deve fabricar uma nova Nullable<T>na pilha e calcular o endereço no objeto recém-alocado.

Johannes Rudolph
fonte
Acho que a última frase citada pode ter um erro de digitação; não deveria "... na pilha ..." estar "na pilha de execução ?" Parece que o unboxing de volta para uma nova instância de heap do GC troca o problema original por outro quase idêntico.
Glenn Slayden 02/03/19
19

Curiosamente, repassei o feedback sobre o suporte do operador, dynamicsendo uma ordem de magnitude mais lenta para Nullable<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) nullpara estruturas não anuláveis, ele o impede de Nullable<T>:

using System;
using System.Diagnostics;
static class Program {
    static void Main() { 
        // JIT
        TestUnrestricted<int>(1,5);
        TestUnrestricted<string>("abc",5);
        TestUnrestricted<int?>(1,5);
        TestNullable<int>(1, 5);

        const int LOOP = 100000000;
        Console.WriteLine(TestUnrestricted<int>(1, LOOP));
        Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
        Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
        Console.WriteLine(TestNullable<int>(1, LOOP));

    }
    static long TestUnrestricted<T>(T x, int loop) {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
    static long TestNullable<T>(T? x, int loop) where T : struct {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}
Marc Gravell
fonte
Yowser. Essa é uma diferença realmente dolorosa. Eek.
21716 Jon Skeet
Se nenhuma outra boa saiu de tudo isso, ele me levou a incluir advertências, tanto para o meu código original e este :)
Jon Skeet
Sei que essa é uma pergunta antiga, mas você poderia explicar o que você quer dizer com "os pontos JIT (e remove) nullpara estruturas não anuláveis"? Você quer dizer que ele substitui nullpor um valor padrão ou algo durante o tempo de execução?
23811 Justin Morgan
2
@ Justin - um método genérico pode ser usado em tempo de execução com qualquer número de permutações de parâmetros genéricos ( Tetc). 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.
Marc Gravell
O teste nulo irrestrito ainda é 2,5 ordens de magnitude mais lento, mas há alguma otimização em andamento quando você não usa a countvariável. A adição Console.Write(count.ToString()+" ");após o teste watch.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 quando nullé aprovada, confirmando que o código original não está realmente realizando a verificação nula e o incremento para os outros testes. LINQPad
Mark Hurd
12

Este é o resultado de FindSumWithAsAndHas acima: texto alternativo

Este é o resultado de FindSumWithCast: texto alternativo

Constatações:

  • Usando as, ele testa primeiro se um objeto é uma instância do Int32; sob o capô que está usando isinst Int32(que é semelhante ao código escrito à mão: if (o é int)). E as, usando , ele também unboxing incondicionalmente o objeto. E é um verdadeiro matador de desempenho chamar uma propriedade (ainda é uma função oculta), IL_0027

  • Usando elenco, você testa primeiro se o objeto é um int if (o is int); sob o capô que está usando isinst Int32. Se for uma instância de int, você poderá desmarcar com segurança o valor IL_002D

Simplificando, este é o pseudo-código do uso da asabordagem:

int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    

E este é o pseudo-código do uso da abordagem de conversão:

if (o isinst Int32)
    sum += (o unbox Int32)

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 um int. Não se pode dizer a mesma coisa usando uma asabordagem.

Michael Buen
fonte
11

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 ...

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}

torna-se simplesmente ...

if (o is int x)
{
    // ...use x in here
}

Eu descobri que um uso comum para a nova sintaxe é quando você está escrevendo um tipo de valor .NET (ou seja, structem C # ) que implementa IEquatable<MyStruct>(como a maioria deveria). Depois de implementar o Equals(MyStruct other)método fortemente tipado , agora você pode redirecionar normalmente a Equals(Object obj)substituição não tipada (herdada de Object) para ele da seguinte maneira:

public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);

 


Apêndice: O código IL deRelease 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

// static void test1(Object o, ref int y)
// {
//     int? x = o as int?;
//     if (x.HasValue)
//         y = x.Value;
// }

[0] valuetype [mscorlib]Nullable`1<int32> x
        ldarg.0
        isinst [mscorlib]Nullable`1<int32>
        unbox.any [mscorlib]Nullable`1<int32>
        stloc.0
        ldloca.s x
        call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
        brfalse.s L_001e
        ldarg.1
        ldloca.s x
        call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
        stind.i4
L_001e: ret

// static void test2(Object o, ref int y)
// {
//     if (o is int x)
//         y = x;
// }

[0] int32 x,
[1] object obj2
        ldarg.0
        stloc.1
        ldloc.1
        isinst int32
        ldnull
        cgt.un
        dup
        brtrue.s L_0011
        ldc.i4.0
        br.s L_0017
L_0011: ldloc.1
        unbox.any int32
L_0017: stloc.0
        brfalse.s L_001d
        ldarg.1
        ldloc.0
        stind.i4
L_001d: ret

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').

Glenn Slayden
fonte
9

Criação de perfil adicional:

using System;
using System.Diagnostics;

class Program
{
    const int Size = 30000000;

    static void Main(string[] args)
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "";
            values[i + 2] = 1;
        }

        FindSumWithIsThenCast(values);

        FindSumWithAsThenHasThenValue(values);
        FindSumWithAsThenHasThenCast(values);

        FindSumWithManualAs(values);
        FindSumWithAsThenManualHasThenValue(values);



        Console.ReadLine();
    }

    static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += (int)o;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithManualAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            bool hasValue = o is int;
            int x = hasValue ? (int)o : 0;

            if (hasValue)
            {
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Manual As: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenManualHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

}

Resultado:

Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282

O que podemos deduzir dessas figuras?

  • Primeiro, a abordagem é então lançada é significativamente mais rápida do que a abordagem. 303 vs 3524
  • Segundo, o valor é marginalmente mais lento que o vazamento. 3524 vs 3272
  • Terceiro, o .HasValue é um pouco mais lento do que o uso manual (ou seja, uso is ). 3524 vs 3282
  • Quarto, fazendo uma comparação de maçã para maçã (ou seja, a atribuição de HasValue simulado e a conversão de Valor simulado acontecem juntos) entre simulado como e real como abordagem, podemos ver simulado como ainda significativamente mais rápido que real . 395 vs 3524
  • Por fim, com base em primeira e quarta conclusão, há algo de errado com a implementação ^ _ ^
Michael Buen
fonte
8

Não tenho tempo para experimentá-lo, mas você pode querer ter:

foreach (object o in values)
        {
            int? x = o as int?;

Como

int? x;
foreach (object o in values)
        {
            x = o as int?;

Você está criando um novo objeto a cada vez, o que não explica completamente o problema, mas pode contribuir.

James Black
fonte
1
Não, eu corri isso e é um pouco mais lento.
Henk Holterman
2
Declarar uma variável em um local diferente afeta apenas significativamente o código gerado quando a variável é capturada (nesse ponto, afeta a semântica real) na minha experiência. Observe que ele não está criando um novo objeto no heap, embora certamente esteja criando uma nova instância de int?usando a pilha unbox.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.
9139 Jon Skeet
Eu estava pensando que o elenco provavelmente está otimizado, já que existe há tanto tempo.
James Black
1
is / cast é um alvo fácil para otimização, é um idioma tão irritantemente comum.
Anton Tykhyy
4
As variáveis ​​locais são alocadas na pilha quando o quadro da pilha para o método é criado, portanto, onde você declara a variável no método, não faz diferença. (A menos que esteja encerrado, é claro, mas não é o caso aqui.)
Guffa
8

Eu tentei o tipo exato de verificação de construção

typeof(int) == item.GetType(), que executa tão rápido quanto a item is intversão e sempre retorna o número (ênfase: mesmo que você tenha escrito Nullable<int>a no array, você precisará usá-lo typeof(int)). Você também precisa de uma null != itemverificação adicional aqui.

Contudo

typeof(int?) == item.GetType() permanece rápido (em contraste com item 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/asnecessá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?)nullna matriz, a verificação de tipo retornará false.

dalo
fonte
Interessante ... a idéia de usar o "as" + HasValue (not is mais HasValue, note) é que ele está executando a verificação de tipo apenas uma vez em vez de duas vezes. Ele está fazendo o "check and unbox" em uma única etapa. Parece que deveria ser mais rápido ... mas claramente não é. Não sei ao certo o que você quer dizer com a última frase, mas não existe uma caixa int?- se você colocar uma caixaint? colocar valor em ele acaba como uma caixa int ou uma nullreferência.
precisa
7
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);
        FindSumWithAsAndHas(values);
        FindSumWithAsAndIs(values);


        FindSumWithIsThenAs(values);
        FindSumWithIsThenConvert(values);

        FindSumWithLinq(values);



        Console.ReadLine();
    }

    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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }


    static void FindSumWithAsAndIs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As and Is: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }







    static void FindSumWithIsThenAs(object[] values)
    {
        // Apple-to-apple comparison with Cast routine above.
        // Using the similar steps in Cast routine above,
        // the AS here cannot be slower than Linq.



        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {

            if (o is int)
            {
                int? x = o as int?;
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then As: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithIsThenConvert(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {            
            if (o is int)
            {
                int x = Convert.ToInt32(o);
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Convert: {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);
    }
}

Saídas:

Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811

[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

Inside VS, Configuration: Debug

Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018




Outside VS, Configuration: Debug

Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944




Inside VS, Configuration: Release

Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932




Outside VS, Configuration: Release

Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936
Michael Buen
fonte
Qual versão do framework você está usando, sem interesse? Os resultados no meu netbook (usando o .NET 4RC) são ainda mais dramáticos - as versões usando o As são muito piores que os resultados. Talvez eles tenham melhorado para o .NET 4 RTM? Eu ainda acho que poderia ser mais rápido ...
Jon Skeet
@ Michael: Você estava executando uma compilação não otimizada ou no depurador?
precisa
@ Jon: construção unoptimized, sob depurador
Michael Buen
1
@ Michael: Direito - que tendem a ver os resultados de desempenho sob um depurador como em grande parte irrelevante :)
Jon Skeet
@ Jon: Se por Debugger, ou seja, dentro do VS; Sim, o benchmark anterior foi feito no depurador. Eu comparo novamente, dentro do VS e fora dele, e compilado como depuração e compilado como versão. Veja a edição
Michael Buen