Pregunta

Estoy haciendo un poco de rayos sobre el rendimiento de los miembros virtuales vs sellados.

A continuación se muestra mi código de prueba.

La salida es

virtual total 3166ms
per call virtual 3.166ns
sealed total 3931ms
per call sealed 3.931ns

Debo estar haciendo algo mal porque de acuerdo con esto, la llamada virtual es más rápida que la llamada sellada.

Me estoy ejecutando en modo de lanzamiento con "Optimize Code" activado.

Editar: cuando se ejecuta fuera de VS (como una aplicación de consola), los tiempos están cerca de un calor muerto. Pero lo virtual casi siempre sale al frente.

[TestFixture]
public class VirtTests
{

    public class ClassWithNonEmptyMethods
    {
        private double x;
        private double y;

        public virtual void VirtualMethod()
        {
            x++;
        }
        public void SealedMethod()
        {
            y++;
        }
    }

    const int iterations = 1000000000;


    [Test]
    public void NonEmptyMethodTest()
    {

        var foo = new ClassWithNonEmptyMethods();
        //Pre-call
        foo.VirtualMethod();
        foo.SealedMethod();

        var virtualWatch = new Stopwatch();
        virtualWatch.Start();
        for (var i = 0; i < iterations; i++)
        {
            foo.VirtualMethod();
        }
        virtualWatch.Stop();
        Console.WriteLine("virtual total {0}ms", virtualWatch.ElapsedMilliseconds);
        Console.WriteLine("per call virtual {0}ns", ((float)virtualWatch.ElapsedMilliseconds * 1000000) / iterations);


        var sealedWatch = new Stopwatch();
        sealedWatch.Start();
        for (var i = 0; i < iterations; i++)
        {
            foo.SealedMethod();
        }
        sealedWatch.Stop();
        Console.WriteLine("sealed total {0}ms", sealedWatch.ElapsedMilliseconds);
        Console.WriteLine("per call sealed {0}ns", ((float)sealedWatch.ElapsedMilliseconds * 1000000) / iterations);

    }

}
¿Fue útil?

Solución

Está probando los efectos de la alineación de la memoria en la eficiencia del código. El compilador JIT de 32 bits tiene problemas para generar un código eficiente para tipos de valor que tienen más de 32 bits de tamaño, largo y doble en el código C#. La raíz del problema es el asignador de montón GC de 32 bits, solo promete la alineación de la memoria asignada en direcciones que son un múltiplo de 4. Ese es un problema aquí, está incrementando dobles. Un doble es eficiente solo cuando está alineada en una dirección que es un múltiplo de 8. El mismo problema con la pila, en caso de variables locales, también está alineada solo a 4 en una máquina de 32 bits.

El caché de la CPU L1 se organiza internamente en bloques llamado "línea de caché". Hay una penalización cuando el programa lee un doble mal alineado. Especialmente uno que se extiende a horcajadas en el final de una línea de caché, los bytes de dos líneas de caché deben leerse y pegarse juntos. La alineación errónea no es infrecuente en el jitter de 32 bits, es simplemente 50-50 probabilidades de que el campo 'X' se asigne en una dirección que es un múltiplo de 8. Si no es entonces 'X' y 'Y' se desalinean y uno de ellos puede a horcajadas sobre la línea de caché. La forma en que escribió la prueba, eso hará que VirtualMethod o SelledMethod sea más lento. Asegúrese de dejar que usen el mismo campo para obtener resultados comparables.

Lo mismo es cierto para el código. Cambie el código por la prueba virtual y sellada para cambiar arbitrariamente el resultado. No tuve problemas para hacer la prueba sellada un poco más rápido de esa manera. Dada la modesta diferencia en la velocidad, probablemente esté buscando un problema de alineación de código. El X64 Jitter hace un esfuerzo para insertar NOPS para alinear un objetivo de rama, el X86 Jitter no.

También debe ejecutar la prueba de sincronización varias veces en un bucle, al menos 20. Es probable que también observe el efecto del recolector de basura que mueve el objeto de clase. El doble puede tener una alineación diferente después, cambiando drásticamente el momento. Acceder a un valor de tipo de valor de 64 bits como largo o doble tiene 3 tiempos distintos, alineados en 8, alineados en 4 dentro de una línea de caché y alineado en 4 en dos líneas de caché. En rápido orden lento.

La penalización es empinada, leyendo un doble que se extiende a horcajadas sobre una línea de caché es más o menos Tres veces más lento que leer uno alineado. También la razón central por la cual se asigna un doble [] (matriz de dobles) en el montón de objeto grande incluso cuando tiene solo 1000 elementos, bien al sur del umbral normal de 80 kb, el LOH tiene una garantía de alineación de 8. Estos problemas de alineación Desaparece completamente en el código generado por el X64 Jitter, tanto la pila como el montón GC tienen una alineación de 8.

Otros consejos

Primero, debes marcar el método sealed.

En segundo lugar, proporcione un override al método virtual. Crear una instancia de la clase derivada.

Como tercera prueba, cree un sealed override método.

Ahora puedes comenzar a comparar.

Editar: Probablemente deberías ejecutar este exterior vs.

Actualizar:

Ejemplo de lo que quiero decir.

abstract class Foo
{
  virtual void Bar() {}
}

class Baz : Foo
{
  sealed override void Bar() {}
}

class Woz : Foo
{
  override void Bar() {}
}

Ahora pruebe la velocidad de llamada de Bar para una instancia de Baz y Woz. También sospecho que la visibilidad de los miembros y la clase fuera de la asamblea podría afectar el análisis JIT.

Es posible que esté viendo algunos costos iniciales. Intente envolver el código Test-A/Test-B en un bucle y ejecutarlo varias veces. También puede estar viendo algún tipo de efectos de pedido. Para evitar eso (y la parte superior/inferior de los efectos de bucle), desenrolcelo 2-3 veces.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top