Existe uma desvantagem no uso do AggressiveInlining em propriedades simples?

16

Aposto que eu mesmo poderia responder se soubesse mais sobre as ferramentas para analisar como o C # / JIT se comporta, mas como não o faço, por favor, pergunte.

Eu tenho um código simples como este:

    private SqlMetaData[] meta;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private SqlMetaData[] Meta
    {
        get
        {
            return this.meta;
        }
    }

Como você pode ver, eu coloquei AggressiveInlining porque acho que deveria estar embutido.
Eu acho que. Não há garantia de que o JIT incluiria o contrário. Estou errado?

Poderia fazer esse tipo de coisa prejudicar o desempenho / estabilidade / qualquer coisa?

Sarja
fonte
2
1) Na minha experiência, esses métodos primitivos serão incorporados sem o atributo Achei principalmente o atributo útil com métodos não triviais que ainda devem ser incorporados. 2) Não há garantia de que um método decorado com o atributo também seja incorporado. É apenas uma dica para o JITter.
CodesInChaos
Não sei muito sobre o novo atributo inlining, mas colocar um aqui quase certamente não fará diferença no desempenho. Tudo o que você está fazendo é retornar uma referência a uma matriz, e o JIT certamente já estará fazendo a escolha correta aqui.
Robert Harvey
14
3) Inline demais significa que o código se torna maior e pode não caber mais nos caches. As falhas de cache podem ser um impacto significativo no desempenho. 4) Eu recomendo não usar o atributo até que um benchmark mostre que ele melhora o desempenho.
#
4
Pare de se preocupar. Quanto mais você tenta enganar o compilador, mais ele encontrará maneiras de enganá-lo. Encontre outra coisa com que se preocupar.
David.pfx
1
Para meus dois centavos, vi grandes ganhos no modo de liberação, especialmente ao chamar uma função maior em um loop apertado.
Jjxtra 11/08

Respostas:

22

Compiladores são bestas inteligentes. Geralmente, eles extraem automaticamente o máximo de desempenho possível de qualquer lugar que puderem.

Tentar enganar o compilador geralmente não faz uma grande diferença e tem muitas chances de sair pela culatra. Por exemplo, inlining torna seu programa maior, pois duplica o código em todos os lugares. Se sua função for usada em muitos lugares ao longo do código, poderá ser prejudicial, como apontado @CodesInChaos. Se for óbvio que a função deve ser incorporada, você pode apostar que o compilador fará isso.

Em caso de hesitação, você ainda pode fazer as duas coisas e comparar se houver algum ganho de desempenho, essa é a única maneira certa até agora. Mas minha aposta é que a diferença será negligenciável, o código fonte será apenas "mais barulhento".

dagnelies
fonte
3
Eu acho que o "ruído" é o ponto mais importante aqui. Mantenha seu código organizado e confie no seu compilador para fazer a coisa certa até prova em contrário. Tudo o resto é uma otimização prematura perigosa.
5gon12eder
1
Se os compiladores são tão inteligentes, por que tentar enganar o compilador?
Little Endian
11
Compiladores não são inteligentes . Compiladores não fazem "a coisa certa". Não atribua inteligência onde não está. De fato, o compilador C # / JITer é excessivamente burro. Por exemplo, ele não alinhará nada além de 32 bytes IL ou casos envolvendo structs como parâmetros - onde, em muitos casos, deveria e poderia. Além de perder centenas de otimizações óbvias - incluindo, entre outras, evitar verificações e alocações desnecessárias de limites, entre outras coisas.
JBeurer #
4
A ilusão de verificação do @DaveBlack Bounds em C # acontece em uma lista muito pequena de casos muito básicos, geralmente no seqüencial mais básico para loops executados, e mesmo assim muitos loops simples falham ao serem otimizados. Os loops de matriz multidimensionais não obtêm eliminação da verificação de limites, os loops iterados em ordem decrescente não, os loops nas matrizes alocadas recentemente não. Muitos casos simples em que você esperaria que o compilador fizesse seu trabalho. Mas isso não acontece. Porque é tudo, menos inteligente. blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/…
JBeurer
3
Compiladores não são "bestas inteligentes". Eles simplesmente aplicam um monte de heurísticas e fazem trocas para tentar encontrar um equilíbrio para a maioria dos cenários previstos pelos autores do compilador. Sugiro a leitura: docs.microsoft.com/pt-br/previous-versions/dotnet/articles/…
cdiggins
8

