Este es un seguimiento de ¿Qué tan lento es realmente Python? (¿O qué tan rápido es tu idioma?) .
Resulta que fue un poco demasiado fácil obtener una aceleración x100 para mi última pregunta. Para aquellos que han disfrutado el desafío pero quieren algo más difícil donde realmente pueden usar sus habilidades de bajo nivel, aquí está la parte II. El desafío es obtener una aceleración x100 para el siguiente código de Python según lo probado en mi computadora.
Para hacerlo más difícil, estoy usando pypy esta vez. El tiempo actual para mí es de 1 minuto y 7 segundos usando pypy 2.2.1.
Reglas
- La primera persona en enviar el código que puedo ejecutar, es correcta y es x100 veces más rápido en mi máquina, recibirá una recompensa de 50 puntos.
- Otorgaré la victoria al código más rápido después de una semana.
import itertools
import operator
import random
n = 8
m = 8
iters = 1000
# creates an array of 0s with length m
# [0, 0, 0, 0, 0, 0, 0, 0]
leadingzerocounts = [0]*m
# itertools.product creates an array of all possible combinations of the
# args passed to it.
#
# Ex:
# itertools.product("ABCD", "xy") --> Ax Ay Bx By Cx Cy Dx Dy
# itertools.product("AB", repeat=5) --> [
# ('A', 'A', 'A', 'A', 'A'),
# ('A', 'A', 'A', 'A', 'B'),
# ('A', 'A', 'A', 'B', 'A'),
# ('A', 'A', 'A', 'B', 'B'),
# etc.
# ]
for S in itertools.product([-1,1], repeat = n+m-1):
for i in xrange(iters):
F = [random.choice([-1,0,0,1]) for j in xrange(n)]
# if the array is made up of only zeros keep recreating it until
# there is at least one nonzero value.
while not any(F):
F = [random.choice([-1,0,0,1]) for j in xrange(n)]
j = 0
while (j < m and sum(map(operator.mul, F, S[j:j+n])) == 0):
leadingzerocounts[j] +=1
j += 1
print leadingzerocounts
La salida debe ser similar a
[6335185, 2526840, 1041967, 439735, 193391, 87083, 40635, 19694]
Debe usar una semilla aleatoria en su código y se aceptará cualquier generador de números aleatorios que sea lo suficientemente bueno como para dar respuestas cercanas a las anteriores.
Mi máquina Los tiempos se ejecutarán en mi máquina. Esta es una instalación estándar de ubuntu en un procesador AMD FX-8350 de ocho núcleos. Esto también significa que necesito poder ejecutar su código.
Explicación de código
Este código itera sobre todas las matrices S de longitud n + m-1 que están formadas por -1s y 1s. Para cada matriz S, muestra 1000 matrices aleatorias distintas de cero F de longitud n formadas por -1,0 o 1 con probabilidad de 1/4, 1/2, / 14 de tomar cada valor. Luego calcula los productos internos entre F y cada ventana de S de longitud n hasta que encuentra un producto interno distinto de cero. Agrega 1 a leadingzerocounts
cada posición donde encontró un producto interno cero.
Estado
Perl . 2.7 veces la ralentización por @tobyink. (En comparación con pypy no cpython).
J . 39 veces más velocidad por @Eelvex.
- C . 59 veces acelerado por @ace.
- Julia . 197 veces más rápido sin incluir el tiempo de inicio en un minuto más. 8.5 veces más rápido, incluido el tiempo de inicio (en este caso, es más rápido usar 4 procesadores que 8).
- Fortran . 438 veces más rápido por @ semi-extrínseco.
- Rpython . 258 veces acelerado por @primo.
- C ++ . 508 veces acelerado por @ilmale.
(Dejé de cronometrar las nuevas mejoras porque son demasiado rápidas e iters era demasiado pequeño).
Se señaló que los tiempos por debajo de un segundo no son confiables y también algunos idiomas tienen un costo inicial. El argumento es que si va a incluir eso, también debe incluir el tiempo de compilación de C / C ++, etc. Aquí están los tiempos para el código más rápido con el número de iteraciones aumentado a 100,000.
- Julia . 42 segundos por @ un minuto más.
- C ++ . 14 segundos por @GuySirton.
- Fortran . 14s por @ semi-extrinsic.
- C ++ . 12s por @ilmale.
- Rpython . 18s por @primo.
- C ++ . 5s por @Stefan.
El ganador es ... ¡Stefan!
Reto de seguimiento publicado. ¿Qué tan alto puedes llegar? (Un desafío de codificación + algoritmos) . Este es más difícil.
fuente
Respuestas:
C ++ bit magic
~ 16 ms multiproceso, 56 ms de subproceso único. ~ 4000 aceleración.
(la aceleración se basa en un código multiproceso en mi i7-2820QM y los 1 min 9 segundos mencionados en la pregunta. Dado que el sistema OP tiene peor rendimiento de un solo subproceso que mi CPU pero mejor rendimiento de subprocesos múltiples, espero que este número sea preciso)
La parte multiproceso es bastante ineficiente debido al desove de los hilos. Probablemente podría hacerlo mejor aprovechando mi biblioteca de trabajos personalizada, pero esa tiene errores en sistemas Unix. Para obtener una explicación y un código casi idéntico sin subprocesos, consulte https://codegolf.stackexchange.com/a/26485/20965 .
editar
Le di a cada hilo su propio RNG y reduje la longitud de bits a 32, lo que redujo el tiempo de ejecución en unos pocos ms.
Salida de muestra:
fuente
C ++
x150x450x530En lugar de matriz, utilicé bits (y magia oscura).
Gracias @ace por la función aleatoria más rápida.
Cómo funciona: los primeros 15 bits del número entero
s
representan la matrizS[15]
; los ceros representan -1, los que representan +1. La matrizF
se construye de manera similar. Pero con dos bits para cada símbolo.Causar
S
yF
tener una representación diferente que tengo que intercalarS
consigo mismo para ser comparable conF
.F
)F
)Ahora podemos simplemente usar Carnot para calcular el producto interno. Recuerde que una variable solo puede asumir el valor 00 u 11
0. 00 = 11 (-1 * -1 = +1)
0. 01 = 10 (-1 * 0 = 0)
0. 10 = 01 (-1 * 0 = 0)
0. 11 = 00 (-1 * +1 = -1)
1. 00 = 00 (+1 * -1 = -1)
1. 10 = 10 (+1 * 0 = 0)
1. 01 = 01 (+1 * 0 = 0)
1. 11 = 11 (+1 * +1 = +1)
Parece un no xor para mí. :)
Sum the ones es solo un juego de cambio y máscara, nada realmente complejo.
Aquí una salida de muestra:
El programa ha sido compilado con:
en Fedora 20 con gcc 4.8.2 La CPU es un i7 8core.
Probablemente pueda obtener algunos parámetros de compilación de ajuste de ms.
Si bien este es el tiempo de solución OP en mi máquina:
Editar:
Simplemente agregando openmp y cambiar el orden de la para tengo una ganancia x3, lo que lleva a una mejora del rendimiento x450 frente al código OP. : D En este caso, la
leadingZero
matriz debe ser atómica. Los globales aleatorios ... son aleatorios, serán más aleatorios.necesita agregar
-fopenmp
a la bandera del compiladorEditar: 2 Como sugerente por el usuario71404 Cambié las funciones sumOnes y sumArray y ahora es súper rápido.
Con openmp es más lento, porque los atómicos agregan demasiada sobrecarga.
Sin atómica es aún más rápido, pero obtengo un resultado incorrecto.
2137992 1147218 619297 321243 155815 70946 32919 15579
Para comprender sumArray, considere que 16 bits representan y una matriz de 8 números.
00 no tiene 1 y representa -1
01 y 10 tiene un 1 y representa 0
11 tiene dos 1s y representa 1
Para que el recuento incorporado el número de bits establecido en 1 [ http://en.wikipedia.org/wiki/ Hamming_weight] y a cada grupo eliminamos 1. Cool.
sumOnes es solo magia negra.
Aquí las últimas compilaciones de banderas y códigos.
gcc -std = c ++ 11 -mfpmath = sse -O3 -flto -march = native -funroll-loops -Wall -lstdc ++
fuente
inline int32_t sumOnes(int32_t v) { /* 0xAAAA == 0b1010 1010 1010 1010 */ return !! (0xAAAA & (v ^ ~(v << 1))); } inline int32_t sumArray(int32_t v) { return __builtin_popcount(v) - 8; }
esto fue sugerido por @ user71404Julia: 0.7s, 120x más rápido
Como demostró el usuario 20768, un puerto directo del código para Julia es aproximadamente el doble de rápido que PyPy. Pero podemos hacer mucho mejor que eso.
Puede ejecutar esto usando
julia -p 8 -e 'require("golf.jl");main()'
(el 8 es el número de procesos, es posible que desee jugar con él). En la última versión preliminar de Julia, esto toma 0.7s vs. 1m22s para PyPy.Si tiene suficientes núcleos en su computadora y tal vez active algunas instancias de AWS, debería poder afeitarse un poco más :)
fuente
C, 1.210s
Con el código de OP corriendo 1m45.729s en mi máquina.
Compilacion:
Un agradecimiento especial: @dyp para compilar banderas e ideas para optimizaciones.
Salida de muestra:
fuente
-march=native -fwhole-program -fstrict-aliasing -ftree-vectorize
cierto. Llegué a <4 s usando algunos C ++ 11, incluido un MT19937 más auniform_int_distribution
.F
.n
es igual a8
, probablemente pueda usar AVX (o 2 * SSE) para calcular el producto dot con unS
almacenamiento adecuado .smmintrin.h
)Perl
Esto no es tan rápido como la solución C, pero creo que es bastante rápido para un lenguaje interpretado de alto nivel. Reduce aproximadamente el 40% del tiempo de ejecución de la implementación de Python.
El algoritmo :: Combinatoria está disponible en Ubuntu (
sudo apt-get install libalgorithm-combinatorics-perl
). Los otros módulos utilizados son módulos centrales de Perl, por lo que ya deberían estar instalados como parte de la instalación básica de Ubuntu.fuente
0..N-1
rango en el últimomap
, ¿verdad? ¿Se te olvidóuse warnings
? :-) Aunque la lógica en OP es confusa, la ventana deslizante nunca llega al último elemento deS
.warnings
permitiendo que los elementos faltantes se trataran como cero.N-1
mejora esto Y en realidad mejora la velocidad muy ligeramente: ahora es aproximadamente un 40% más rápido que la implementación de Python.any
alternativamente se puede encontrar en List :: MoreUtils, que aunque no es un módulo central, es uno de los módulos CPAN más utilizados.Julia: ¡4.66 veces más lento!
Realmente estoy empezando a dudar de las estadísticas en su sitio web ...
Tenga en cuenta que el siguiente código de Julia es efectivamente una transcripción directa del código de Python del OP sin ninguna optimización. Uso la
time()
función para excluir el tiempo de inicio lento de Julia ...Julia: 5 m 32.912 s
Código de OP en PyPy: 1 m 11.506 s
Salida de Julia:
fuente
RPython 0.187s (258x más rápido)
Fuente original con PyPy2.2.1: 1m 6.718s
Ahora con subprocesos, se ha eliminado el respaldo para Python estándar. El número de subprocesos de trabajo se puede especificar como un parámetro de línea de comando, el valor predeterminado es dos.
RPython es un subconjunto restringido de Python, que puede traducirse a C y luego compilarse utilizando la cadena de herramientas RPython . Su propósito expreso es ayudar en la creación de intérpretes de idiomas, pero también se puede utilizar para compilar programas simples como el anterior. La mayoría de las características 'más elegantes' de Python, como
itertools
o inclusomap
no están disponibles.Para compilar, haga un clon local del repositorio actual de pypy y ejecute lo siguiente:
El ejecutable resultante se llamará
convolution-c
o similar en el directorio de trabajo actual.He parametrizado las variables de entrada, por lo que el programa debe ejecutarse como:
para que coincida con el código de muestra.
Notas de implementacion
S in itertools.product([-1,1], repeat = n+m-1)
se convierteS in xrange(1<<n+m-1)
, interpretandoS
como un mapa de bits: [0
,1
] → [-1
,1
]Del mismo modo,
F
es también un mapa de bits, con cada dos bits que representan un único valor:[
00
,01
,10
,11
] → [-1
,0
,0
,1
]Se utiliza una tabla de verdad para buscar el producto, en lugar de realizar una aplicación múltiple.
Debido a que se utilizan enteros con signo de 32 bits, no
n
pueden ser mayores que 15n+m
ni mayores que 31. Se puede lograr un soporte arbitrario de enteros conrpython.rlib.rbigint
Si es necesario, módulo.La primera iteración del bucle dot-producto se desenrolla, y se combina con la prueba de nulidad de
F
.Se utiliza un PRNG casero, la fuente está en la lista. El autor del artículo demuestra un período de 2 32 -1, y afirma que pasa todas las pruebas de Diehard, excepto una, aunque personalmente no lo he confirmado.
La semilla aleatoria cambia cada milisegundo, lo que es tan bueno como lo permita el uso de una marca de tiempo. Además, cada subproceso de trabajo envía
xor
su identificación de proceso con este valor, para garantizar que cada uno tenga una semilla diferente.Tiempos de muestra
2 hilos de trabajo:
4 hilos de trabajo:
8 hilos de trabajo:
Fuente original de OP:
Tiempo para 100000 iteraciones:
fuente
Julia: 1 min 21.4s (2.2x más rápido) (modificación del código de Arman)
Código de operación en PyPy: 3 min 1.4 s
Ambos hechos en el REPL, sin incluir el tiempo para cargar paquetes.
Hay algunos problemas con el código de Arman que lo hace muy lento: utiliza muchas funciones anónimas y funciones de orden superior innecesariamente. Para probar si todo un vector F es cero, ¿por qué no escribir todo (F == 0) en lugar de todo (x-> x == 0, F)? Es más corto y literalmente mil veces más rápido.
También usa sum (map (*, x, y)) como producto de punto en lugar de simplemente punto (x, y). La primera versión es 650 veces más lenta para un vector de 10k dobles. Y la función del producto punto se implementa como un bucle for en Julia pura.
Además, las comprensiones de matriz son lentas. Es mejor escribir [0,1,0, -1] [rand (1: 4, n)] en lugar de [[-1 0 0 1] [rand (1: 4)] para j = 1: n] .
Finalmente, las variables globales son malas juju en Julia. Julia solo es rápida si escribe código de tal manera que permita que el JIT y la inferencia de tipos funcionen. Una gran parte de esto es la estabilidad de tipo: el compilador debe poder asegurarse de que el tipo de una variable no cambiará dentro de un ciclo, por ejemplo.
fuente
Nimrod
Salida de ejemplo:
Nimrod compila a C, por lo tanto, la elección del compilador de C para el backend también es importante.
Usando clang, compila con:
Usando gcc, compila con:
Omita
--passc:-flto
si tiene un compilador de C más antiguo que no admite LTO. Omita la--cc=...
opción si está de acuerdo con la opción predeterminada para el compilador de C. El código requiere Nimrod 0.9.4 o 0.9.5 .En mi iMac quadcore (2.66 GHz core i5), el código se ejecuta en aproximadamente .15 segundos con gcc 4.9, .16 segundos con clang, en comparación con 88 segundos para PyPy 2.2.1 (es decir, una aceleración de más de 500 veces). Desafortunadamente, no tengo acceso a una máquina con más de cuatro núcleos que también tenga instalado PyPy o donde pueda instalar PyPy fácilmente, aunque obtengo aproximadamente 0,1 segundos (con mucho ruido de medición) en un AMD de 64 núcleos Opteron 6376 1.4 GHz (según / proc / cpuinfo) con gcc 4.4.6.
La implementación intenta ser fiel al código original en lugar de optimizar el código a costa de la legibilidad, sin renunciar a optimizaciones obvias. Curiosamente, la recursión de cola
initVecRand()
es un poco más rápida que un bucle con una instrucción de interrupción con gcc y clang. Desenrollar manualmente una iteración delconvolve
bucle de prueba dentro del bucle principal también produjo una aceleración, presumiblemente debido a una mejor predicción de ramificación.fuente
Java
Traduje la solución C ++ anterior a Java:
En mi máquina obtengo el siguiente resultado para el programa java:
El programa OP se ejecuta unos 53 segundos en mi máquina:
El programa c ++ se ejecutó solo unos 0,15 segundos:
Eso es aproximadamente 2.5 veces más rápido que la solución java correspondiente (no excluí el inicio de VM). Esta solución de Java es aproximadamente 142 veces más rápida que el programa ejecutado con PyPy.
Como estaba personalmente interesado, configuré
iters
100_000 para Java y C ++, pero el factor de 2.5 no disminuyó a favor de Java si algo aumentaba.EDITAR: Ejecuté los programas en una PC Arch Linux de 64 bits.
EDIT2: Quiero agregar que comencé con una traducción aproximada del código de Python:
Este programa ejecutó aproximadamente 3.6 segundos:
Que es aproximadamente 14 veces más rápido que la solución PyPy. (Elegir la función aleatoria estándar sobre la función fastRandom lleva a un tiempo de ejecución de 5 segundos)
fuente
Python 3.5 + numpy 1.10.1, 3.76 segundos
Las pruebas se ejecutaron en mi Macbook Pro. El código de OP tomó ~ 6 minutos en la misma máquina.
La razón por la que estoy respondiendo esta pregunta es porque no tengo 10 reputaciones y no puedo responder la Parte I :-p
Durante los últimos días, he estado tratando de descubrir cómo realizar convoluciones masivas de manera eficiente con numpy (sin depender de un paquete de terceros, incluso scipy). Cuando me encontré con esta serie de desafíos durante mi investigación, decidí intentarlo. Puede que haya llegado a este juego demasiado tarde, pero aquí está mi intento de usar Python 3.5 y numpy 1.10.1.
Precalculé las matrices S y F, y aplané la matriz S mientras realizaba la convolución, que (según mis experimentos) podría aprovechar la velocidad de np.convolve. En otras palabras, como no encontré una rutina de convolución vectorizada, falsifiqué el código vectorizando el conjunto completo y esperé que np.convolved hiciera la vectorización bajo el capó para mí, lo que parecía estar funcionando. Tenga en cuenta que utilicé mode = 'same' y recorté los elementos iniciales y finales que eran inútiles.
En mi Macbook Pro, los resultados de la prueba dan 3,76 segundos . Cuando ejecuté el código de OP (modificado a Python 3.5), obtuve unos 6 minutos . La aceleración es de aproximadamente 100 veces.
Un inconveniente es que debido a que las matrices S y F deben almacenarse, el requisito de memoria puede ser un problema si los tamaños son demasiado grandes.
Utilicé el mismo método para la Parte I y obtuve una aceleración de ~ 60-100x en mi computadora portátil.
Como hice todo en mi Macbook Pro, si alguien pudiera probar mi código y hacerme saber cómo funciona en su máquina, ¡lo agradecería mucho!
fuente
J,
130x~ 50x aceleración?Tiempos en un debian aleatorio:
Creo que hay margen de mejora.
fuente
pypy
, nopython
, razón por la cual su secuencia de comandos parece estar acelerando 130 veces.C ++: x200 (i7 de 4 núcleos, debe escalar a x400 en 8 núcleos)
Intentando una solución C ++ 11 más sencilla (probado con VS 2012, gcc y clang) con paralelización.
Para que esto se compile y se ejecute en Linux con gcc 4.8.1:
Bajo Linux también necesitamos
std::launch::async
forzar múltiples hilos. Me faltaba eso en una versión anterior.En Visual Studio (2012+) esto debería funcionar, pero hacer una versión de lanzamiento para el tiempo ...
En mi viejo i3 dual core esto funciona en ~ 0.9 segundos. En mi i7 quad core, esto es 0.319s frente a pypy 66 segundos.
En un i7 de 8 núcleos, esto debería estar en el rango de aceleración x400. Cambiar a matrices de estilo C lo aceleraría, pero estaba interesado en permanecer con contenedores C ++. Para mí es interesante ver la aceleración que puede obtener mientras se mantiene relativamente cerca del dominio del problema y en un nivel relativamente alto, algo en lo que creo que C ++ es realmente bueno. También es de destacar la relativa facilidad de paralelización utilizando construcciones C ++ 11.
La solución de bits de @ ilmale es muy buena y funciona para -1/1/0. También se podría lanzar SSE a esto y tal vez obtener una aceleración significativa.
Más allá de la paralelización, hay otro "truco" que reduce el número de sumas. Resultados de muestra: 6332947 2525357 1041957 438353 193024 87331 40902 19649
fuente
Fortran: 316x
De acuerdo, Fortran: lo tengo hasta
106x155x160x316x cuando uso un Xorshift RNG y OpenMP en una CPU i7 de 4 núcleos. Aparte de eso, no hay grandes trucos. Para que el iterador construya S, solo uso la representación binaria del entero de 16 bits i. Notarás que, aparte del RNG en línea y el "iterador" / mapeo de i a S, el código es tan de alto nivel como el código de Python.Editar: eliminó el "if" en el Xorshift, ahora usando "r = abs (w / ...)" en lugar de "r = w / ...". Va de 106x a 155x.
Edit2: esto genera 15 veces más números aleatorios que la solución C ++. Si alguien tiene una solución de sobrecarga cero para convertir un int aleatorio en una matriz de 0s y 1s en Fortran, soy todo oídos. Entonces podríamos vencer a C ++ :)
Edit3: La primera edición introdujo un error, como señaló Lembik. Esto se soluciona ahora, con una pequeña mejora en la aceleración. Intentaré usar la sugerencia de Eelvex para obtener más velocidad.
Edit4: el perfil indicó que la conversión a real y de nuevo a entero con nint () fue lenta. Reemplacé esto con una división entera haciendo escala y redondeo, pasando de 160x a 316x de aceleración.
Compilar con:
Salida de ejemplo:
Código de OP:
fuente