Ha habido mil millones de iteraciones de desafíos de Fibonacci en este sitio web, ¡así que vamos a darle vida a un desafío de Fibonacci de mil millones de iteraciones!
Su desafío es generar los primeros 1000 dígitos decimales del número 1,000,000,000 de Fibonacci con el programa más corto posible. Luego, opcionalmente, esto puede ser seguido por cualquier salida adicional de su elección, que incluye pero no se limita al resto de los dígitos.
Estoy utilizando la convención de que fib 0 = 0
, fib 1 = 1
.
Su programa debe ser lo suficientemente rápido para que pueda ejecutarlo y verificar su corrección. Para este propósito, aquí están los primeros 1000 dígitos:
7952317874554683467829385196197148189255542185234398913453039937343246686182519370050999626136556779332482035723222451226291714456275648259499530612111301255499879639516053459789018700567439946844843034599802419924043753401950114830107234265037841426980398387360784284231996457340782784200767760907777703183185744656536253511502851715963351023990699232595471322670365506482435966586886048627159716916351448788527427435508113909167963907380398242848033980110276370544264285032744364781198451825462130529529633339813483105771370128111851128247136311414208318983802526907917787094802217750859685116363883374847428036737147882079956688807509158372249451437519320162582002000530798309887261257028201907509370554232931107084976854715833585623910450679449120011564762925649144509531904684984417002512086504020779012501356177874199605085558317190905395134468919443313026824813363234190494375599262553025466528838122639433600483849535070647711986769279568548796855207684897741771784375859496425384355879105799
code-golf
kolmogorov-complexity
fibonacci
restricted-time
user1502040
fuente
fuente
Your program must be fast enough for you to run it and verify its correctness.
¿Qué hay de la memoria?a+=b;b+=a;
bucle (tal vez con Java BigInteger) es la opción obvia, al menos si incluso estás pensando en el rendimiento. Una implementación recursiva siempre me ha parecido terriblemente ineficiente.write()
llamada al sistema). Me gusta el requisito de rendimiento, eso me hizo mucho más divertido.Respuestas:
Python 2 + sympy, 72 bytes
Pruébalo en línea!
-10 bytes eliminando el término prácticamente-0 gracias a Jeff Dege
-1 byte (1000 -> 1e3 gracias a Zacharý)
-2 bytes eliminando la variable innecesaria gracias a Erik the Outgolfer
-2 bytes moviéndose a Python 2 gracias a Zacharý
-3 bytes por 11 '
-11
gracias a ThePirateBay -3 bytes cambiandostr
por backticks gracias a notjagan¡ahora supera la solución haskell sin publicar de OP!
fuente
from sympy import*;sqrt
no guarda más bytesimport sympy;sympy.sqrt
:)sympy
es un paquete matemático simbólico para Python, por lo que no hay problemas con el error de redondeo, al menos hasta números muy grandes (este número no es lo suficientemente grande, jajaja). Luego lo calculo para darme los primeros 1e3 dígitos porque, de lo contrario, si elimina la.evalf(1e3)
parte, me da una representación de notación científica muy corta.Python 2 , 106 bytes
Pruébalo en línea!
No hay bibliotecas, solo aritmética de enteros. Corre casi al instante.
El núcleo es la identidad de divide y vencerás:
Esto nos permite actualizar
(a,b) = (f(n),f(n+1))
a doblen -> 2*n
. Como queremos obtenern=10**9
, esto solo requierelog_2(10**9)=30
iteraciones. Construimosn
hasta10**9
haciendo repetidamenten->2*n+c
para cada dígitoc
de su expansión binaria. Cuandoc==1
, el valor duplicado se desplaza hacia arriba2*n -> 2*n+1
con un cambio de Fibonacci de un paso(a,b)=(b+a,b)
Para mantener los valores
a,b
manejables, almacenamos solo sus primeros1006
dígitos dividiendo el piso entre10
hasta que estén debajo2**3340 ~ 1e1006
.fuente
a,b,c=a*a+b*b,a*a-c*c,b*b+c*c
.Código de máquina x86 de 32 bits (con llamadas al sistema Linux):
106105 bytesregistro de cambios: guardó un byte en la versión rápida porque una constante off-by-one no cambia el resultado para Fib (1G).
O 102 bytes para una versión un 18% más lenta (en Skylake) (usando
mov
/sub
/ encmc
lugar delea
/cmp
en el bucle interno, para generar la ejecución y el ajuste en10**9
lugar de2**32
). O 101 bytes para una versión más lenta ~ 5.3x con una rama en el manejo de acarreo en el bucle más interno. (¡Medí una tasa de predicción errónea de sucursal del 25.4%!)O 104/101 bytes si se permite un cero inicial. (Se necesita 1 byte adicional para que el código duro omita 1 dígito de la salida, que es lo que se necesita para Fib (10 ** 9)).
Desafortunadamente, el modo NASM de TIO parece ignorarse
-felf32
en los indicadores del compilador. Aquí hay un enlace de todos modos con mi código fuente completo, con todo el lío de ideas experimentales en los comentarios.Este es un programa completo . Imprime los primeros 1000 dígitos de Fib (10 ** 9) seguidos de algunos dígitos adicionales (los últimos de los cuales son incorrectos) seguidos de algunos bytes basura (sin incluir una nueva línea). La mayor parte de la basura no es ASCII, por lo que es posible que desee canalizar
cat -v
. Sin embargo, no rompe mi emulador de terminal (KDEkonsole
). Los "bytes basura" están almacenando Fib (999999999). Ya tenía-1024
en un registro, por lo que era más barato imprimir 1024 bytes que el tamaño adecuado.Estoy contando solo el código de máquina (tamaño del segmento de texto de mi ejecutable estático), no la pelusa que lo convierte en un ejecutable ELF. ( Son posibles ejecutables ELF muy pequeños , pero no quería molestarme con eso). Resultó ser más corto usar memoria de pila en lugar de BSS, por lo que puedo justificar no contar nada más en el binario ya que no dependo de ningún metadato. (La producción de un binario estático despojado de la forma normal hace que un ELF de 340 bytes sea ejecutable).
Podría hacer una función de este código que podría llamar desde C. Costaría unos pocos bytes guardar / restaurar el puntero de la pila (tal vez en un registro MMX) y alguna otra sobrecarga, pero también guardar bytes al regresar con la cadena en memoria, en lugar de hacer una
write(1,buf,len)
llamada al sistema. Creo que jugar golf en código de máquina debería ganarme un poco de holgura aquí, ya que nadie más ha publicado una respuesta en ningún idioma sin precisión extendida nativa, pero creo que una versión funcional de esto aún debería tener menos de 120 bytes sin volver a jugar golf todo cosa.Algoritmo:
fuerza bruta
a+=b; swap(a,b)
, truncada según sea necesario para mantener solo los primeros> = 1017 dígitos decimales. Se ejecuta en 1min13s en mi computadora (o 322.47 mil millones de ciclos de reloj + - 0.05%) (y podría ser un poco más rápido con unos pocos bytes adicionales de tamaño de código, o hasta 62s con un tamaño de código mucho mayor desde el desenrollado del bucle. No matemática inteligente, solo haciendo el mismo trabajo con menos gastos generales). Se basa en la implementación de Python de @ AndersKaseorg , que se ejecuta en 12min35s en mi computadora (4.4GHz Skylake i7-6700k). Ninguna de las versiones tiene errores de caché L1D, por lo que mi DDR4-2666 no importa.A diferencia de Python, almaceno los números de precisión extendida en un formato que libera los dígitos decimales truncados . Almaceno grupos de 9 dígitos decimales por entero de 32 bits, por lo que un desplazamiento del puntero descarta los 9 dígitos bajos. Esto es efectivamente una base de mil millones, que es una potencia de 10. (Es pura coincidencia que este desafío necesite el número mil millonésimo de Fibonacci, pero me ahorra un par de bytes frente a dos constantes separadas).
Siguiendo la terminología de GMP , cada fragmento de 32 bits de un número de precisión extendida se denomina "extremidad". La ejecución mientras se agrega debe generarse manualmente con una comparación con 1e9, pero luego se usa normalmente como una entrada a la
ADC
instrucción habitual para la siguiente extremidad. (También tengo que ajustar manualmente al[0..999999999]
rango, en lugar de 2 ^ 32 ~ = 4.295e9. Hago esto sin ramificaciones conlea
+cmov
, usando el resultado de la comparación).Cuando la última rama produce una ejecución distinta de cero, las siguientes dos iteraciones del bucle externo leen desde una rama más alta de lo normal, pero aún escriben en el mismo lugar. Esto es como hacer un
memcpy(a, a+4, 114*4)
desplazamiento hacia la derecha en una extremidad, pero como parte de los siguientes dos bucles de adición. Esto sucede cada ~ 18 iteraciones.Hacks para ahorrar tamaño y rendimiento:
Las cosas habituales como en
lea ebx, [eax-4 + 1]
lugar demov ebx, 1
, cuando lo séeax=4
. Y usarloop
en lugares dondeLOOP
la lentitud solo tiene un pequeño impacto.Truncar por 1 miembro de forma gratuita mediante la compensación de los punteros que leemos, mientras sigue escribiendo al inicio del búfer en el
adc
bucle interno. Leemos[edi+edx]
y escribimos a[edi]
. Entonces podemos obteneredx=0
u4
obtener un desplazamiento de lectura y escritura para el destino. Necesitamos hacer esto para 2 iteraciones sucesivas, primero compensando ambas, luego solo compensando el dst. Detectamos el segundo caso al miraresp&4
antes de restablecer los punteros al frente de los buffers (usando&= -1024
, porque los buffers están alineados). Ver comentarios en el código.El entorno de inicio de proceso de Linux (para un ejecutable estático) pone a cero la mayoría de los registros, y la memoria de pila debajo de
esp
/rsp
se pone a cero. Mi programa se aprovecha de esto. En una versión de función invocable de esto (donde la pila no asignada podría estar sucia), podría usar BSS para memoria puesta a cero (al costo de quizás 4 bytes más para configurar punteros). La reducción a ceroedx
tomaría 2 bytes. El x86-64 System V ABI no garantiza ninguno de estos, pero la implementación de Linux hace cero (para evitar filtraciones de información del núcleo). En un proceso vinculado dinámicamente, se/lib/ld.so
ejecuta antes_start
y deja registros distintos de cero (y probablemente basura en la memoria debajo del puntero de la pila).Me mantengo
-1024
enebx
uso fuera de los bucles. Úselobl
como un contador para los bucles internos, que termina en cero (que es el byte bajo de-1024
, restaurando así la constante para su uso fuera del bucle). Intel Haswell y versiones posteriores no tienen penalizaciones por fusión de registros parciales para registros low8 (y de hecho ni siquiera los renombran por separado) , por lo que hay una dependencia en el registro completo, como en AMD (no es un problema aquí). Sin embargo, esto sería horrible en Nehalem y anteriores, que tienen puestos de registro parcial al fusionarse. Hay otros lugares donde escribo registros parciales y luego leo el registro completo sinxor
poner a cero omovzx
, generalmente porque sé que algún código anterior puso a cero los bytes superiores, y nuevamente está bien en AMD e Intel SnB-family, pero lento en Intel pre-Sandybridge.Lo uso
1024
como el número de bytes para escribir en stdout (sub edx, ebx
), por lo que mi programa imprime algunos bytes de basura después de los dígitos de Fibonacci, porquemov edx, 1000
cuesta más bytes.(no se utiliza)
adc ebx,ebx
con EBX = 0 para obtener EBX = CF, el ahorro de 1 byte vssetc bl
.dec
/jnz
dentro de unadc
bucle conserva CF sin causar un bloqueo parcial de bandera cuandoadc
lee banderas en Intel Sandybridge y posteriores. Es malo en CPU anteriores , pero AFAIK gratis en Skylake. O, en el peor de los casos, un impulso adicional.Use la memoria a continuación
esp
como una zona roja gigante . Dado que este es un programa completo de Linux, sé que no instalé ningún controlador de señal y que nada más bloqueará asincrónicamente la memoria de la pila de espacio de usuario. Este puede no ser el caso en otros sistemas operativos.Aproveche el motor de pila para ahorrar ancho de banda de emisión de uop usando
pop eax
(1 uop + uop de sincronización de pila ocasional) en lugar delodsd
(2 uops en Haswell / Skylake, 3 en IvB y anteriores según las tablas de instrucciones de Agner Fog )). IIRC, esto redujo el tiempo de ejecución de aproximadamente 83 segundos a 73. Probablemente podría obtener la misma velocidad al usar unmov
modo de direccionamiento indexado, comomov eax, [edi+ebp]
donde seebp
mantiene el desplazamiento entre las memorias intermedias src y dst. (Haría que el código fuera del bucle interno fuera más complejo, ya que tendría que negar el registro de desplazamiento como parte del intercambio de src y dst para las iteraciones de Fibonacci). Consulte la sección "rendimiento" a continuación para obtener más información.comience la secuencia dando a la primera iteración un arrastre (un byte
stc
), en lugar de almacenar un1
en memoria en cualquier lugar. Muchas otras cosas específicas del problema documentadas en los comentarios.Listado NASM (código máquina + fuente) , generado con
nasm -felf32 fibonacci-1G.asm -l /dev/stdout | cut -b -28,$((28+12))- | sed 's/^/ /'
. (Luego, eliminé a mano algunos bloques de material comentado, por lo que la numeración de las líneas tiene huecos). Para eliminar las columnas iniciales y poder introducirlas en YASM o NASM, usecut -b 27- <fibonacci-1G.lst > fibonacci-1G.asm
.Probablemente haya espacio para jugar más bytes, pero ya he pasado al menos 12 horas en esto durante 2 días. No quiero sacrificar la velocidad, a pesar de que es mucho más que lo suficientemente rápido y hay espacio para hacerlo más pequeño en formas que cuestan velocidad . Parte de mi razón para publicar es mostrar qué tan rápido puedo hacer una versión asm de fuerza bruta. Si alguien quiere realmente un tamaño mínimo pero tal vez 10 veces más lento (por ejemplo, 1 dígito por byte), siéntase libre de copiar esto como punto de partida.
El ejecutable resultante (desde
yasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm && ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
) es 340B (despojado):Actuación
El
adc
bucle interno es de 10 uops de dominio fusionado en Skylake (+1 uop de sincronización de pila cada ~ 128 bytes), por lo que puede emitirse a uno por cada ~ 2.5 ciclos en Skylake con un rendimiento óptimo de front-end (ignorando los uops de sincronización de pila) . La latencia de la ruta crítica es de 2 ciclos, para la cadena de dependencia transportada en bucle de la siguiente iteraciónadc
->cmp
->adc
, por lo que el cuello de botella debe ser el límite de emisión inicial de ~ 2.5 ciclos por iteración.adc eax, [edi + edx]
son 2 uops de dominio sin fusionar para los puertos de ejecución: carga + ALU. Se micro fusiona en los decodificadores (1 uop de dominio fusionado), pero se deslamina en la etapa de emisión a 2 uops de dominio fusionado, debido al modo de direccionamiento indexado, incluso en Haswell / Skylake . Pensé que permanecería micro fusionada, como loadd eax, [edi + edx]
hace, pero tal vez mantener los modos de direccionamiento indexados micro fusionados no funciona para los uops que ya tienen 3 entradas (banderas, memoria y destino). Cuando lo escribí, pensaba que no tendría un rendimiento negativo, pero estaba equivocado. Esta forma de manejar el truncamiento ralentiza el ciclo interno cada vez, yaedx
sea 0 o 4.Sería más rápido manejar el desplazamiento de lectura-escritura para el dst al compensar
edi
y usaredx
para ajustar la tienda. Entoncesadc eax, [edi]
/ ... /mov [edi+edx], eax
/ enlea edi, [edi+4]
lugar destosd
. Haswell y más adelante pueden mantener una tienda indexada micro fusionada. (Sandybridge / IvB también lo deslaminaría).En Intel Haswell y versiones anteriores,
adc
ycmovc
son 2 uops cada uno, con latencia 2c . (adc eax, [edi+edx]
todavía no está laminado en Haswell y emite como 3 uops de dominio fusionado). Broadwell y versiones posteriores permiten uops de 3 entradas para más que solo FMA (Haswell), haciendoadc
ycmovc
(y un par de cosas más) instrucciones de un solo uop, como si hubieran estado en AMD durante mucho tiempo. (Esta es una de las razones por las que AMD ha funcionado bien en los puntos de referencia GMP de precisión extendida durante mucho tiempo). De todos modos, el bucle interno de Haswell debe ser de 12 uops (ocasionalmente +1 stack-sync uop), con un cuello de botella frontal de ~ 3c por Es el mejor de los casos, ignorando la sincronización de la pila.El uso
pop
sin un equilibriopush
dentro de un bucle significa que el bucle no puede ejecutarse desde el LSD (detector de flujo de bucle) , y debe volver a leerse desde la caché uop al IDQ cada vez. En todo caso, es algo bueno en Skylake, ya que un ciclo de 9 u 10 uop no se emite de manera óptima a 4 uops en cada ciclo . Probablemente esto sea parte de por qué reemplazar tantolodsd
conpop
ayudado. (El LSD no puede bloquear los uops porque eso no dejaría espacio para insertar un uop de sincronización de pila .) (Por cierto, una actualización de microcódigo deshabilita el LSD por completo en Skylake y Skylake-X para corregir una errata. Medí el anterior antes de obtener esa actualización.)Lo perfilé en Haswell y descubrí que se ejecuta en 381.31 mil millones de ciclos de reloj (independientemente de la frecuencia de la CPU, ya que solo usa caché L1D, no memoria). El rendimiento de problemas de front-end fue de 3.72 uops de dominio fusionado por reloj, frente a 3.70 para Skylake. (Pero, por supuesto, las instrucciones por ciclo se redujeron a 2.42 desde 2.87, porque
adc
ycmov
son 2 uops en Haswell).push
reemplazarlostosd
probablemente no ayudaría tanto, yaadc [esp + edx]
que desencadenaría una uop de sincronización de pila cada vez. Y costaría un byte porstd
lo quelodsd
va en la otra dirección. (mov [edi], eax
/lea edi, [edi+4]
reemplazarstosd
es una victoria, pasando de 32,909Mcycles por 100M iters a 31,954Mcycles por 100M iters. Parece questosd
decodifica como 3 uops, con la dirección de la tienda / datos de la tienda uops no micro-fusionados, entoncespush
+ stack-sync Uops aún podría ser más rápido questosd
)El rendimiento real de ~ 322.47 mil millones de ciclos para iteraciones 1G de 114 extremidades equivale a 2.824 ciclos por iteración del bucle interno , para la versión rápida 105B en Skylake. (Ver
ocperf.py
salida a continuación). Eso es más lento de lo que predije del análisis estático, pero estaba ignorando la sobrecarga del bucle externo y cualquier uops de sincronización de pila.Los contadores de rendimiento
branches
ybranch-misses
muestran que el bucle interno predice erróneamente una vez por bucle externo (en la última iteración, cuando no se toma). Eso también representa parte del tiempo extra.Podría guardar el tamaño del código haciendo que el bucle más interno tenga una latencia de 3 ciclos para la ruta crítica, usando
mov esi,eax
/sub eax,ebp
/cmovc eax, esi
/cmc
(2 + 2 + 3 + 1 = 8B) en lugar delea esi, [eax - 1000000000]
/cmp ebp,eax
/cmovc
(6 + 2 + 3 = 11B ) Elcmov
/stosd
está fuera de la ruta crítica. (El increment-edi uop destosd
puede ejecutarse por separado de la tienda, por lo que cada iteración se bifurca en una cadena de dependencia corta). Solía guardar otro 1B cambiando la instrucción de inicio ebp delea ebp, [ecx-1]
amov ebp,eax
, pero descubrí que tener el errorebp
No cambió el resultado. Esto permitiría que una extremidad sea exactamente == 1000000000 en lugar de envolver y producir un acarreo, pero este error se propaga más lentamente de lo que crece Fib (), por lo que esto no cambia los primeros dígitos de 1k del resultado final. Además, creo que ese error puede corregirse solo cuando solo estamos agregando, ya que hay espacio en una extremidad para sostenerlo sin desbordamiento. Incluso 1G + 1G no desborda un número entero de 32 bits, por lo que eventualmente se filtrará hacia arriba o se truncará.La versión de latencia 3c es 1 uop adicional, por lo que el front-end puede emitirlo en uno por 2.75c ciclos en Skylake, solo un poco más rápido que el back-end puede ejecutarlo. (En Haswell, será un total de 13 uops ya que todavía usa
adc
ycmov
, y un cuello de botella en el front-end a 3.25c por iter).En la práctica, funciona un factor de 1.18 más lento en Skylake (3.34 ciclos por extremidad), en lugar de 3 / 2.5 = 1.2 que predije para reemplazar el cuello de botella frontal con el cuello de botella de latencia simplemente mirando el bucle interno sin sincronización de pila uops Dado que los Uops de sincronización de pila solo dañan la versión rápida (cuello de botella en el front-end en lugar de latencia), no se necesita mucho para explicarlo. por ejemplo, 3 / 2.54 = 1.18.
Otro factor es que la versión de latencia 3c puede detectar la predicción errónea al abandonar el bucle interno mientras la ruta crítica aún se está ejecutando (porque el front-end puede adelantarse al back-end, permitiendo que la ejecución fuera de orden ejecute el bucle- counter uops), por lo que la penalización efectiva del error de predicción es menor. La pérdida de esos ciclos de front-end permite que el back-end se ponga al día.
Si no fuera por eso, tal vez podríamos acelerar la
cmc
versión 3c mediante el uso de una bifurcación en el bucle externo en lugar del manejo sin bifurcación de las compensaciones carry_out -> edx y esp. La predicción de bifurcación + la ejecución especulativa para una dependencia de control en lugar de una dependencia de datos podría permitir que la siguiente iteración comience a ejecutar eladc
bucle mientras los uops del bucle interno anterior todavía están en vuelo. En la versión sin ramificación, las direcciones de carga en el bucle interno tienen una dependencia de datos en CF desde el últimoadc
de la última rama.Los cuellos de botella de la versión de bucle interno de latencia 2c en el extremo frontal, por lo que el extremo posterior prácticamente se mantiene. Si el código del bucle externo era de alta latencia, el front-end podría avanzar emitiendo uops desde la próxima iteración del bucle interno. (Pero en este caso, el material del bucle externo tiene un montón de ILP y nada de alta latencia, por lo que el back-end no tiene mucho que hacer cuando comienza a masticar uops en el programador fuera de servicio como sus entradas se vuelven listas).
( +- x %)
es la desviación estándar en las 4 carreras para ese recuento. Es interesante que ejecute un número tan redondo de instrucciones. Que 924 mil millones no es una coincidencia. Supongo que el bucle externo ejecuta un total de 924 instrucciones.uops_issued
es un recuento de dominio fusionado (relevante para el ancho de banda del problema de front-end), mientras queuops_executed
es un recuento de dominio no fusionado (número de uops enviados a puertos de ejecución). Micro-fusion empaqueta 2 uops de dominio no fusionado en un uop de dominio fusionado, pero la eliminación de movimiento significa que algunos uops de dominio fusionado no necesitan ningún puerto de ejecución. Consulte la pregunta vinculada para obtener más información sobre el conteo de uops y el dominio fusionado frente al no fusionado. (Consulte también las tablas de instrucciones de Agner Fog y la guía de uarch , y otros enlaces útiles en el wiki de etiquetas SO x86 ).De otra ejecución que mide cosas diferentes: los errores de caché L1D son totalmente insignificantes, como se esperaba para leer / escribir los mismos dos buffers 456B. La rama del bucle interno predice erróneamente una vez por bucle externo (cuando no se toma para abandonar el bucle). (El tiempo total es mayor porque la computadora no estaba totalmente inactiva. Probablemente el otro núcleo lógico estuvo activo parte del tiempo, y se dedicó más tiempo a las interrupciones (ya que la frecuencia medida por el espacio del usuario estaba más abajo de 4.400 GHz). O múltiples núcleos estuvieron activos la mayor parte del tiempo, bajando el turbo máximo. No rastreé
cpu_clk_unhalted.one_thread_active
para ver si la competencia HT era un problema).Es posible que mi código se ejecute en menos ciclos en Ryzen, que puede emitir 5 uops por ciclo (o 6 cuando algunos de ellos son instrucciones de 2 uop, como AVX 256b en Ryzen). No estoy seguro de qué haría su front-end
stosd
, que es 3 uops en Ryzen (igual que Intel). Creo que las otras instrucciones en el bucle interno son la misma latencia que Skylake y todas de un solo uop. (Incluyendoadc eax, [edi+edx]
, lo cual es una ventaja sobre Skylake).Esto probablemente podría ser significativamente más pequeño, pero quizás 9 veces más lento si almacenara los números como 1 dígito decimal por byte . Generar llevar a cabo con
cmp
y ajustar concmov
funcionaría igual, pero hacer 1/9 del trabajo. También funcionarían 2 dígitos decimales por byte (base-100, no BCD de 4 bits con un lentoDAA
), ydiv r8
/add ax, 0x3030
convierte un byte 0-99 en dos dígitos ASCII en orden de impresión. Pero 1 dígito por byte no necesitadiv
nada, solo se repite y se agrega 0x30. Si guardo los bytes en orden de impresión, eso haría que el segundo ciclo sea realmente simple.El uso de 18 o 19 dígitos decimales por entero de 64 bits (en modo de 64 bits) haría que se ejecute aproximadamente el doble de rápido, pero con un tamaño de código significativo para todos los prefijos REX y para las constantes de 64 bits. Las extremidades de 32 bits en modo de 64 bits evitan el uso en
pop eax
lugar delodsd
. Todavía podría evitar los prefijos REX al usaresp
como un registro de scratch sin puntero (intercambiando el uso deesi
yesp
), en lugar de usarr8d
como un octavo registro.Si realiza una versión de función invocable, la conversión a 64 bits y el uso
r8d
pueden ser más baratos que guardar / restaurarrsp
. 64 bits tampoco puede usar ladec r32
codificación de un byte (ya que es un prefijo REX). Pero principalmente terminé usandodec bl
2 bytes. (Porque tengo una constante en los bytes superiores deebx
, y solo la uso fuera de los bucles internos, lo que funciona porque el byte bajo de la constante es0x00
).Versión de alto rendimiento
Para obtener el máximo rendimiento (no el código de golf), desearía desenrollar el bucle interno para que se ejecute a lo sumo 22 iteraciones, que es un patrón suficientemente corto tomado / no tomado para que los predictores de ramificación funcionen bien. En mis experimentos,
mov cl, 22
antes de un.inner: dec cl/jnz .inner
ciclo tenía muy pocas predicciones erróneas (como 0.05%, mucho menos de una por ciclo completo del ciclo interno), peromov cl,23
predecía incorrectamente de 0.35 a 0.6 veces por ciclo interno.46
es particularmente malo, prediciendo erróneamente ~ 1.28 veces por bucle interno (128M veces para iteraciones de bucle externo de 100M).114
pronosticó mal exactamente una vez por bucle interno, igual que encontré como parte del bucle Fibonacci.Me dio curiosidad y lo intenté, desenrollando el bucle interno por 6 con un
%rep 6
(porque eso divide 114 de manera uniforme). Eso eliminó principalmente las fallas en las ramas. Lo hiceedx
negativo y lo usé como compensación para lasmov
tiendas, por lo queadc eax,[edi]
podría quedar micro-fusionado. (Y así podría evitarstosd
). Saqué lalea
actualizaciónedi
del%rep
bloque, por lo que solo hace una actualización de puntero por cada 6 tiendas.También me deshice de todas las cosas de registro parcial en el bucle externo, aunque no creo que haya sido significativo. Puede haber ayudado un poco tener CF al final del bucle externo que no depende del ADC final, por lo que algunos de los uops del bucle interno pueden comenzar. El código del bucle externo probablemente podría optimizarse un poco más, ya que
neg edx
fue lo último que hice, después de reemplazarloxchg
con solo 2mov
instrucciones (ya que todavía tenía 1), y reorganizar las cadenas dep junto con soltar los 8 bits registrar cosasEsta es la fuente NASM de solo el bucle Fibonacci. Es un reemplazo directo para esa sección de la versión original.
Actuación:
Eso es para el mismo Fib (1G), produciendo la misma salida en 62.3 segundos en lugar de 73 segundos. (273.146G ciclos, frente a 322.467G. Dado que todo golpea en la memoria caché L1, los ciclos de reloj de núcleo son realmente todo lo que necesitamos ver).
Tenga en cuenta el
uops_issued
recuento total mucho más bajo , muy por debajo deluops_executed
recuento. Eso significa que muchos de ellos estaban micro fusionados: 1 uop en el dominio fusionado (problema / ROB), pero 2 uops en el dominio no fusionado (planificador / unidades de ejecución)). Y esos pocos fueron eliminados en la etapa de emisión / cambio de nombre (como lamov
copia del registro o laxor
puesta a cero, que deben emitirse pero no necesitan una unidad de ejecución). Uops eliminados desequilibrarían el conteo de la otra manera.branch-misses
se redujo a ~ 400k, desde 1G, por lo que desenrollado funcionó.resource_stalls.any
ahora es significativo, lo que significa que el front-end ya no es el cuello de botella: en cambio, el back-end se está quedando atrás y limita el front-end.idq_uops_not_delivered.core
solo cuenta los ciclos donde el front-end no entregó uops, pero el back-end no se detuvo. Eso es bueno y bajo, lo que indica pocos cuellos de botella en el front-end.Dato curioso: la versión de Python pasa más de la mitad de su tiempo dividiendo por 10 en lugar de sumar. (Reemplazar el
a/=10
cona>>=64
acelera en más de un factor de 2, pero cambia el resultado porque el truncamiento binario = truncamiento decimal).Por supuesto, mi versión asm está optimizada específicamente para este tamaño de problema, con el recuento de iteraciones de bucle codificado. Incluso cambiar un número de precisión arbitraria lo copiará, pero mi versión solo puede leer desde un desplazamiento durante las próximas dos iteraciones para omitir incluso eso.
Perfilé la versión de python (python2.7 de 64 bits en Arch Linux):
Los números en (parens) son la cantidad de tiempo que se muestreó el contador de rendimiento. Al mirar más contadores de los que admite el HW, el rendimiento gira entre diferentes contadores y extrapolatos. Eso está totalmente bien para una larga ejecución de la misma tarea.
Si ejecuté
perf
después de configurar sysctlkernel.perf_event_paranoid = 0
(o ejecutarperf
como root), mediría4.400GHz
.cycles:u
no cuenta el tiempo dedicado a las interrupciones (o llamadas del sistema), solo los ciclos de espacio de usuario. Mi escritorio estaba casi totalmente inactivo, pero esto es típico.fuente
Haskell,
8361 bytesSalidas ( F 1000000000 , F 1000000001 ). En mi computadora portátil, imprime correctamente el paréntesis izquierdo y los primeros 1000 dígitos en 133 segundos, usando 1.35 GiB de memoria.
Cómo funciona
La recurrencia de Fibonacci se puede resolver utilizando la exponenciación de la matriz:
[ F i - 1 , F i ; F i , F i + 1 ] = [0, 1; 1, 1] i ,
de donde derivamos estas identidades:
[ F i + j - 1 , F i + j ; F i + j , F i + j + 1 ] = [ F i - 1 , F i ; F i , F i + 1 ] ⋅ [ F j - 1 , F j ; + 1 F j + 1 - F i - 1 F j - 1 = FF j , F j + 1 ],
F i + j = F i i + 1 F j + 1 - ( F i + 1 - F i ) ( F j + 1 - F j ),
F i + j + 1 = F i F j + F i + 1 F j + 1 .
La
p
función calcula ( F i + j , F i + j + 1 ) dada ( F i , F i + 1 ) y ( F j , F j + 1 ). Escribiendof n
para ( F i , F i + 1 ), tenemosp (f i) (f j)
=f (i + j)
.Entonces,
(t=<<t.p) (f i)
=
t ((t.p) (f i)) (f i)
=
t (p (f i).p (f i).p (f i)) (f i)
=
(p (f i).p (f i).p (f i).p (f i).p (f i).p (f i).p (f i).p (f i).p (f i)) (f i)
=
f (10 * i)
,(t$t=<<t.p) (f i)
=
((t=<<t.p).(t=<<t.p).(t=<<t.p)) (f i)
=
f (10^3 * i)
,t(t$t=<<t.p) (f i)
=
((t$t=<<t.p).(t$t=<<t.p).(t$t=<<t.p)) (f i)
=
f (10^9 * i)
,y nos conectamos
f 1
=(1,1)
.fuente
Mathematica, 15
34bytesFibonacci
en sí toma ~ 6s en mi computadora. Y 95 (+/- 5) s para que frontend lo muestre.Los primeros 1000 dígitos (34 bytes):
⌊Fibonacci@1*^9/1*^208986640⌋&
Más largo pero más rápido
ToString@Fibonacci@1*^9~StringTake~1000&
:fuente
div
). Me detuve ya que la gente probablemente terminaría de ver esta pregunta cuando tuviera una función bien desarrollada que hiciera todo ese trabajo. Pero aparentemente la fuerza bruta puede funcionar, como muestran algunas respuestas.Python 2, 70 bytes
Esto se ejecutó en 18 minutos y 31 segundos en mi computadora portátil, produciendo los 1000 dígitos correctos seguidos de
74100118580
(los siguientes dígitos correctos son74248787892
).fuente
div
bucle para hacer 9 dígitos decimales por fragmento. Lleve durante las adiciones con cmp / cmov y 2xADD en lugar de ADC.Haskell , 78 bytes
Pruébalo en línea!
Tomó 48 segundos en TIO. Misma fórmula recursiva que mi respuesta de Python , pero sin truncar.
La constante
2143923439
es10**9-1
, invertida en binario, y con un 1 extra al final. Iterar a través de sus dígitos binarios en reversa simula iterar a través de los dígitos binarios de10**9-1
. Parece más corto codificar esto que calcularlo.fuente
Haskell ,
202184174173170168164162 bytesPruébalo en línea!
Explicación
Esto utiliza una forma bastante rápida para calcular los números de fibonacci. La función
l
toma dos Fibonacci números y calcula los números de Fibonacci 10 más tarde, mientrasf
toma la n º y n + 1 th números de Fibonacci y calcula el 2n + 20 ° y 2n + 21 números º de Fibonacci. Los encadené de manera bastante casual para obtener mil millones y obtener los primeros 1000 dígitos.fuente
Haskell, 81 bytes
Explicación
f n
calcula recursivamente eln
número de fibonacci th utilizando la recurrencia de la respuesta de xnor con eliminación de subexpresión común. A diferencia de las otras soluciones que se han publicado, que usan multiplicaciones de O (log (n)), tenemos una recursión de profundidad O (log (n)) con un factor de ramificación de 2, para una complejidad de multiplicaciones de O (n).¡Sin embargo, no todo está perdido! Debido a que casi todas las llamadas estarán cerca de la parte inferior del árbol de recursión, podemos usar aritmética nativa rápida siempre que sea posible y evitar mucha manipulación de bignums enormes. Escupe una respuesta en un par de minutos en mi caja.
fuente
T-SQL,
422 414453 bytes (verificado, ahora compitiendo)EDIT 2 : ¡Cambió a , ganó algunos bytes pero aumentó la velocidad lo suficiente como para completar hasta mil millones! Completado en 45 horas y 29 minutos , verifica la cadena dada y muestra 8 caracteres adicionales (que pueden ser correctos o no debido a errores de redondeo).
INT BIGINT
DECIMAL(37,0)
T-SQL no tiene soporte nativo de "gran número", así que tuve que rodar mi propio sumador de gran número basado en texto usando cadenas de 1008 caracteres:
Aquí está la versión formateada con comentarios:
Básicamente estoy manipulando manualmente cadenas de relleno de cero de 1008 caracteres que representan mis dos variables de Fibonacci,
@a
y@
.Los agrego
8 1836 dígitos a la vez, quitando los últimos 36 dígitos, convirtiéndolos en un tipo numérico manejable (DECIMAL(37,0)
), agregándolos y luego rompiéndolos en otra cadena larga@c
. Luego "giro"@a
y@
moviendo los últimos 36 dígitos al frente, y repitiendo el proceso. 28 rotaciones * 36 dígitos cubre todos los 1008. Tengo que "cargar el" manualmente.Una vez que nuestro número comienza a exceder la longitud de mi cadena, "cambio a la izquierda" y comenzamos a perder algo de precisión, pero el error está dentro de mis caracteres adicionales.
Intenté usar una tabla SQL llena de INTs y BIGINTs, con lógica similar, y fue dramáticamente más lenta. Extraño.
fuente
PARI / GP, 45 bytes
De alguna manera
\p1000
no es suficiente. Esto no funciona con sistemas de 32 bits. La división final es evitar el punto decimal en notación científica.fuente
Pari / GP , 15 + 5 = 20 bytes
Ejecute con la opción de línea de comando
-s1g
para asignar 1 Gbytes de memoria.fuente
Ruby, 63 bytes
hombre, soy malo jugando al golf rubí; pero la clase BigInt hace maravillas para este tipo de cosas. Usamos el mismo algoritmo que Anders Kaseorg.
fuente