Você está certo - não há como garantir que o método seja incorporado - Enumeração MSDN MethodImplOptions , SO MethodImplOptions.AggressiveInlining vs TargetedPatchingOptOut .

Os programadores são mais inteligentes que um compilador, mas trabalhamos em um nível superior e nossas otimizações são produtos do trabalho de um homem - o nosso. Jitter vê o que está acontecendo durante a execução. Ele pode analisar o fluxo de execução e o código de acordo com o conhecimento colocado por seus projetistas. Você pode conhecer melhor o seu programa, mas eles conhecem melhor o CLR. E quem será mais correto em suas otimizações? Não sabemos ao certo.

É por isso que você deve testar qualquer otimização que fizer. Mesmo que seja muito simples. E leve em consideração que o ambiente pode mudar e sua otimização ou des otimização pode ter um resultado inesperado.

Eugene Podskal
fonte
8

Edição: Eu sei que a minha resposta não respondeu exatamente a pergunta, embora não haja desvantagem real, dos meus resultados de tempo também não há vantagem real. A diferença entre um getter de propriedades embutido é de 0,002 segundos em 500 milhões de iterações. Meu caso de teste também pode não ser 100% exato, pois está usando uma estrutura, porque existem algumas ressalvas no jitter e alinhamento com estruturas.

Como sempre, a única maneira de realmente saber é escrever um teste e descobrir. Aqui estão meus resultados com a seguinte configuração:

Windows 7 Home  
8GB ram  
64bit os  
i5-2300 2.8ghz  

Projeto vazio com as seguintes configurações:

.NET 4.5  
Release mode  
Start without debugger attached - CRUCIAL  
Unchecked "Prefer 32-bit" under project build settings  

Resultados

struct get property                               : 0.3097832 seconds
struct inline get property                        : 0.3079076 seconds
struct method call with params                    : 1.0925033 seconds
struct inline method call with params             : 1.0930666 seconds
struct method call without params                 : 1.5211852 seconds
struct intline method call without params         : 1.2235001 seconds

Testado com este código:

class Program
{
    const int SAMPLES = 5;
    const int ITERATIONS = 100000;
    const int DATASIZE = 1000;

    static Random random = new Random();
    static Stopwatch timer = new Stopwatch();
    static Dictionary<string, TimeSpan> timings = new Dictionary<string, TimeSpan>();

    class SimpleTimer : IDisposable
    {
        private string name;
        public SimpleTimer(string name)
        {
            this.name = name;
            timer.Restart();
        }

        public void Dispose()
        {
            timer.Stop();
            TimeSpan ts = TimeSpan.Zero;
            if (timings.ContainsKey(name))
                ts = timings[name];

            ts += timer.Elapsed;
            timings[name] = ts;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 4)]
    struct TestStruct
    {
        private int x;
        public int X { get { return x; } set { x = value; } }
    }


    [StructLayout(LayoutKind.Sequential, Size = 4)]
    struct TestStruct2
    {
        private int x;

