framework - ¿Por qué este código Java es 6 veces más rápido que el código C#idéntico?
java gui framework (7)
Tengo algunas soluciones diferentes para el problema 5 del Proyecto Euler , pero la diferencia de tiempo de ejecución entre los dos idiomas / plataformas en esta implementación en particular me intriga. No realicé ninguna optimización con los indicadores del compilador, simplemente javac
(a través de la línea de comandos) y csc
(a través de Visual Studio).
Aquí está el código de Java. Termina en 55ms.
public class Problem005b
{
public static void main(String[] args)
{
long begin = System.currentTimeMillis();
int i = 20;
while (true)
{
if (
(i % 19 == 0) &&
(i % 18 == 0) &&
(i % 17 == 0) &&
(i % 16 == 0) &&
(i % 15 == 0) &&
(i % 14 == 0) &&
(i % 13 == 0) &&
(i % 12 == 0) &&
(i % 11 == 0)
)
{
break;
}
i += 20;
}
long end = System.currentTimeMillis();
System.out.println(i);
System.out.println(end-begin + "ms");
}
}
Aquí está el código C # idéntico. Termina en 320ms.
using System; namespace ProjectEuler05 { class Problem005 { static void Main(String[] args) { DateTime begin = DateTime.Now; int i = 20; while (true) { if ( (i % 19 == 0) && (i % 18 == 0) && (i % 17 == 0) && (i % 16 == 0) && (i % 15 == 0) && (i % 14 == 0) && (i % 13 == 0) && (i % 12 == 0) && (i % 11 == 0) ) { break; } i += 20; } DateTime end = DateTime.Now; TimeSpan elapsed = end - begin; Console.WriteLine(i); Console.WriteLine(elapsed.TotalMilliseconds + "ms"); } } }
- Para cronometrar la ejecución del código, debe usar la clase
StopWatch
. - Además, debe tener en cuenta el JIT, el tiempo de ejecución, etc., así que deje que la prueba se ejecute una cantidad suficiente de veces (como 10,000, 100,000 veces) y obtenga algún tipo de promedio. Es importante ejecutar el código varias veces, no el programa. Así que escribe un método, y recorre el método principal para obtener tus medidas.
- elimine todos los elementos de depuración de los ensamblajes y deje que el código se ejecute de forma independiente en una versión de lanzamiento
(Movido de OP)
Cambiar el objetivo de x86 a anycpu ha reducido el tiempo de ejecución promedio a 84 ms por ejecución, en comparación con 282 ms. Tal vez debería dividir esto en un segundo hilo?
ACTUALIZAR:
Gracias a Femaref a continuación, quien señaló algunos problemas de prueba y, de hecho, después de seguir sus sugerencias, los tiempos son menores, lo que indica que el tiempo de configuración de VM fue significativo en Java, pero probablemente no en C #. En C #, fueron los símbolos de depuración los que fueron significativos.
Actualicé mi código para ejecutar cada bucle 10,000 veces, y solo imprimí el promedio de ms al final. El único cambio significativo que hice fue a la versión C # donde cambié a la [clase StopWatch] [3] para una mayor resolución. Me quedé con milisegundos porque es lo suficientemente bueno.
Resultados:
Los cambios de prueba no explican por qué Java es (aún) mucho más rápido que C #. El rendimiento de C # fue mejor, pero esto se puede explicar completamente eliminando los símbolos de depuración. Si lees [Mike Two] [4] y el intercambio de I en los comentarios adjuntos a este OP, verás que obtuve un promedio de ~ 280 ms en cinco ejecuciones del código C # simplemente cambiando de Debug a Release.
Números:
- Un bucle de 10,000 cuentas del código Java no modificado me dio un promedio de 45 ms (menos de 55 ms)
- Un bucle de 10,000 cuentas del código C # usando la clase StopWatch me dio un promedio de 282 ms (menos de 320 ms)
Todo esto deja la diferencia sin explicación. De hecho, el diferencial empeoró. Java pasó de ser ~ 5.8x más rápido a ~ 6.2x más rápido.
En Java usaría System.nanoTime (). Cualquier prueba que demore menos de 2 segundos debe durar más tiempo. Vale la pena señalar que Java es bastante bueno optimizando el código ineficiente o el código que no hace nada. Una prueba más interesante sería si optimizas el código.
Está intentando obtener una solución que puede determinar sin utilizar un bucle. Es decir, un problema que se haría mejor de otra manera.
Usted quiere el producto de los factores de 11 a 20, que son 2,2,2,2,3,3,5,7,11,13,17,19. Multiplica estos juntos y tendrás la respuesta.
Esta es una tarea demasiado corta para hacer el tiempo adecuado. Debe ejecutar ambos al menos 1000 veces y ver qué sucede. Parece que estás ejecutando estos desde una línea de comando, en cuyo caso posiblemente estés comparando los compiladores JIT para ambos. Intente colocar ambos botones detrás en una GUI simple y haga que el botón se repita unos cientos de veces al menos antes de devolver el tiempo transcurrido. Incluso ignorando la compilación JIT, el tiempo podría ser desviado por la granularidad del programador del sistema operativo.
Ah, y debido a JIT ... solo cuenta el SEGUNDO resultado de presionar un botón. :)
Hay algunas optimizaciones posibles. Tal vez el JIT de Java los esté realizando y el CLR no.
Optimización # 1:
(x % a == 0) && (x % b == 0) && ... && (x % z == 0)
es equivalente a
(x % lcm(a, b, ... z) == 0)
Así que en tu ejemplo la cadena de comparación podría ser reemplazada por
if (i % 232792560 == 0) break;
(pero, por supuesto, si ya ha calculado el LCM, ¡no tiene mucho sentido ejecutar el programa en primer lugar!)
Optimización # 2 :
Esto también es equivalente:
if (i % (14549535 * 16)) == 0 break;
o
if ((i % 16 == 0) && (i % 14549535 == 0)) break;
La primera división se puede reemplazar con una máscara y comparar con cero:
if (((i & 15) == 0) && (i % 14549535 == 0)) break;
La segunda división puede ser reemplazada por una multiplicación por el inverso modular:
final long LCM = 14549535;
final long INV_LCM = 8384559098224769503L; // == 14549535**-1 mod 2**64
final long MAX_QUOTIENT = Long.MAX_VALUE / LCM;
// ...
if (((i & 15) == 0) &&
(0 <= (i>>4) * INV_LCM) &&
((i>>4) * INV_LCM < MAX_QUOTIENT)) {
break;
}
Es algo improbable que el JIT emplee esto, pero no es tan inverosímil como podría pensar, algunos compiladores de C implementan la sustracción de punteros de esta manera.
La clave para hacer que estos dos se acerquen es asegurarse de que la comparación sea justa.
En primer lugar, asegúrese de que los costos asociados con la ejecución de compilaciones de depuración, cargando los símbolos pdb como lo hizo.
A continuación, debe asegurarse de que no se están contabilizando los costos de inicio. Obviamente, estos son costos reales y pueden ser importantes para algunas personas, pero en este caso estamos interesados en el bucle en sí.
A continuación debe tratar el comportamiento específico de la plataforma. Si está en una máquina Windows de 64 bits, puede estar ejecutando en modo de 32 bits o de 64 bits. En el modo de 64 bits, el JIT es diferente en muchos aspectos, a menudo altera considerablemente el código resultante. Específicamente, y creo que sería pertinente, usted obtiene acceso al doble de registros de propósito general.
En este caso, la sección interna del bucle, cuando se traduzca ingenuamente en código de máquina, deberá cargar en los registros las constantes utilizadas en las pruebas de módulo. Si no hay suficientes para mantener todo lo necesario en el bucle, debe empujarlos desde la memoria. Incluso viniendo de la caché de nivel 1, esto sería un gran éxito en comparación con mantenerlo todo en los registros.
En VS 2010 MS cambió el destino predeterminado de anycpu a x86 . No tengo nada como los recursos o el conocimiento del cliente de MSFT, por lo que no intentaré adivinarlo. Sin embargo, cualquiera que busque algo como el análisis de rendimiento que está realizando debe probar ambos.
Una vez que esas disparidades se resuelven, los números parecen mucho más razonables. Cualquier diferencia adicional probablemente requiera más que suposiciones acertadas, en lugar de eso, necesitarían una investigación sobre las diferencias reales en el código de máquina generado.
Hay varias cosas sobre esto que creo que serían interesantes para un compilador optimizado.
- Los que ya hemos mencionado:
- La opción mcm es interesante pero no puedo ver a un escritor compilador molestando.
- La reducción de la división a la multiplicación y el enmascaramiento.
- No sé lo suficiente sobre esto, pero otras personas han intentado observar que mencionan el divisor de los chips de Intel más recientes significativamente mejor.
- Tal vez incluso puedas arreglar algo complejo, con SSE2.
- Ciertamente, la operación de módulo 16 está madura para la conversión en una máscara o cambio.
- Un compilador podría detectar que ninguna de las pruebas tiene efectos secundarios.
- se podría tratar especulativamente de evaluar varios de ellos a la vez; en un procesador súper escalar, esto podría impulsar las cosas un poco más rápido, pero dependería en gran medida de qué tan bien interactuara el diseño de los compiladores con el motor de ejecución OO.
- Si la presión del registro era estricta, podría implementar las constantes como una sola variable, establecer al inicio de cada bucle y luego aumentar a medida que avanza.
Estas son todas conjeturas, y deben verse como los meandros ociosos. Si quieres saber desmontarlo.
Quizás porque la construcción de los objetos DateTime
es mucho más costosa que System.currentTimeMillis
.