Ahora que todos han desarrollado su (a menudo sorprendente) experiencia de codificación de bajo nivel para ¿Cuán lento es realmente Python? (¿O qué tan rápido es su idioma?) Y ¿Cuán lento es realmente Python (Parte II)? Es hora de un desafío que también ampliará su capacidad para mejorar un algoritmo.
El siguiente código calcula una lista de longitud 9. La posición i
en la lista cuenta el número de veces que se encontraron al menos i
ceros consecutivos al calcular los productos internos entre F
y S
. Para hacer esto exactamente, itera sobre todas las listas posibles F
de longitud n
y listas S
de longitud n+m-1
.
#!/usr/bin/python
import itertools
import operator
n=8
m=n+1
leadingzerocounts = [0]*m
for S in itertools.product([-1,1], repeat = n+m-1):
for F in itertools.product([-1,1], repeat = n):
i = 0
while (i<m and sum(map(operator.mul, F, S[i:i+n])) == 0):
leadingzerocounts[i] +=1
i+=1
print leadingzerocounts
La salida es
[4587520, 1254400, 347648, 95488, 27264, 9536, 4512, 2128, 1064]
Si aumenta n a 10,12,14,16,18,20 usando este código, muy rápidamente se vuelve demasiado lento.
Reglas
- El desafío es dar la salida correcta para el mayor número posible. Solo los valores pares de n son relevantes.
- Si hay un empate, la victoria va al código más rápido en mi máquina para el n más grande.
- Me reservo el derecho de no probar el código que lleva más de 10 minutos.
- Puede cambiar el algoritmo de la forma que desee, siempre que proporcione la salida correcta. De hecho , tendrá que cambiar el algoritmo para hacer un progreso decente hacia ganar.
- El ganador recibirá una semana desde que se estableció la pregunta.
- La recompensa se otorgará cuando venza, un poco después de que se otorgue el ganador.
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. Como consecuencia, solo use software gratuito fácilmente disponible e incluya instrucciones completas sobre cómo compilar y ejecutar su código.
Estado .
- C . n = 12 en 49 segundos por @Fors
- Java . n = 16 en 3:07 por @PeterTaylor
- C ++ . n = 16 en 2:21 por @ilmale
- Rpython . n = 22 en 3:11 por @primo
- Java . n = 22 en 6:56 por @PeterTaylor
- Nimrod . n = 24 en 9:28 segundos por @ReimerBehrends
¡El ganador fue Reimer Behrends con una entrada en Nimrod!
Como verificación, la salida para n = 22 debería ser [12410090985684467712, 2087229562810269696, 351473149499408384, 59178309967151104, 9975110458933248, 1682628717576192, 284866824372224, 48558946385920, 8416739196928, 1518499004416, 301448822784, 71620493312, 22100246528, 8676573184, 3897278464, 1860960256, 911646720, 451520512, 224785920, 112198656, 56062720, 28031360, 14015680]
.
La competencia ya terminó pero ... ofreceré 200 puntos por cada envío que aumente n en 2 (dentro de los 10 minutos en mi computadora) hasta que me queden sin puntos. Esta oferta está abierta para siempre .
fuente
Respuestas:
Nimrod (N = 22)
Compilar con
(Nimrod se puede descargar aquí ).
Esto se ejecuta en el tiempo asignado para n = 20 (y para n = 18 cuando solo se usa un solo subproceso, lo que demora aproximadamente 2 minutos en el último caso).
El algoritmo utiliza una búsqueda recursiva, podando el árbol de búsqueda cada vez que se encuentra un producto interno distinto de cero. También reducimos el espacio de búsqueda a la mitad al observar que para cualquier par de vectores
(F, -F)
solo necesitamos considerar uno porque el otro produce exactamente los mismos conjuntos de productos internos (al negarS
también).La implementación utiliza las funciones de metaprogramación de Nimrod para desenrollar / en línea los primeros niveles de la búsqueda recursiva. Esto ahorra un poco de tiempo al usar gcc 4.8 y 4.9 como el backend de Nimrod y una buena cantidad para el sonido metálico.
El espacio de búsqueda podría reducirse aún más al observar que solo necesitamos considerar valores de S que difieran en un número par de las primeras N posiciones de nuestra elección de F. Sin embargo, la complejidad o las necesidades de memoria de eso no escalan para valores grandes de N, dado que el cuerpo del bucle se omite por completo en esos casos.
Tabular donde el producto interno es cero parece ser más rápido que usar cualquier funcionalidad de conteo de bits en el ciclo. Aparentemente acceder a la mesa tiene bastante buena localidad.
Parece que el problema debería ser susceptible a la programación dinámica, considerando cómo funciona la búsqueda recursiva, pero no hay una forma aparente de hacerlo con una cantidad razonable de memoria.
Salidas de ejemplo:
N = 16:
N = 18:
N = 20:
Para comparar el algoritmo con otras implementaciones, N = 16 toma aproximadamente 7.9 segundos en mi máquina cuando se usa un solo hilo y 2.3 segundos cuando se usan cuatro núcleos.
N = 22 tarda unos 15 minutos en una máquina de 64 núcleos con gcc 4.4.6 como backend de Nimrod y desborda enteros de 64 bits
leadingZeros[0]
(posiblemente no sin firmar, no lo he mirado).Actualización: he encontrado espacio para un par de mejoras más. Primero, para un valor dado de
F
, podemos enumerar las primeras 16 entradas de losS
vectores correspondientes con precisión, porque deben diferir enN/2
lugares exactos . Así precomputamos una lista de vectores de bits de tamañoN
que tienenN/2
determinados bits y utilizar estos para derivar la parte inicial deS
partirF
.En segundo lugar, podemos mejorar la búsqueda recursiva al observar que siempre conocemos el valor de
F[N]
(ya que el MSB es cero en la representación de bits). Esto nos permite predecir con precisión a qué rama recurrimos desde el producto interno. Si bien eso realmente nos permitiría convertir toda la búsqueda en un bucle recursivo, eso realmente arruina bastante la predicción de rama, por lo que mantenemos los niveles superiores en su forma original. Todavía ahorramos algo de tiempo, principalmente al reducir la cantidad de ramificaciones que estamos haciendo.Para alguna limpieza, el código ahora usa enteros sin signo y los repara a 64 bits (en caso de que alguien quiera ejecutar esto en una arquitectura de 32 bits).
La aceleración general está entre un factor de x3 y x4. N = 22 todavía necesita más de ocho núcleos para ejecutarse en menos de 10 minutos, pero en una máquina de 64 núcleos ahora se reduce a unos cuatro minutos (con un
numThreads
aumento correspondiente en consecuencia). Sin embargo, no creo que haya mucho más margen de mejora sin un algoritmo diferente.N = 22:
Actualizada nuevamente, haciendo uso de otras posibles reducciones en el espacio de búsqueda. Se ejecuta en aproximadamente 9:49 minutos para N = 22 en mi máquina quadcore.
Actualización final (creo). Mejores clases de equivalencia para las opciones de F, reduciendo el tiempo de ejecución de N = 22 a
3:19 minutos y57 segundos (editar: accidentalmente lo ejecuté con solo un hilo) en mi máquina.Este cambio hace uso del hecho de que un par de vectores produce los mismos ceros iniciales si uno puede transformarse en el otro al rotarlo. Desafortunadamente, una optimización de bajo nivel bastante crítica requiere que el bit superior de F en la representación de bits sea siempre el mismo, y mientras usa esta equivalencia recorta un poco el espacio de búsqueda y reduce el tiempo de ejecución en aproximadamente un cuarto sobre el uso de un espacio de estado diferente reducción en F, la sobrecarga de eliminar la optimización de bajo nivel más que compensarla. Sin embargo, resulta que este problema puede eliminarse considerando también el hecho de que F que son inversas entre sí también son equivalentes. Si bien esto aumentó un poco la complejidad del cálculo de las clases de equivalencia, también me permitió retener la optimización de bajo nivel mencionada anteriormente, lo que condujo a una aceleración de aproximadamente x3.
Una actualización más para admitir enteros de 128 bits para los datos acumulados. Para compilar con enteros de 128 bits, necesitará
longint.nim
desde aquí y compilar con-d:use128bit
. N = 24 todavía lleva más de 10 minutos, pero he incluido el resultado a continuación para los interesados.N = 24:
fuente
Java (
n=22
?)Creo que la mayoría de las respuestas son mejores que
n=16
usar un enfoque similar a este, aunque difieren en las simetrías que explotan y la forma en que dividen la tarea entre hilos.Los vectores definidos en la pregunta se pueden reemplazar con cadenas de bits, y el producto interno con XOR haciendo coincidir la ventana superpuesta y verificando que haya exactamente
n/2
bits establecidos (y, por lo tanto,n/2
bits borrados). Hayn! / ((n/2)!)
cadenas den
bits (coeficiente binomial central) conn/2
bits establecidos (que yo llamo cadenas equilibradas ), por lo que para cualquier datoF
hay tantas ventanasS
que dan un producto interno cero. Además, la acción de deslizarse a loS
largo de uno y verificar si todavía podemos encontrar un bit entrante que proporcione un producto interno cero corresponde a buscar un borde en un gráfico cuyos nodos son las ventanas y cuyos bordes unen un nodou
a un nodov
cuyos primerosn-1
bits son los ultimosn-1
bits deu
.Por ejemplo, con
n=6
yF=001001
obtenemos este gráfico:y para
F=001011
obtener este gráfico:Luego, necesitamos contar para cada uno
i
de0
an
cuántos caminos de longitudi
hay, sumando los gráficos para cada unoF
. Creo que la mayoría de nosotros estamos usando la búsqueda en profundidad.Tenga en cuenta que los gráficos son escasos: es fácil demostrar que cada nodo tiene un grado máximo de 1 y un grado máximo de uno. Eso también significa que las únicas estructuras posibles son cadenas simples y bucles simples. Esto simplifica un poco el DFS.
Exploto un par de simetrías: las cadenas equilibradas están cerradas bajo bit inverso (la
~
operación en muchos idiomas de la familia ALGOL) y bajo rotación de bits, por lo que podemos agrupar valores de losF
que están relacionados por estas operaciones y solo hacemos el DFS una vez.En mi Core 2 de 2.5GHz obtengo
Como la computadora de Lembik tiene 8 núcleos y ejecutó mi programa anterior de un solo subproceso dos veces más rápido que el mío, soy optimista de que se ejecutará
n=22
en menos de 8 minutos.fuente
do
Básicamente es solo una implementación ligeramente optimizada del algoritmo en la pregunta. Se puede administrar
n=12
dentro del límite de tiempo.Prueba de funcionamiento
n=12
, incluida la compilación:Comentario: acabo de encender mi cerebro y usé algunos combinatorios simples para calcular que el primer valor siempre será
n! / ((n / 2)!)^2 * 2^(n + m - 1)
. Me parece que debe haber una solución completamente algebraica para este problema.fuente
Java,
n=16
Para cualquier valor dado de
F
hay\binom{n}{n/2}
vectores que tienen un producto interno cero con él. Por lo tanto, podemos construir un gráfico cuyos vértices sean aquellos vectores coincidentes y cuyos bordes correspondan al desplazamiento deS
, y luego solo necesitamos contar las rutas de longitud hastan
en el gráfico.No he intentado microoptimizar esto reemplazando los condicionales con operaciones bit a bit, pero cada incremento doble
n
aumenta el tiempo de ejecución aproximadamente 16 veces, por lo que eso no hará una diferencia suficiente a menos que esté bastante cerca del umbral. En mi máquina, no lo soy.En mi Core 2 de 2.5GHz obtengo
fuente
f
y vértices iniciales, itere sobre todosf_xor_g
conn/2
bits establecidos exactamente . Para cada uno de estos, iterar sobre todof
y tomarg = f ^ f_xor_g
.RPython, N = 22 ~ 3: 23
Multihilo, utilizando un descenso recursivo sin pila. El programa acepta dos argumentos de línea de comando: N y el número de subprocesos de trabajo.
Compilar
Haga un clon local del repositorio PyPy usando mercurial, git o lo que prefiera. Escriba el siguiente encantamiento (suponiendo que se nombre el script anterior
convolution-high.py
):donde
%PYPY_REPO%
representa una variable de entorno que apunta al repositorio que acaba de clonar. La compilación lleva aproximadamente un minuto.Tiempos de muestra
N = 16, 4 hilos:
N = 18, 4 hilos:
N = 20, 4 hilos:
N = 22, 4 hilos:
fuente
Python 3.3, N = 20, 3.5min
Descargo de responsabilidad: mi intención NO es publicar esto como mi propia respuesta, ya que el algoritmo que estoy usando es solo un puerto descarado de la solución RPython de primo . Mi propósito aquí es sólo para mostrar lo que puede hacer en Python si se combinan la magia de numpy y Numba módulos.
Numba explicó en resumen:
Actualización 1 : noté después de arrojar los números que simplemente podemos omitir algunos de los números por completo. Entonces ahora maxf se convierte en (1 << n) // 2 y maxs se convierte en maxf 2 **. Esto acelerará el proceso bastante. n = 16 ahora solo toma ~ 48s (antes 4,5 min). También tengo otra idea que intentaré y veré si puedo hacerlo un poco más rápido.
Actualización 2: Algoritmo modificado (solución de primo). Si bien mi puerto aún no admite subprocesos múltiples, es bastante trivial agregarlo. Incluso es posible liberar CPython GIL usando Numba y ctypes. Sin embargo, esta solución también funciona muy rápido en un solo núcleo.
Y finalmente:
Esto se ejecuta en mi máquina en 212688ms o ~ 3.5min.
fuente
C ++ N = 16
Estoy probando en un EEEPC con un átomo ... mi tiempo no tiene mucho sentido. : D
El átomo resuelve n = 14 en 34 segundos. Yn = 16 en 20 minutos. Quiero probar n = 16 en PC OP. Soy optimista.
La idea es que cada vez que encontremos una solución para una F dada, hemos encontrado una solución 2 ^ i porque podemos cambiar la parte inferior de S que conduce al mismo resultado.
compilar:
gcc 26459.cpp -std = c ++ 11 -O3 -march = native -fstrict-aliasing -ftree-vectorize -Wall -pedantic -o 26459
fuente
JAVASCRIPT n: 12
En mi computadora tardó 231.242 segundos. En la demostración estoy usando trabajadores web para evitar congelar el navegador. Esto seguramente se puede mejorar aún más con trabajadores paralelos. Sé que JS no tiene ninguna oportunidad en este desafío, ¡pero lo hice por diversión!
Haga clic para ejecutar la demostración en línea
fuente
n=22
[235388928,86292480,19031048,5020640,1657928,783920,545408,481256,463832,460256,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744]
i.imgur.com/FIJa2Ch.pngFortran: n = 12
Acabo de hacer una versión rápida y sucia en Fortran, sin optimizaciones excepto OpenMP. Debería exprimirse por debajo de los 10 minutos durante n = 12 en la máquina de OP, toma 10:39 en mi máquina, que es un poco más lenta.
Los enteros de 64 bits tienen un impacto negativo en el rendimiento; Supongo que tendría que repensar todo el algoritmo para que esto sea mucho más rápido. No sé si me voy a molestar, creo que preferiré pasar un tiempo pensando en un buen desafío que sea más para mis gustos. Si alguien más quiere tomar esto y ejecutarlo, adelante :)
fuente
Lua: n = 16
Descargo de responsabilidad: mi intención NO es publicar esto como mi propia respuesta, ya que el algoritmo que estoy usando es descaradamente robado de la respuesta inteligente de Anna Jokela . que fue robado descaradamente de la inteligente respuesta de ilmale .
Además, ni siquiera es válido: tiene imprecisiones causadas por números de coma flotante (sería mejor si Lua admitiera enteros de 64 bits). Sin embargo, todavía lo estoy cargando, solo para mostrar qué tan rápida es esta solución. Es un lenguaje de programación dinámico y, sin embargo, puedo calcular n = 16 en un tiempo razonable (1 minuto en una CPU de 800MHz).
Ejecutar con LuaJIT, el intérprete estándar es demasiado lento.
fuente
long long
en lugar dedouble
con un ajuste de compilación), no LuaJIT.