        public int X
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get { return x; }
            set { x = value; }
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct3
    {
        private int x;
        private int y;

        public void Update(int _x, int _y)
        {
            x += _x;
            y += _y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct4
    {
        private int x;
        private int y;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Update(int _x, int _y)
        {
            x += _x;
            y += _y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct5
    {
        private int x;
        private int y;

        public void Update()
        {
            x *= x;
            y *= y;
        }
    }

    [StructLayout(LayoutKind.Sequential, Size = 8)]
    struct TestStruct6
    {
        private int x;
        private int y;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Update()
        {
            x *= x;
            y *= y;
        }
    }

    static void RunTests()
    {
        for (var i = 0; i < SAMPLES; ++i)
        {
            Console.Write("Sample {0} ... ", i);
            RunTest1();
            RunTest2();
            RunTest3();
            RunTest4();
            RunTest5();
            RunTest6();
            Console.WriteLine(" complate");
        }
    }

    static int RunTest1()
    {
        var data = new TestStruct[DATASIZE];
        var temp = 0;
        unchecked
        {
            //init the data, just so jitter can't make assumptions
            for (var j = 0; j < DATASIZE; ++j)
                data[j].X = random.Next();

            using (new SimpleTimer("struct get property"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        temp += data[j].X;
                    }
                }
            }
        }
        //again need variables to cross scopes to make sure the jitter doesn't do crazy optimizations
        return temp;
    }

    static int RunTest2()
    {
        var data = new TestStruct2[DATASIZE];
        var temp = 0;
        unchecked
        {
            //init the data, just so jitter can't make assumptions
            for (var j = 0; j < DATASIZE; ++j)
                data[j].X = random.Next();

            using (new SimpleTimer("struct inline get property"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        temp += data[j].X;
                    }
                }
            }
        }
        //again need variables to cross scopes to make sure the jitter doesn't do crazy optimizations
        return temp;
    }

    static void RunTest3()
    {
        var data = new TestStruct3[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct method call with params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update(j, i);
                    }
                }
            }
        }
    }

    static void RunTest4()
    {
        var data = new TestStruct4[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct inline method call with params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update(j, i);
                    }
                }
            }
        }
    }

    static void RunTest5()
    {
        var data = new TestStruct5[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct method call without params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update();
                    }
                }
            }
        }
    }

    static void RunTest6()
    {
        var data = new TestStruct6[DATASIZE];
        unchecked
        {
            using (new SimpleTimer("struct intline method call without params"))
            {
                for (var j = 0; j < DATASIZE; ++j)
                {
                    for (var i = 0; i < ITERATIONS; ++i)
                    {
                        //use some math to make sure its not optimized out (aka don't use an incrementor)
                        data[j].Update();
                    }
                }
            }
        }
    }

    static void Main(string[] args)
    {
        RunTests();
        DumpResults();
        Console.Read();
    }

    static void DumpResults()
    {
        foreach (var kvp in timings)
        {
            Console.WriteLine("{0,-50}: {1} seconds", kvp.Key, kvp.Value.TotalSeconds);
        }
    }
}
Chris Phillips
fonte
5

Os compiladores fazem muitas otimizações. Inlining é um deles, quer o programador queira ou não. Por exemplo, MethodImplOptions não tem uma opção "embutido". Porque o embutimento é feito automaticamente pelo compilador, se necessário.

Muitas outras otimizações são feitas especialmente se ativadas nas opções de compilação, ou o modo "release" fará isso. Mas essas otimizações são do tipo "funcionou para você, ótimo! Não funcionou, deixe" otimizações e geralmente oferecem melhor desempenho.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

é apenas um sinalizador para o compilador de que uma operação embutida é realmente necessária aqui. Mais informações aqui e aqui

Para responder sua pergunta;

Não há garantia de que o JIT incluiria o contrário. Estou errado?

Verdade. Nenhuma garantia; Nem o C # tem uma opção "forçar a inclusão".

Poderia fazer esse tipo de coisa prejudicar o desempenho / estabilidade / qualquer coisa?

Nesse caso, não, como é dito em Gravando aplicativos gerenciados de alto desempenho: uma cartilha

Os métodos get e set de propriedade geralmente são bons candidatos para inlining, pois tudo o que eles fazem é normalmente inicializar membros de dados privados.

myuce
fonte
1
Espera-se que as respostas respondam totalmente à pergunta. Embora este seja o começo de uma resposta, ele realmente não entra na profundidade esperada para uma resposta.
1
Atualizei minha resposta. Espero que ajude.
myuce