c# sempreupdate Surpresa de desempenho com tipos "as" e anuláveis




ubuntu (7)

Estou apenas revisando o capítulo 4 do C # em Profundidade, que lida com tipos anuláveis, e estou adicionando uma seção sobre como usar o 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 sobre o equivalente em C # 1, usando "é" seguido de um elenco - afinal de contas, só precisamos pedir verificação dinâmica de tipo uma vez e depois uma simples verificação de valor .

Este não parece ser o caso, no entanto. Eu incluí um aplicativo de teste de amostra abaixo, que basicamente soma todos os inteiros dentro de uma matriz de objetos - mas a matriz contém muitas referências nulas e referências de string, bem como inteiros em caixa. O benchmark mede o código que você teria que usar em C # 1, o código usando o operador "as" e apenas para chutar uma solução LINQ. Para minha surpresa, o código C # 1 é 20 vezes mais rápido neste caso - e até mesmo o código LINQ (que eu esperava ser mais lento, considerando os iteradores envolvidos) bate o código "as".

A implementação do .NET do isinst para tipos anuláveis ​​é realmente muito lenta? É o unbox.any adicional. unbox.any que cause o problema? Existe outra explicação para isso? No momento, parece que vou ter que incluir um alerta 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);
    }
}

https://code.i-harness.com


Claramente, o código de 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 descompactado para uma variável que tenha o mesmo tipo do valor da caixa. Isso permite que o compilador JIT gere um código muito eficiente, sem necessidade de considerar conversões de valor.

O teste do operador is é fácil, apenas verifique se o objeto não é nulo e é do tipo esperado, mas leva algumas instruções de código de máquina. O elenco também é fácil, o compilador JIT sabe a localização dos bits de valor no objeto e os usa diretamente. Nenhuma cópia ou conversão ocorre, todo código de máquina é 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.

Casting para int? leva muito mais trabalho. A representação do valor do inteiro in a box não é compatível com o layout de memória do Nullable<int> . Uma conversão é necessária e o código é complicado devido a possíveis tipos de enumeração em caixa. O compilador JIT gera uma chamada para uma função auxiliar CLR chamada JIT_Unbox_Nullable para realizar o trabalho. Esta é uma função de propósito geral para qualquer tipo de valor, muitos códigos para checar os tipos. E o valor é copiado. É difícil estimar o custo, já que esse código está bloqueado dentro do mscorwks.dll, mas é provável que haja 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 é um cast 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 porque é tão lento quanto o elenco para Nullable<int> , dado que menos trabalho deveria ser necessário. Eu suspeito que o ngen.exe pode causar problemas aqui.


Curiosamente, eu passei feedback sobre o suporte ao operador via dynamic sendo uma ordem de grandeza mais lenta para Nullable<T> (semelhante a este teste inicial ) - eu suspeito por razões muito semelhantes.

Tenho que amar Nullable<T> . Outra divertida é que mesmo que o JIT detecte (e remova) null para estruturas não anuláveis, ele faz um bilingue para 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;
    }
}

Eu não tenho tempo para experimentar, 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.


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

typeof(int) == item.GetType() , que executa tão rápido quanto o item is int version, e sempre retorna o número (ênfase: mesmo se você escreveu um Nullable<int> para o array, você precisaria usar typeof(int) ). Você também precisa de um adicional null != item check aqui.

Contudo

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

O typeof-construct é, a meu ver, o caminho mais rápido para a verificação exata de tipos, já que ele usa o RuntimeTypeHandle. Uma vez que os tipos exatos neste caso não combinam com nullable, meu palpite é, is/as tenho que fazer um levantamento de peso adicional aqui para garantir que seja de fato uma instância de um tipo Nullable.

E honestamente: o que o seu is Nullable<xxx> plus HasValue te compra? Nada. Você sempre pode ir diretamente para o tipo subjacente (valor) (nesse 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 no array, a verificação do tipo retornará false.


Para manter esta resposta atualizada, vale a pena mencionar que a maior parte da discussão nesta página agora é discutível agora com o C # 7.1 eo .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
}

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 implementa IEquatable<MyStruct> (como a maioria deve). Depois de implementar o método Equals(MyStruct other) fortemente tipado, agora você pode Equals(MyStruct other) redirecionar a substituição Equals(Object obj) não tipificada (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 da versão Release para as duas primeiras funções de exemplo mostradas acima nesta resposta (respectivamente) é dado aqui. Enquanto o código de IL para a nova sintaxe é de fato 1 byte menor, ele ganha muito grande fazendo chamadas zero (vs. dois) e evitando a operação unbox 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 que substanciam minha observação sobre o desempenho da nova sintaxe C # 7 superando as opções anteriormente disponíveis, veja here (em particular, o exemplo 'D').


Parece-me que o isinst é muito 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 eu posso ver é que

isinst     [mscorlib]System.Int32

é alterado para

isinst     valuetype [mscorlib]System.Nullable`1<int32>

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 feito dentro do VS, depuração de 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 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






unboxing