¿Existe una desventaja al usar AggressiveInlining en propiedades simples?

16

Apuesto a que podría responderlo yo mismo si supiera más acerca de las herramientas para analizar cómo se comporta C # / JIT, pero como no lo sé, por favor tengan paciencia para preguntarme.

Tengo un código simple como este:

    private SqlMetaData[] meta;

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

Como puede ver, puse AggressiveInlining porque siento que debería estar en línea.
Yo creo que. No hay garantía de que el JIT lo alinee de otra manera. ¿Me equivoco?

¿Podría hacer este tipo de cosas dañar el rendimiento / estabilidad / algo?

Sarga
fuente
2
1) En mi experiencia, tales métodos primitivos se alinearán sin el atributo. Principalmente encontré el atributo útil con métodos no triviales que aún deberían estar en línea. 2) No hay garantía de que un método decorado con el atributo esté en línea tampoco. Es simplemente una pista para el JITter.
CodesInChaos
No sé mucho sobre el nuevo atributo de alineación, pero poner uno aquí seguramente no hará ninguna diferencia en el rendimiento. Todo lo que está haciendo es devolver una referencia a una matriz, y el JIT seguramente ya tomará la decisión correcta aquí.
Robert Harvey
14
3) Si se incluye demasiado, el código se hace más grande y es posible que ya no se ajuste a los cachés. Los errores de caché pueden ser un éxito significativo en el rendimiento. 4) Recomiendo no usar el atributo hasta que un punto de referencia demuestre que mejora el rendimiento.
CodesInChaos
44
Deja de preocuparte. Cuanto más intentes burlar al compilador, más encontrará maneras de burlarte de ti. Encuentra algo más de qué preocuparte.
david.pfx
1
Por mis dos centavos, he visto grandes ganancias en el modo de lanzamiento, especialmente cuando se llama a una función más grande en un ciclo cerrado.
jjxtra

Respuestas:

22

Los compiladores son bestias inteligentes. Por lo general, exprimirán automáticamente tanto rendimiento como puedan desde cualquier lugar que puedan.

Tratar de ser más astuto que el compilador no suele hacer una gran diferencia, y tiene muchas posibilidades de ser contraproducente. Por ejemplo, la integración hace que su programa sea más grande ya que duplica el código en todas partes. Si su función se usa en muchos lugares en todo el código, en realidad podría ser perjudicial, como se señaló en @CodesInChaos. Si es obvio que la función debe estar en línea, puede apostar que el compilador lo hará.

En caso de duda, aún puede hacer ambas cosas y comparar si hay algún aumento de rendimiento, esa es la única forma segura de hacerlo ahora. Pero mi apuesta es que la diferencia será despreciable, el código fuente será "más ruidoso".

dagnelies
fuente
3
Creo que el "ruido" es el punto más importante aquí. Mantenga su código ordenado y confíe en su compilador para hacer lo correcto hasta que se demuestre lo contrario. Todo lo demás es una optimización prematura peligrosa.
5gon12eder
1
Si los compiladores son tan inteligentes, ¿por qué tratar de ser más astuto que el compilador?
Little Endian
11
Los compiladores no son inteligentes . Los compiladores no hacen "lo correcto". No atribuyas inteligencia donde no está. De hecho, el compilador / JITer de C # es excesivamente tonto. Por ejemplo, no incluirá nada más allá de 32 bytes IL o casos que involucren structs como parámetros, donde en muchos casos debería y podría. Además de perder cientos de optimizaciones obvias , que incluyen, entre otras, evitar verificaciones y asignaciones innecesarias de límites, entre otras cosas.
JBeurer
44
La elusión de comprobación @DaveBlack Bounds en C # ocurre en una lista muy pequeña de casos muy básicos, generalmente en la secuencia más básica para los bucles realizados, e incluso entonces muchos bucles simples no se optimizan. Los bucles de matriz multidimensional no obtienen eliminación de verificación de límites, los bucles iterados en orden descendente no, los bucles en matrices recién asignadas no lo hacen. Muchos casos simples en los que esperarías que el compilador hiciera su trabajo. Pero no lo hace. Porque es cualquier cosa, pero inteligente. blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/…
JBeurer
3
Los compiladores no son "bestias inteligentes". Simplemente aplican un montón de heurísticas y hacen compensaciones para tratar de encontrar un equilibrio para la mayoría de los escenarios anticipados por los escritores del compilador. Sugiero leer: docs.microsoft.com/en-us/previous-versions/dotnet/articles/…
cdiggins
8

Tiene razón: no hay forma de garantizar que el método esté en línea: MSDN MethodImplOptions Enumeration , SO MethodImplOptions.AggressiveInlining vs TargetedPatchingOptOut .

Los programadores son más inteligentes que un compilador, pero trabajamos en un nivel superior y nuestras optimizaciones son productos del trabajo de un hombre: el nuestro. Jitter ve lo que sucede durante la ejecución. Puede analizar tanto el flujo de ejecución como el código de acuerdo con el conocimiento aportado por sus diseñadores. Puede conocer mejor su programa, pero ellos conocen mejor el CLR. ¿Y quién será más correcto en sus optimizaciones? No lo sabemos con certeza.

Es por eso que debes probar cualquier optimización que hagas. Incluso si es muy simple. Y tenga en cuenta que el entorno puede cambiar y su optimización o desoptimización puede tener un resultado bastante inesperado.

Eugene Podskal
fuente
8

EDITAR: Me doy cuenta de que mi respuesta no respondió exactamente la pregunta, aunque no hay una desventaja real, a partir de los resultados de mi tiempo tampoco hay una verdadera ventaja. La diferencia entre un captador de propiedades en línea es 0.002 segundos sobre 500 millones de iteraciones. Mi caso de prueba también puede no ser 100% exacto ya que está usando una estructura porque hay algunas advertencias sobre la inquietud y la alineación con las estructuras.

Como siempre, la única forma de saber realmente es escribir un examen y resolverlo. Aquí están mis resultados con la siguiente configuración:

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

Proyecto vacío con la siguiente configuración:

.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

Probado con 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
fuente
5

Los compiladores hacen muchas optimizaciones. La inclusión es uno de ellos, ya sea que el programador lo quiera o no. Por ejemplo, MethodImplOptions no tiene una opción "en línea". Porque la compilación la realiza automáticamente el compilador si es necesario.

Muchas otras optimizaciones se realizan especialmente si se habilitan desde las opciones de compilación, o el modo "lanzamiento" lo hará. Pero estas optimizaciones son optimizaciones tipo "funcionó para usted, ¡excelente! No funcionó, déjelo" y generalmente ofrecen un mejor rendimiento.

[MethodImpl(MethodImplOptions.AggressiveInlining)]

es solo una señal para el compilador de que realmente se necesita una operación de alineación aquí. Más información aquí y aquí

Para responder tu pregunta;

No hay garantía de que el JIT lo alinee de otra manera. ¿Me equivoco?

Cierto. Sin garantía; Ninguno de los dos C # tiene una opción de "forzar en línea".

¿Podría hacer este tipo de cosas dañar el rendimiento / estabilidad / algo?

En este caso, no, como se dice al escribir aplicaciones administradas de alto rendimiento: un manual

Los métodos de obtención y establecimiento de propiedades generalmente son buenos candidatos para la inclusión en línea, ya que todo lo que hacen generalmente es inicializar miembros de datos privados.

myuce
fuente
1
Se espera que las respuestas respondan completamente la pregunta. Si bien este es un comienzo para una respuesta, realmente no entra en la profundidad esperada para una respuesta.
1
Actualicé mi respuesta. Espero que ayude.
myuce