Costo de bloquear .NET vs Java

Estaba jugando con disruptor framework y su puerto para la plataforma .NET y encontré un caso interesante. Puede ser que me pierda algo por completo, así que estoy buscando la ayuda del Todopoderoso. Comunidad.

        long iterations = 500*1000*1000;
        long testValue = 1;

        //.NET 4.0. Release build. Mean time - 26 secs;
        object lockObject = new object();
        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            lock (lockObject)
            {
                testValue++;    
            }
        }
        sw.Stop();

        //Java 6.25. Default JVM params. Mean time - 17 secs.
        Object lock = new Object();
        long start = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++)
        {
                synchronized (lock)
                {
                    testValue++;
                }
        }
        long stop = System.currentTimeMillis();

Parece que adquirir el bloqueo en el escenario con un hilo de inicio de sesión en .NET cuesta solo un 50% más que en Java. Al principio sospechaba de los temporizadores, pero he realizado la misma prueba varias veces con resultados que se aproximan a los valores medios mencionados anteriormente. Entonces sospeché de Sincronizado bloque de código, pero no hace más que monitoreador / monitoreado instrucciones de código de bytes - lo mismo que bloquear palabra clave en .NET. ¿Alguna otra idea de por qué tomar un candado es tan caro en .NET vs Java?

preguntado el 27 de agosto de 11 a las 17:08

¿midió sin la cerradura para asegurarse de tener una línea de base justa? También JITing podría ser un factor aquí, podría poner el código en un método separado, ejecutarlo una vez y solo entonces comenzar a medir. -

Por lo que vale; si bien no es una respuesta directa, usaría Interlocked.Increment(ref testValue) en lugar de bloquearlo, o bloquearlo una vez solo durante todo el ciclo. Las diferencias de rendimiento que está viendo no deberían ser evidentes si usa un patrón de bloqueo adecuado. -

@BrokenGlass Tienes razón sobre una línea de base, pero no creo que JITing deba tomar una cantidad de tiempo apreciable para un método único y simple. -

¿Ejecutó el código C # sin el depurador adjunto? -

@vcsjones: No es que no esté de acuerdo, pero Interlocked.Increment tomaría más tiempo, lo que simplemente desafía y responde a la pregunta. -

6 Respuestas

Sí, parece que realizar un bloqueo no atendido es más caro en .NET que en Java. (Los resultados en mi netbook son un poco más dramáticos aún).

Hay varios aspectos del rendimiento que serán más rápidos en una plataforma que en otra, a veces hasta este punto. HotSpot JIT y .NET JIT son radicalmente diferentes en varios aspectos, entre otras cosas porque .NET JIT solo se ejecuta una vez en IL, mientras que HotSpot es capaz de optimizar cada vez más a medida que un fragmento de código en particular se ejecuta cada vez con más frecuencia.

La pregunta importante es si esto es realmente importante. Si su aplicación de la vida real gasta realmente adquiere un candado incontestado 500 millones de veces por minuto, probablemente is significativo, y probablemente debería rediseñar su aplicación un poco. Si su aplicación de la vida real realmente hace un trabajo real dentro de la cerradura (o entre adquisiciones de la cerradura), es poco probable que sea un cuello de botella real.

Recientemente encontré dos errores de .NET (primera parte; la segunda parte) que tengo que solucionar mientras estoy escribiendo una "biblioteca de nivel de sistema" y habrían marcado una diferencia significativa cuando una aplicación montón de análisis de fecha / hora, pero este tipo de microoptimización es raramente vale la pena hacer.

Respondido 27 ago 11, 21:08

Gracias, podría ser que una cosa sea un poco más lenta que otra y no sea yo quien se haya perdido por completo algún punto obvio. - Andrey Taptunov

Lo primero que debe recordar sobre los micro-benchmarks es que Java es particularmente bueno para identificar y eliminar código que no hace nada. He descubierto que una y otra vez, Java hace código inútil más rápido que cualquier otro lenguaje. ;)

Si Java es sorprendentemente rápido en comparación con otro idioma, la primera pregunta debería ser; ¿El código hace algo remotamente útil? (o incluso parecer que podría ser útil)

Java tiende a desenrollarse en bucle más de lo que solía hacerlo. También puede combinar cerraduras. Como su prueba es indiscutible y hace cualquier cosa, su código parece tener un aspecto similar.

for (int i = 0; i < iterations; i+=8) {
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
}

que se convierte

for (int i = 0; i < iterations; i+=8) {
    synchronized (lock) {
        testValue++;
        testValue++;
        testValue++;
        testValue++;
        testValue++;
        testValue++;
        testValue++;
        testValue++;
    }
}

ya que no se usa testValue.

for (int i = 0; i < iterations; i+=8) {
    synchronized (lock) {
    }
}

y finalmente

{ }

Respondido 28 ago 11, 00:08

