Pergunta

Estou apenas revisando o capítulo 4 de C# em profundidade, que lida com 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 C# 1, usando "is" seguido por um elenco - afinal, dessa maneira só precisamos pedir verificação de tipo dinâmico uma vez e, em seguida, uma simples verificação de valor .

Parece não ser o caso, no entanto. Incluí um aplicativo de teste de amostra abaixo, que basicamente resume todos os números inteiros dentro de uma matriz de objetos - mas a matriz contém muitas referências nulas e referências de string, além de números inteiros em caixa. O benchmark mede o código que você teria que usar no C# 1, o código usando o operador "AS" e apenas para chutes uma solução LINQ. Para minha surpresa, o código C# 1 é 20 vezes mais rápido neste caso - e até o código LINQ (que eu esperava ser mais lento, dado que os iteradores envolvidos) vencem o código "AS".

É a implementação .NET de isinst Para tipos anuláveis, muito lento? É o adicional unbox.any Isso causa o problema? Existe outra explicação para isso? No momento, parece que vou ter que incluir um aviso contra usar isso em situações sensíveis ao desempenho ...

Resultados:

Elenco: 10000000: 121
AS: 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);
    }
}
Foi útil?

Solução

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 a haver é que um objeto só pode ser não caixa para uma variável que tenha o mesmo tipo que o valor da caixa. Isso permite que o compilador JIT gere código muito eficiente, nenhuma conversão de valor deve ser considerada.

o é O teste do operador é fácil, basta verificar se o objeto não é nulo e é do tipo esperado, leva apenas algumas instruções de código da máquina. O elenco 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 de volta no .NET 1.0 quando o boxe era comum.

Lançando para int? leva muito mais trabalho. A representação de valor do número inteiro em caixa não é compatível com o layout de memória de Nullable<int>. Uma conversão é necessária e o código é complicado devido aos possíveis tipos de enumeração em caixa. O compilador JIT gera uma chamada para uma função CLR Helper chamada jit_unbox_nullable para fazer o trabalho. Esta é uma função de uso geral para qualquer tipo de valor, muito código para verificar os tipos. E o valor é copiado. Difícil estimar o custo, pois esse código está bloqueado dentro do mscorwks.dll, mas é provável que centenas de instruções de código da máquina.

O método de extensão linq oftype () também usa o é operador e o elenco. No entanto, este é um elenco para um tipo genérico. O compilador JIT gera uma chamada para uma função auxiliar, jit_unbox () que pode executar um elenco em um tipo de valor arbitrário. Eu não tenho uma ótima explicação por que é tão lenta quanto o elenco para Nullable<int>, dado que menos trabalho deve ser necessário. Eu suspeito que o NGEN.EXE possa causar problemas aqui.

Outras dicas

Parece -me que o isinst é realmente lento em tipos anuláveis. No método FindSumWithCast eu mudei

if (o is int)

para

if (o is int?)

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>

Isso começou originalmente como um comentário para a excelente resposta de Hans Passant, mas ficou muito tempo, então eu quero adicionar alguns bits aqui:

Primeiro, o C# as O operador emitirá um isinst Il Instrução (o mesmo acontece com o is operador). (Outra instrução interessante é castclass, emitiu quando você faz um elenco direto e o compilador sabe que a verificação do tempo de execução não pode ser omitida.)

Aqui está o quê isinst faz (ECMA 335 Partição III, 4.6):

Formato: isinst typetok

typetok é um token de metadados (a typeref, typedef ou typespec), indicando a classe desejada.

Se typetok é um tipo de valor não indicado ou um tipo de parâmetro genérico que é interpretado como "em caixa" typetok.

Se typetok é um tipo anulável, Nullable<T>, é interpretado como "embalado" T

Mais importante:

Se o tipo real (não o tipo de verificador rastreado) de obj é verificador-Assignável a o tipo tipo tipoTok então isinst é bem -sucedido e obj (Como resultado) é retornado inalterado enquanto a verificação rastreia seu tipo como typetok. Ao contrário das coercões (§1.6) e conversões (§3.27), isinst Nunca altera o tipo real de um objeto e preserva a identidade do objeto (consulte a Partição I).

Então, o assassino de desempenho não é isinst neste caso, mas o adicional unbox.any. Isso não ficou claro com a resposta de Hans, pois ele olhou apenas para o código Jited. Em geral, o compilador C# emitirá um unbox.any após um isinst T? (mas omitirá caso você faça isinst T, quando T é um tipo de referência).

Por que ele faz isso? isinst T? nunca o efeito que teria sido óbvio, ou seja, você recebe um T?. Em vez disso, todas essas instruções garantem que você tenha um "boxed T" que pode ser desbote para T?. Para obter um real T?, ainda precisamos unir nosso "boxed T" para T?, é por isso que o compilador emite um unbox.any depois isinst. Se você pensar bem, isso faz sentido porque o "formato da caixa" para T? é apenas um "boxed T" e fazendo castclass e isinst Executar o Unbox seria inconsistente.

Apoiando a descoberta de Hans com algumas informações do padrão, aqui vai:

