Descargo de responsabilidad
Sé que los puntos de referencia artificiales son malos. Pueden mostrar resultados solo para situaciones estrechas muy específicas. No asumo que un idioma sea mejor que el otro debido al estúpido banco. Sin embargo, me pregunto por qué los resultados son tan diferentes. Por favor vea mis preguntas al final.
Descripción de la evaluación comparativa de matemáticas
El punto de referencia son cálculos matemáticos simples para encontrar pares de números primos que difieren en 6 (los llamados números primos sexy ). Por ejemplo, los números primos sexy por debajo de 100 serían:(5 11) (7 13) (11 17) (13 19) (17 23) (23 29) (31 37) (37 43) (41 47) (47 53) (53 59) (61 67) (67 73) (73 79) (83 89) (97 103)
Tabla de resultados
En la tabla: tiempo de cálculo en segundos En ejecución: todos excepto Factor se estaban ejecutando en VirtualBox (invitado amd64 inestable de Debian, host Windows 7 x64) CPU: AMD A4-3305M
Sexy primes up to: 10k 20k 30k 100k
Bash 58.00 200.00 [*1] [*1]
C 0.20 0.65 1.42 15.00
Clojure1.4 4.12 8.32 16.00 137.93
Clojure1.4 (optimized) 0.95 1.82 2.30 16.00
Factor n/a n/a 15.00 180.00
Python2.7 1.49 5.20 11.00 119
Ruby1.8 5.10 18.32 40.48 377.00
Ruby1.9.3 1.36 5.73 10.48 106.00
Scala2.9.2 0.93 1.41 2.73 20.84
Scala2.9.2 (optimized) 0.32 0.79 1.46 12.01
[* 1] - Me da miedo imaginar cuánto tiempo tomará
Listados de código
C:
int isprime(int x) {
int i;
for (i = 2; i < x; ++i)
if (x%i == 0) return 0;
return 1;
}
void findprimes(int m) {
int i;
for ( i = 11; i < m; ++i)
if (isprime(i) && isprime(i-6))
printf("%d %d\n", i-6, i);
}
main() {
findprimes(10*1000);
}
Rubí:
def is_prime?(n)
(2...n).all?{|m| n%m != 0 }
end
def sexy_primes(x)
(9..x).map do |i|
[i-6, i]
end.select do |j|
j.all?{|j| is_prime? j}
end
end
a = Time.now
p sexy_primes(10*1000)
b = Time.now
puts "#{(b-a)*1000} mils"
Scala:
def isPrime(n: Int) =
(2 until n) forall { n % _ != 0 }
def sexyPrimes(n: Int) =
(11 to n) map { i => List(i-6, i) } filter { _ forall(isPrime(_)) }
val a = System.currentTimeMillis()
println(sexyPrimes(100*1000))
val b = System.currentTimeMillis()
println((b-a).toString + " mils")
Scala opimized isPrime
(la misma idea que en la optimización de Clojure):
import scala.annotation.tailrec
@tailrec // Not required, but will warn if optimization doesn't work
def isPrime(n: Int, i: Int = 2): Boolean =
if (i == n) true
else if (n % i != 0) isPrime(n, i + 1)
else false
Clojure:
(defn is-prime? [n]
(every? #(> (mod n %) 0)
(range 2 n)))
(defn sexy-primes [m]
(for [x (range 11 (inc m))
:let [z (list (- x 6) x)]
:when (every? #(is-prime? %) z)]
z))
(let [a (System/currentTimeMillis)]
(println (sexy-primes (* 10 1000)))
(let [b (System/currentTimeMillis)]
(println (- b a) "mils")))
Clojure optimizado is-prime?
:
(defn ^:static is-prime? [^long n]
(loop [i (long 2)]
(if (= (rem n i) 0)
false
(if (>= (inc i) n) true (recur (inc i))))))
Pitón
import time as time_
def is_prime(n):
return all((n%j > 0) for j in xrange(2, n))
def primes_below(x):
return [[j-6, j] for j in xrange(9, x+1) if is_prime(j) and is_prime(j-6)]
a = int(round(time_.time() * 1000))
print(primes_below(10*1000))
b = int(round(time_.time() * 1000))
print(str((b-a)) + " mils")
Factor
MEMO:: prime? ( n -- ? )
n 1 - 2 [a,b] [ n swap mod 0 > ] all? ;
MEMO: sexyprimes ( n n -- r r )
[a,b] [ prime? ] filter [ 6 + ] map [ prime? ] filter dup [ 6 - ] map ;
5 10 1000 * sexyprimes . .
Bash (zsh):
#!/usr/bin/zsh
function prime {
for (( i = 2; i < $1; i++ )); do
if [[ $[$1%i] == 0 ]]; then
echo 1
exit
fi
done
echo 0
}
function sexy-primes {
for (( i = 9; i <= $1; i++ )); do
j=$[i-6]
if [[ $(prime $i) == 0 && $(prime $j) == 0 ]]; then
echo $j $i
fi
done
}
sexy-primes 10000
Preguntas
- ¿Por qué Scala es tan rápido? ¿Es por escritura estática ? ¿O simplemente está usando JVM de manera muy eficiente?
¿Por qué hay una diferencia tan grande entre Ruby y Python? Pensé que estos dos no son totalmente diferentes. Quizás mi código esté equivocado. ¡Por favor iluminame! Gracias.UPD Sí, eso fue un error en mi código. Python y Ruby 1.9 son bastante iguales.- Realmente impresionante salto en productividad entre las versiones de Ruby.
- ¿Puedo optimizar el código de Clojure agregando declaraciones de tipo? ¿Ayudará?
sqrt(n)
pero eso puede llevar algún tiempo calcular. Además, su código C imprime los números primos a medida que los encuentra, mientras que sus otros lenguajes los calculan en listas y luego los imprimen. Si bien C es, como era de esperar, el más rápido, es posible que pueda obtenerlo más rápido.C: 2.723s
Go: 2.743s
.sqrt
para esta verificación. Puede calcular el cuadrado dei
como enfor (i = 2; i * i <= x; ++i) ...
isPrime
con@tailrec
, para asegurarse de que está utilizando la recursividad de cola. Es fácil hacer algo por error que evite la recursividad de la cola, y esta anotación debería advertirle si eso sucede.Respuestas:
Respuestas aproximadas:
(2...n).all?
la funciónis-prime?
esté bastante bien optimizada en Ruby (EDITAR: parece que este es el caso, consulte la respuesta de Julian para obtener más detalles ...)La optimización más importante en el código de Clojure sería usar matemáticas primitivas escritas dentro
is-prime?
, algo como:Con esta mejora, obtengo que Clojure complete 10k en 0.635 segundos (es decir, el segundo más rápido en su lista, superando a Scala)
PD: tenga en cuenta que tiene código de impresión dentro de su punto de referencia en algunos casos, no es una buena idea ya que distorsionará los resultados, especialmente si el uso de una función como
print
la primera causa la inicialización de los subsistemas IO o algo así.fuente
is-prime?
muestra una mejora del doble. ;)(zero? (mod n i))
debería ser más rápido que(= (mod n i) 0)
Aquí hay una versión rápida de Clojure, usando los mismos algoritmos básicos:
Funciona unas 20 veces más rápido que el original en mi máquina. Y aquí hay una versión que aprovecha la nueva biblioteca de reductores en 1.5 (requiere Java 7 o JSR 166):
Funciona unas 40 veces más rápido que el original. En mi máquina, son 100k en 1,5 segundos.
fuente
unchecked-remainder-int
o simplemente enrem
lugar demod
junto con los resultados de escritura estática para aumentar el rendimiento 4 veces. ¡Agradable!Responderé solo el n. ° 2, ya que es el único que tengo algo remotamente inteligente que decir, pero para su código Python, está creando una lista intermedia en
is_prime
, mientras que está usando.map
en suall
Ruby, que es solo iterando.Si cambia su
is_prime
a:están a la par.
Podría optimizar aún más Python, pero mi Ruby no es lo suficientemente bueno para saber cuándo he dado más ventaja (por ejemplo, usar
xrange
hace que Python gane en mi máquina, pero no recuerdo si el rango de Ruby que usaste crea un rango completo en la memoria o no).EDITAR: Sin ser demasiado tonto, hacer que el código de Python se vea así:
lo que no cambia mucho más, lo pone en 1,5 segundos para mí y, siendo más tonto, ejecutarlo con PyPy lo pone en 0,3 segundos para 10K y 21 segundos para 100K.
fuente
False
(buena captura).xrange
! Lo he arreglado y ahora Python y Ruby muestran resultados iguales.lru_cache
implementación aleatoria para 2.7 en AS, 100K se ejecuta en 2.3s.Puede hacer que Scala sea mucho más rápido modificando su
isPrime
método paraNo es tan conciso, pero el programa se ejecuta en el 40% del tiempo.
Recortamos
Range
losFunction
objetos superfluos y anónimos , el compilador de Scala reconoce la recursividad de la cola y la convierte en un bucle while, que la JVM puede convertir en un código máquina más o menos óptimo, por lo que no debería estar demasiado lejos de C versión.Consulte también: ¿Cómo optimizar las comprensiones y los bucles en Scala?
fuente
i == n || n % i != 0 && isPrime(n, i + 1)
, que es más corto, aunque un poco más difícil de leer@tailrec
anotación para asegurarse de que hará esa optimización.Aquí está mi versión scala en paralelo y no paralelo, solo por diversión: (en mi computación de doble núcleo, la versión paralela toma 335ms mientras que la versión no paralela toma 655ms)
EDITAR: De acuerdo con la sugerencia de Emil H , he cambiado mi código para evitar los efectos del calentamiento de IO y jvm:
El resultado se muestra en mi cálculo:
fuente
isSexyPrime
podría estar (más) optimizado cuando se llama desdefindPrimesPar
y no tanto cuando se llama desdefindPrimes
No importa los puntos de referencia; el problema me interesó e hice algunos ajustes rápidos. Utiliza el
lru_cache
decorador, que memoriza una función; así que cuando llamamosis_prime(i-6)
, básicamente obtenemos ese cheque principal gratis. Este cambio reduce el trabajo aproximadamente a la mitad. Además, podemos hacer que lasrange()
llamadas pasen por los números impares, reduciendo el trabajo aproximadamente a la mitad nuevamente.http://en.wikipedia.org/wiki/Memoization
http://docs.python.org/dev/library/functools.html
Esto requiere Python 3.2 o más reciente para obtenerlo
lru_cache
, pero podría funcionar con un Python más antiguo si instala una receta de Python que proporcionelru_cache
. Si está usando Python 2.x, realmente debería usar enxrange()
lugar derange()
.http://code.activestate.com/recipes/577479-simple-caching-decorator/
Lo anterior solo tomó muy poco tiempo para editarse. Decidí dar un paso más y hacer que la prueba de primos solo pruebe con divisores primos, y solo hasta la raíz cuadrada del número que se está probando. La forma en que lo hice solo funciona si verifica los números en orden, para que pueda acumular todos los números primos a medida que avanza; pero este problema ya estaba comprobando los números en orden, así que estaba bien.
En mi computadora portátil (nada especial; el procesador es un AMD Turion II "K625" de 1.5 GHz) esta versión produjo una respuesta de 100K en menos de 8 segundos.
El código anterior es bastante fácil de escribir en Python, Ruby, etc., pero sería más complicado en C.
No puede comparar los números de esta versión con los números de las otras versiones sin volver a escribir las otras para usar trucos similares. No estoy tratando de probar nada aquí; Simplemente pensé que el problema era divertido y quería ver qué tipo de mejoras de rendimiento fáciles podía obtener.
fuente
lru_cache
es definitivamente ingenioso. Para ciertas clases de problemas, como la generación de números de Fibonacci sucesivos, ¡puede dar una gran aceleración simplemente agregando ese decorador de una línea en la función! Aquí hay un enlace a una charla de Raymond Hettinger que cubrelru_cache
aproximadamente 26 minutos en. Blip.tv/pycon-us-videos-2009-2010-2011/…lru_cache
evita repetir un cálculo que ya se hizo recientemente, y eso es todo; No veo cómo eso es "realmente usar otro algoritmo". Y Python sufre de ser lento, pero se beneficia de tener cosas interesantes comolru_cache
; No veo nada malo en utilizar las partes beneficiosas de un idioma. Y dije que no se debe comparar el tiempo de ejecución de mi respuesta con los otros idiomas sin hacer cambios similares a los demás. Entonces, no entiendo a qué te refieres.0.03
segundos (30
ms) .¡No te olvides de Fortran! (Mayormente bromeando, pero esperaría un desempeño similar al de C). Las declaraciones con signos de exclamación son opcionales, pero de buen estilo. (
!
es un personaje de comentario en fortran 90)fuente
No pude resistirme a hacer algunas de las optimizaciones más obvias para la versión C que hicieron que la prueba de 100k ahora tomara 0.3s en mi máquina (5 veces más rápido que la versión C en la pregunta, ambas compiladas con MSVC 2010 / Ox) .
Aquí está la implementación idéntica en Java:
Con Java 1.7.0_04, esto se ejecuta casi exactamente tan rápido como la versión C. La máquina virtual cliente o servidor no muestra mucha diferencia, excepto que el entrenamiento JIT parece ayudar un poco a la máquina virtual del servidor (~ 3%) mientras que casi no tiene ningún efecto con la máquina virtual cliente. La salida en Java parece ser más lenta que en C. Si la salida se reemplaza con un contador estático en ambas versiones, la versión de Java se ejecuta un poco más rápido que la versión C.
Estos son mis tiempos para la carrera de 100k:
y la carrera de 1 M (16386 resultados):
Si bien esto no responde realmente a sus preguntas, muestra que los pequeños ajustes pueden tener un impacto notable en el rendimiento. Entonces, para poder comparar realmente idiomas, debe intentar evitar todas las diferencias algorítmicas tanto como sea posible.
También da una pista de por qué Scala parece bastante rápido. Se ejecuta en Java VM y, por lo tanto, se beneficia de su impresionante rendimiento.
fuente
En Scala, intente usar Tuple2 en lugar de List, debería ir más rápido. Simplemente elimine la palabra 'Lista' ya que (x, y) es un Tuple2.
Tuple2 está especializado para Int, Long y Double, lo que significa que no tendrá que empaquetar / desempaquetar esos tipos de datos sin procesar. Fuente Tuple2 . List no está especializado. Fuente de la lista .
fuente
forall
a él. También pensé que este podría no ser el código más eficiente (más porque se crea una gran colección estricta para grandes enn
lugar de solo usar una vista), pero ciertamente es corto + elegante, y me sorprendió lo bien que funcionó a pesar de usar un mucho estilo funcional.def sexyPrimes(n: Int) = (11 to n).map(i => (i-6, i)).filter({ case (i, j) => isPrime(i) && isPrime(j) })
y es aproximadamente un 60% más rápido aquí, por lo que debería superar el código C :)collect
sustancialmente más lento. Más rápido es si primero hace el filtro y luego el mapa.withFilter
es un poco más rápido porque en realidad no crea colecciones intermedias.(11 to n) withFilter (i => isPrime(i - 6) && isPrime(i)) map (i => (i - 6, i))
Aquí está el código para la versión de Go (golang.org):
Funcionó tan rápido como la versión C.
Usando un Asus u81a Intel Core 2 Duo T6500 2.1GHz, 2MB de caché L2, 800MHz FSB. 4 GB de RAM
La versión 100k:
C: 2.723s
Go: 2.743s
Con 1000000 (1 M en lugar de 100 K):
C: 3m35.458s
Go: 3m36.259s
Pero creo que sería justo usar las capacidades integradas de subprocesos múltiples de Go y comparar esa versión con la versión C normal (sin subprocesos múltiples), solo porque es casi demasiado fácil realizar subprocesos múltiples con Go.
Actualización: hice una versión paralela usando Goroutines en Go:
La versión paralelizada usó un promedio de 2.743 segundos, exactamente el mismo tiempo que usó la versión regular.La versión paralelizada se completó en 1.706 segundos. Utilizaba menos de 1,5 Mb de RAM.
Una cosa extraña: mi kubuntu de 64 bits de doble núcleo nunca alcanzó su punto máximo en ambos núcleos. Parecía que Go estaba usando solo un núcleo.Fijo con una llamada aruntime.GOMAXPROCS(4)
Actualización: Ejecuté la versión paralelizada hasta 1 millón de números.
Uno de los núcleos de Mi CPU estuvo al 100% todo el tiempo, mientras que el otro no se usó en absoluto (extraño). Tomó un minuto más que las versiones C y Go normales. :(Con 1000000 (1 M en lugar de 100 K):
C: 3m35.458s
Go: 3m36.259s
Go using goroutines:
3 min 27,137 s2m16.125s
La versión 100k:
C: 2.723s
Go: 2.743s
Go using goroutines: 1.706s
fuente
-O3
o mejor.Solo por el gusto de hacerlo, aquí hay una versión paralela de Ruby.
En mi MacBook Air Core i5 de 1.8GHz, los resultados de rendimiento son:
Parece que el JIT de JVM le está dando a Ruby un buen aumento de rendimiento en el caso predeterminado, mientras que el verdadero multiproceso ayuda a JRuby a funcionar un 50% más rápido en el caso de subprocesos. ¡Lo que es más interesante es que JRuby 1.7 mejora la puntuación de JRuby 1.6 en un saludable 17%!
fuente
Basado en la respuesta de x4u , escribí una versión de scala usando recursividad, y la mejoré yendo solo a sqrt en lugar de x / 2 para la función de verificación principal. Obtengo ~ 250ms por 100k y ~ 600ms por 1M. Seguí adelante y pasé a 10M en ~ 6s.
También volví y escribí una versión de CoffeeScript (V8 JavaScript), que obtiene ~ 15ms para 100k, 250ms para 1M y 6s para 10M, usando un contador (ignorando E / S). Si enciendo la salida, toma ~ 150ms para 100k, 1s para 1M y 12s para 10M. No se pudo usar la recursividad de cola aquí, desafortunadamente, así que tuve que convertirlo nuevamente en bucles.
fuente
La respuesta a su pregunta n. ° 1 es que Sí, la JVM es increíblemente rápida y sí, la escritura estática ayuda.
La JVM debería ser más rápida que C a largo plazo, posiblemente incluso más rápida que el lenguaje ensamblador "Normal". Por supuesto, siempre puede optimizar manualmente el ensamblaje para vencer cualquier cosa haciendo un perfil de tiempo de ejecución manual y creando una versión separada para cada CPU, simplemente tiene que ser increíblemente bueno y conocedor.
Las razones de la velocidad de Java son:
La JVM puede analizar su código mientras se ejecuta y optimizarlo manualmente, por ejemplo, si tuviera un método que pudiera analizarse estáticamente en el momento de la compilación para ser una función verdadera y la JVM notó que a menudo lo llamaba con el mismo parámetros, en realidad PODRÍA eliminar la llamada por completo y simplemente inyectar los resultados de la última llamada (no estoy seguro de si Java realmente hace esto exactamente, pero hace muchas cosas como esta).
Debido a la escritura estática, la JVM puede saber mucho sobre su código en tiempo de compilación, esto le permite optimizar previamente algunas cosas. También permite que el compilador optimice cada clase individualmente sin saber cómo otra clase planea usarla. Además, Java no tiene punteros arbitrarios a la ubicación de la memoria, SABE qué valores en la memoria pueden y no pueden cambiarse y puede optimizar en consecuencia.
La asignación de pila es MUCHO más eficiente que C, la asignación de pila de Java se parece más a la asignación de pila de C en velocidad, pero más versátil. Se ha dedicado mucho tiempo a los diferentes algoritmos utilizados aquí, es un arte; por ejemplo, todos los objetos con una vida útil corta (como las variables de pila de C) se asignan a una ubicación libre "conocida" (sin buscar un lugar libre con suficiente espacio) y se liberan todos juntos en un solo paso (como una pila emergente).
La JVM puede conocer las peculiaridades de la arquitectura de su CPU y generar código de máquina específicamente para una CPU determinada.
La JVM puede acelerar su código mucho después de que lo envió. Al igual que mover un programa a una nueva CPU puede acelerarlo, moverlo a una nueva versión de la JVM también puede brindarle rendimientos de gran velocidad adaptados a CPU que ni siquiera existían cuando compiló inicialmente su código, algo que c físicamente no puede prescindir de un recomiple.
Por cierto, la mayor parte de la mala reputación de la velocidad de Java proviene del largo tiempo de inicio para cargar la JVM (¡Algún día alguien integrará la JVM en el sistema operativo y esto desaparecerá!) Y el hecho de que muchos desarrolladores son realmente malos para escribir. Código de GUI (especialmente subprocesado) que causaba que las GUI de Java a menudo dejaran de responder y presentaran fallas. Los lenguajes simples de usar como Java y VB tienen sus fallas amplificadas por el hecho de que las capacidades del programador promedio tienden a ser más bajas que los lenguajes más complicados.
fuente