@RoundTower ... se necesita tiempo para decidir si vale la pena desenrollar un bucle, determinar si las cerraduras se pueden fusionar y realizar un análisis de escape. A veces, es posible que el ciclo ya esté parcialmente calculado MIENTRAS se toman estas decisiones ... En cualquier caso, "la JVM realmente apesta" no es una declaración útil ni informada. - Steve McLeod

¿La variable 'testValue' es local a un método? Si es así, es posible que el JRE haya detectado que el bloqueo es innecesario ya que la variable es local a un hilo y, por lo tanto, no se bloquea en absoluto.

Esto se explica aquí.

Para mostrar cuán difícil es saber qué optimizaciones decide hacer la JVM, y cuándo decide hacerlo, examine estos resultados de ejecutar su código tres veces consecutivas:

public static void main(String[] args) {
  System.out.println("Java version: " + System.getProperty("java.version"));
  System.out.println("First call : " + doIt(500 * 1000 * 1000, 1)); // 14 secs
  System.out.println("Second call: " + doIt(500 * 1000 * 1000, 1)); // 1 sec
  System.out.println("Third call : " + doIt(500 * 1000 * 1000, 1)); // 0.4 secs
}

private static String doIt(final long iterations, long testValue) {
    Object lock = new Object();
    long start = System.currentTimeMillis();
    for (int i = 0; i < iterations; i++) {
        synchronized (lock) {
            testValue++;
        }
    }
    long stop = System.currentTimeMillis();
    return (stop - start) + " ms, result = " + testValue;
}

Estos resultados son tan difíciles de explicar, creo que solo un ingeniero de JVM podría ayudar a arrojar luz.

Respondido 28 ago 11, 20:08

Lo siento, no estaba lo suficientemente claro con mis ejemplos, pero ambas variables iteraciones y testValue se pasan como parámetros al método. De todos modos, gracias por el enlace al buen artículo. - Andrey Taptunov

Hola Andrei, ya veo ... Pero en Java, los valores largos se pasan por valor y, por lo tanto, siguen siendo locales al método y, por lo tanto, al hilo. Es casi seguro que todavía son candidatos para que el bloqueo sea ignorado. - Steve McLeod

Steve, entiendo tu punto, pero si se ignora el bloqueo o si todos los bloqueos se contraen en uno solo, este código no debería tardar 17 segundos en ejecutarse y no debería haber ninguna diferencia entre el mismo código sin ningún bloqueo (~ 1 segundo en mi caja). - Andrey Taptunov

Recuerde, ambos son extremadamente rápidos; estamos hablando de 50 ciclos de CPU para bloquear-leer-escribir-desbloquear aquí.

En Java, lo comparé con un impl simulado en caso no atendido

volatile int waitingList=0;

    AtomicInteger x = new AtomicInteger(0);
    for (int i = 0; i < iterations; i++)
    {
        while( ! x.compareAndSet(0, 1) )
            ;

        testValue++;

        if(waitingList!=0)
            ;
        x.set(0);
    }

Esta simulación de huesos desnudos es un poco más rápida que la synchronized versión, el tiempo necesario es 15/17.

Eso muestra que en su caso de prueba, Java no hizo optimizaciones locas, honestamente bloqueó-leer-actualizar-desbloquear para cada iteración. Sin embargo, la impl de Java es tan rápida como la implícita básica; no puede ser más rápido.

Aunque el impl de C # también está cerca del mínimo, aparentemente hace una o dos cosas más que Java. No estoy familiarizado con C #, pero esto probablemente indica alguna diferencia semántica, por lo que C # tiene que hacer algo más.

Respondido 30 ago 11, 10:08

Cuando investigué los costos de bloqueo / sincronización hace unos años en Java, terminé con una gran pregunta sobre cómo el bloqueo afectaba el rendimiento general también para otros subprocesos que acceden a cualquier tipo de memoria. Lo que puede verse afectado es la memoria caché de la CPU, especialmente en una computadora multiprocesador, y depende de cómo la arquitectura específica de la CPU maneja la sincronización de la memoria caché. Creo que el rendimiento general no se ve afectado en una arquitectura de CPU única moderna, pero no estoy seguro.

De todos modos, en caso de duda, especialmente cuando se pueden usar computadoras multiproceso para alojar el software, puede valer la pena poner un candado en un nivel superior en varias operaciones.

Respondido el 12 de Septiembre de 13 a las 23:09

Java JIT optimizará la sincronización ya que el objeto de bloqueo es local de subproceso (es decir, está confinado a la pila del subproceso y nunca se comparte) y, por lo tanto, nunca se puede sincronizar desde otro subproceso. No estoy seguro de si .NET JIT hará esto.

Vea este artículo muy informativo, especialmente la parte de la elisión de la cerradura.

Respondido el 19 de junio de 15 a las 10:06

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.