(ECMA 335 Partição III, 4.33): unbox.any

Quando aplicado à forma em caixa de um tipo de valor, o unbox.any Instrução extrai o valor contido no OBJ (do tipo O). (É equivalente a unbox Seguido por ldobj.) Quando aplicado a um tipo de referência, o unbox.any a instrução tem o mesmo efeito que castclass typetok.

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

Tipicamente, unbox Simplesmente calcula o endereço do tipo de valor que já está presente dentro do objeto em caixa. Essa abordagem não é possível ao unirboxing Nullable Value Tipos. Porque Nullable<T> Os valores são convertidos em caixa Ts Durante a operação da caixa, uma implementação geralmente deve fabricar um novo Nullable<T> na pilha e calcule o endereço para o objeto recém -alocado.

Curiosamente, eu transmiti feedback sobre o suporte do operador via dynamic sendo uma ordem de magnitude mais lenta para Nullable<T> (igual a Este teste inicial) - Eu suspeito por razões muito semelhantes.

Tem que amar Nullable<T>. Outra diversão é que, embora os pontos JIT (e removam) null Para estruturas não nulos, ele faz com que 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;
    }
}

Este é o resultado de findsumwithasandhas acima: alt text

Este é o resultado do findsumWithcast: alt text

Descobertas:

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

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

Simplificando, este é o pseudo-código de usar as abordagem:

int? x;

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

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

E este é o pseudo-código de usar a abordagem de elenco:

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

Então o elenco ((int)a[i], bem, a sintaxe parece um elenco, mas na verdade é que a abordagem da mesma sintaxe, da próxima vez que serei pedante com a terminologia certa) a abordagem é realmente mais rápida, você só precisava para desbotar um valor quando um objeto é decididamente um int. O mesmo não pode ser dito sobre o uso de um as abordagem.

Profiling ainda mais:

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 inferir dessas figuras?

  • Primeiro, a abordagem IS-Then-Cast é significativamente mais rápida do que Como abordagem. 303 vs 3524
  • Segundo, o valor é marginalmente mais lento do que a fundição. 3524 vs 3272
  • Terceiro, .Hasvalue é marginalmente mais lento do que usar o manual (ou seja, usando é). 3524 vs 3282
  • Quarto, fazendo uma comparação de maçã a apple (ou seja, atribuindo o hasvalue simulado e a conversão de valor simulado) entre simulado como e real como abordagem, podemos ver simulado como ainda é significativamente mais rápido do que real como. 395 vs 3524
  • Por fim, com base na primeira e quarta conclusão, há algo errado com Comoimplementação ^_ ^

Para manter essa resposta atualizada, vale a pena mencionar que a maior parte da discussão nesta página agora está discutível agora com C# 7.1 e .NET 4.7 que suporta uma sintaxe esbelta 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
}

Descobri que um uso comum para a nova sintaxe é quando você está escrevendo um .NET Tipo de valor (ou seja struct dentro C#) Isso implementa IEquatable<MyStruct> (como a maioria deveria). Depois de implementar o fortemente tado Equals(MyStruct other) Método, agora você pode redirecionar graciosamente os não Equals(Object obj) substituir (herdado de Object) a ele o seguinte:

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

 


Apêndice: o Release construir Il O código para as duas primeiras funções de exemplo mostradas acima nesta resposta (respectivamente) são fornecidas aqui. Enquanto o código da IL para a nova sintaxe é de fato 1 byte menor, ele ganha principalmente por zero chamadas (vs. dois) e evitando o unbox Operação completamente quando possível.

// 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, o que substancia minha observação sobre o desempenho do novo C#7 Sintaxe superando as opções previamente disponíveis, consulte aqui (em particular, exemplo 'd').

Não tenho tempo para tentar, 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, que não explicará completamente o problema, mas pode contribuir.

Eu tentei o construto de verificação exato do tipo

typeof(int) == item.GetType(), que tem um desempenho tão rápido quanto o item is int versão, e sempre retorna o número (ênfase: mesmo se você escrevesse um Nullable<int> para a matriz, você precisaria usar typeof(int)). Você também precisa de um adicional null != item Verifique aqui.

No entanto

typeof(int?) == item.GetType() permanece rápido (em contraste com item is int?), mas sempre retorna falsa.

O tipo de construção é aos meus olhos da maneira mais rápida para exato Tipo de verificação, pois usa o RunTimetypeHandle. Como os tipos exatos neste caso não combinam com o Nullable, meu palpite é, is/as tem que fazer um peso adicional aqui para garantir que seja de fato uma instância de um tipo anulável.

E honestamente: o que é o seu is Nullable<xxx> plus HasValue comprar você? Nada. Você sempre pode ir diretamente para o tipo (valor) subjacente (neste caso). Você obtém o valor ou "não, não uma instância do tipo que você estava pedindo". Mesmo se você escreveu (int?)null Para a matriz, a verificação do tipo retornará falsa.

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: 2010-06-19

NOTA: O teste anterior foi realizado dentro do VS, depuração de configuração, usando o VS2009, usando o Core i7 (empresa 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
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top