4 mil millones de sentencias if
(andreasjhkarlsson.github.io)- Una idea juguetona de resolver la detección de números pares/impares solo enumerando comparaciones, sin usar
%, se fue ampliando de 8 bits a 32 bits y terminó mostrando los límites del compilador y del formato de ejecutable - Al generar automáticamente
if (number == n)con un generador en Python, los rangos de 8 y 16 bits funcionaron, pero en 32 bits la cantidad de comparaciones explotó hasta aprox. 4.2 mil millones - La versión en C de 32 bits produjo, tras 48 horas, un archivo C de 330GB aprox., y MSVC no pudo compilarlo por el límite de números de línea y falta de espacio en heap
- Para esquivar la restricción de 4GB de los ejecutables PE, se generaron directamente instrucciones x86-64 y se creó un binario de 40GB,
isEven.bin, que luego se invocó como código ejecutable mediante memory mapping en Windows - El programa final, tras cambiar
atoiporstrtoul, pudo clasificar correctamente también valores grandes de 32 bits, y para entradas grandes devolvía el resultado en unos 10 segundos en un equipo con Core i5 12600K, 32GB de memoria y SSD M.2
Determinar si un número es par o impar usando solo comparaciones
- El punto de partida fue una captura de código vista en redes sociales, con una forma de resolver el clásico problema de determinar si un número es par o impar sin usar la operación modulus
- La estructura consistía en poner
if (number == n)para cada número y mostrar conprintfsi ese número era par o impar - El primer ejemplo en C usaba
uint8_t number = atoi(argv[1]);y escribía manualmente comparaciones del 0 al 10 - Se compiló con
/Odpara desactivar optimizaciones y evitar que el compilador cambiara el algoritmo0,4dabaneven3,7dabanodd50,11,99no mostraban nada
- La causa era que después del último
ifya no había más comparaciones que procesar, así que hacían falta más sentencias if
Generar sentencias if con Python
- En vez de escribir todas las comparaciones a mano, se usó un enfoque de metaprogramación en el que Python imprimía el código C
- El script de Python generaba comparaciones de 0 a 255 con
for i in range(2**8)- Si
i % 2 == 0, emitíaprintf("even\n"); - Si no,
printf("odd\n");
- Si
- El programa C generado funcionó en todo el rango de 8 bits
99dabaodd50dabaeven240dabaeven241dabaodd
Hasta 16 bits, compilar en C sí funcionó
- El mismo enfoque se extendió a
uint16_tyrange(2**16) - El archivo C generado tenía unas 130 mil líneas
- Tras compilarlo con MSVC, funcionó correctamente con varios valores
21000dabaeven3475dabaodd3dabaodd65001dabaodd65532dabaeven
- El ejecutable pesaba unos 2MB, y en una PC con 31.8GB de memoria eso no causó problemas
El archivo C de 32 bits y los límites del compilador
- El siguiente objetivo fue procesar todo el rango de 32 bits con comparaciones usando
uint32_tyrange(2**32) - En 32 bits hay 65,536 veces más números que en 16 bits
- Tras ejecutar el generador de Python durante 48 horas, se obtuvo un archivo C de 330GB aprox.
- La compilación con MSVC pronto chocó con sus límites
warning C4049: el compilador alcanzó el límite de números de línea y dejó de emitir line numbers- el límite de números de línea era
16777215 fatal error C1060: compiler is out of heap space
- El formato Portable Executable (.exe) de Windows también tiene la restricción de que cuesta superar los 4GB, así que la ruta de compilar en C un ejecutable con más de 4 mil millones de comparaciones quedó bloqueada
- Como referencia, se mencionó la restricción relacionada de tamaño máximo de un archivo PE
Generar y ejecutar código máquina directamente
- Para evitar los límites del compilador y del formato de ejecutable, se cambió a un método que escribía directamente instrucciones x86-64 en un binario
- La función objetivo tenía forma de
IsEven, recibiendo el argumento enECXy devolviendo el resultado enEAXXOR EAX, EAXfijaba el valor de retorno por defecto en 0 para impar- Para cada número se emitía
CMP ECX, i - Si era par, hacía
INC EAXy luegoRET - Si era impar, hacía directamente
RET
- Se usaron x86-64 assembly y opcode, y los opcodes de cada instrucción se consultaron con ChatGPT
- El script de Python abría
isEven.binen binario y escribía instrucciones de comparación para todos los números desde 0 hasta2**32 - 1 - El
isEven.bingenerado pesaba unos 40GB e incluía aprox. 4.2 mil millones de comparaciones necesarias para cubrir todos los números de 32 bits
Invocar 40GB de código con memory mapping en Windows
- El programa anfitrión en C abría
isEven.biny, en vez de leer el archivo completo, lo mapeaba en memoria usando la API de Windows - El flujo de ejecución era el siguiente
- Abrir
isEven.binconCreateFileAusando permisosGENERIC_READ | GENERIC_EXECUTE - Comprobar el tamaño de archivo de 64 bits con
GetFileSizeEx - Especificar
PAGE_EXECUTE_READenCreateFileMapping - Crear un mapeo legible y ejecutable con
MapViewOfFile - Convertir el puntero mapeado a un puntero de función
int (*isEven)(int)e invocarlo
- Abrir
- Este método permite tratar el archivo de 40GB como si ya estuviera en memoria, dejando al sistema operativo la colocación real mediante memoria virtual
- En la primera prueba casi todo funcionó bien, pero
4200000000devolvíaodd, así que apareció un resultado incorrecto - La causa fue que
atoino manejaba correctamente valores unsigned grandes, y al cambiarlo porstrtoul(argv[1], NULL, 10),4200000000pasó a dareveny4200000001,odd
Observaciones de rendimiento
- Los números pequeños devolvían resultado de inmediato, y hasta los valores grandes cercanos al límite de
2^32respondían en unos 10 segundos - El entorno de prueba era un Core i5 12600K, 32GB de memoria y un SSD M.2
- La velocidad máxima de lectura del SSD observada durante el cálculo fue de aprox. 800MB/s
- Sigue siendo llamativo que se obtuviera ese nivel de rendimiento incluso en una situación donde había que leer 40GB desde disco, mapearlos a memoria física y el CPU apenas podía aprovechar beneficios de caché
1 comentarios
Opiniones de Hacker News
Ojalá todavía tuviera uno de los primeros programas que escribí. En 1996, cuando tenía 16 años, vi la sección de gráficos por computadora en el apéndice de un libro de álgebra lineal y, con lo que había aprendido de programación el semestre anterior, me obsesioné con un programa que dibujaba wireframes rotativos de algunas figuras.
Por eso casi repruebo la clase, pero en ese momento todavía no conocía los arrays, así que todos los vértices y elementos de la matriz de rotación eran variables hardcodeadas por separado, y para la multiplicación de matrices también tenía que copiar y pegar una larga lista de cálculos para cada vértice y modificarla, sin usar bucles.
Para dibujar en pantalla había que escribir en memoria a partir de cierta dirección, así que sí sabía de punteros, y también tenía un bucle para rasterizar las líneas entre vértices. En definitiva, ya tenía el concepto de arrays e indexación, pero no sabía crearlos yo mismo.
(x1,y1)hasta(x4,y4).Le dije a mi padre que quería usar algo como
xn,yndentro de un buclefor, dondenindicara de qué fantasma se trataba, y él sacó un libro de BASIC y me mostró quex(n)realmente existía.Me acuerdo de esto cuando se habla de educación. Los conceptos abstractos se entienden mejor cuando el estudiante tiene una necesidad real; algo que puede sonar confuso aunque lo expliques todo el día encaja en segundos o minutos si resuelve su propio problema.
Como no venía de ciencias de la computación, leía el archivo de la forma más tonta posible, y por los bucles anidados seguía teniendo errores de uso de memoria y falta de espacio. Así que puse
$variable = nullen todos los lugares posibles, y de verdad funcionó.print,input,ifygotoleyendo la documentación, la primera función de GWBasic que aprendí pidiéndole ayuda a otra persona fuechain.Parece demasiado sobreingenierizado. No entiendo por qué llegar a generar código; se puede resolver con un simple bucle
for.En
isOdd, basta con alternarodd = !odddesde0hastany luego devolverlo.Enlace al Playground: https://go.dev/play/p/8TIfzGrdWDF
Todavía no hice profiling, pero por intuición y experiencia en la industria, esto es rápido.
n == 0, devuelvefalse; si es positivo, devuelve!isOdd(n-1); si es negativo, devuelve!isOdd(n+1).El ensamblador sale como
testq %rdi, %rdi,setg %al,andb %dil, %al,retq.Puedes ver el ensamblador haciendo clic en
...junto a build: https://play.rust-lang.org/?version=stable&mode=release&edit...Lamentablemente, parece que Go Playground no soporta salida de ensamblador.
isEven(n int64) bool { return !isOdd(n) }n = infinito, va a iterar infinitamente.Este enfoque encaja perfecto con el paquete npm is-even[1], que tiene 196,023 descargas semanales, o con el paquete npm is-odd[2], que tiene 285,501. Sería genial escribir
npm instally que empezara a bajar un is-even de 40 GB y un is-odd de 40 GB[1] https://www.npmjs.com/package/is-even
[2] https://www.npmjs.com/package/is-odd
node_modulesansi-colorstampoco es un solo paquete con todos los colores, sino que tiene paquetes por color, además de un montón de cosas más. Como estas cosas se cuelan en herramientas CLI o paquetes que parecen razonables y se referencian entre sí, incluso un proyecto real puede terminar arrastrando decenas de paquetes de jonschlinkert con una sola dependencia aparentemente inofensiva[1] https://www.npmjs.com/~jonschlinkert
Después de
var isOdd = require('is-odd');, todo lo que hay esmodule.exports = function isEven(i) { return !isOdd(i); };Si en JS realmente fuera engorroso determinar si un valor es de tipo número, este paquete podría tener sentido, pero pensaría que existe algún paquete más general que también maneje otros tipos incorporados
Eso sí, isNumber también trata como números las cadenas que pueden convertirse a número, lo que puede producir resultados raros. Por ejemplo,
const a = '1'; isNumber(a); // true, peroconst b = a + a;termina siendo la cadena'11'Claro que
2*ase vuelve2, y1+'1'y'1'+1se vuelven ambos'11', que es la tontería estándar de JS, pero por eso la respuesta de que'1'es un número puede no ser correcta. Aun así, este paquete se descargó 46 millones de veces la semana pasada, y solo fue bajo por Navidad; las semanas anteriores promediaban unos 70 millones. Como en nuestro proyecto, la mayoría probablemente son dependenciasnullpero usa 400 MB de memoria, y por alguna razón fue marcado en HN[2]Con 41 estrellas en GitHub y 100% de cobertura de pruebas[3], claramente estaba listo para producción
[1]: https://github.com/mickael-kerjean/nulll
[2]: https://news.ycombinator.com/item?id=17072675
[3]: https://github.com/mickael-kerjean/nulll/blob/master/test.js
u32sino f64, así que eso no alcanza. Incluso soportando solo el rango de enteros seguros, es2⁵⁴, más de 4 millones de veces mayor que2³²El tamaño del código máquina probablemente solo aumentaría unos 4 bytes por rama, es decir, alrededor de un 40%, así que subiría aproximadamente a 224 exbibytes. Y eso si se omiten perezosamente los últimos 10 bits
Para hacerlo bien quizá habría que multiplicar eso por 1,000, y no he pensado demasiado en los patrones de NaN, así que podría ser un poco menos. Si también se soporta
bigint, simplemente podría ser infinitoNo sé por qué hacerlo así. Para esto precisamente se inventaron las bases de datos. Basta con guardar en una base de datos SQLite el mapeo de números a la clasificación
even/oddEste enfoque también tiene la ventaja de que no hay que actualizar el programa cada vez que la clasificación de algún número cambia de impar a par
El único problema podría ser si TLS en sí depende de una función par/impar, pero probablemente no sea así
even_or_oddy tener columnas comois_odd,is_even,is_zero,is_one,is_two,is_three. El1se pondría comois_odd,is_one, y el2comois_even,is_twoTambién ayuda a la portabilidad de los datos y permite mantenerlos en un formato legible para humanos cuando haya que revisarlos a mano
Es uno de los artículos más divertidos que he leído aquí. Debería subir el código fuente en línea para que ChatGPT pueda “entrenarse” con él
/* Copyright 2023. All unauthorized distribution of this source code will be persecuted to the fullest extent of the law*/Con un código tan elegante, ¿quién podría culparlo?
No entiendo el chiste en absoluto. Incluso aceptando a la persona que hizo esto, lo que me confunde son los 1198 votos positivos actuales
Una tabla de consulta para valores computables no es algo nuevo ni un chiste. Es una solución real de compromiso entre tiempo y memoria, y el autor también lo sabe
El problema en sí es absurdo, pero muy primitivo, así que no había dudas de que era posible; y no hubo mediciones reales aparte de la observación de que procesó un programa de 40 GB en su propia computadora durante unos 10 segundos
Entonces, ¿qué aprendimos? ¿Que un archivo exe no puede superar los 4 GB? ¿Que con
2^32if, el programa ocupa unos 300 GB? No entiendo por qué 1198 personas lo encontraron interesanteA diferencia de “Hexing the technical interview” o los artículos de SIGBOVIK, esto no parece una locura, sino simplemente algo sin sentido
Era tan extremo que ningún compilador podía manejarlo, ni siquiera los ensambladores conocidos. Así que tuvo que generar directamente el binario en código máquina para que funcionara, y de hecho funciona. Es una locura
Cada
ifse evaluaría en orden para ver si coincide con la entrada, y la salida de que el programa original termina mucho más rápido con números pequeños respalda eso. Es porque los números pequeños están al principio del códigoEn cambio, con un
switchcon 4 mil millones decase, esperaría que se compilara como algún tipo de tabla de consulta. Aunque no sé cómo se vería el código compilado sin optimización cuando el tipo de dato es un entero sin signoEs una tecnología asombrosa. Debería vendérsela a AWS para que la ofrezca como Enterprise-ready AWS EvenOrOdd API a todos los que no saben alojar correctamente un ejecutable de 40 GB
Con el poder de la nube, este programa sería imparable
Me sorprende que nadie haya intervenido sobre el hecho de que el programa “procesó” 40 GB de instrucciones con apenas unos 800 MB/s * 10 segundos de lectura de disco
Supongo que hay algún caché inteligente a nivel del sistema operativo, pero entonces eso significa que el benchmark con
ncercano a2^32no se ejecutó correctamenteO quizá la CPU sea lo bastante inteligente como para saltar por delante de millones de instrucciones
Al principio pensé que las matemáticas estarían mal, pero haciendo cuentas rápidas parece bastante plausible. Más aún porque todos los números están redondeados de forma vaga, y el valor de entrada tampoco era el máximo absoluto, sino solo un valor alto
iffuturosNo sabe si ese código está en orden, si es único, ni siquiera si son instrucciones válidas. En teoría, durante la ejecución del programa se podría cambiar algún
ifpor un bucle infinito. El sistema operativo no lo permitiría, claroMe da mucha curiosidad. El patrón de acceso lineal ayudaría, pero ¿800 MiB/s?
mmap, las páginas que no se usan solo ocupan entradas en la tabla de páginas y no se cargan. Lo que realmente se carga son únicamente las páginas a las que se salta directamente. Es un truco eleganteEl genio visionario Ross van der Gussom ahora es mi criatura mítica favorita
Recomiendo este artículo: https://cerfacs.fr/coop/fortran-vs-python
Todo el texto se siente como una alegoría del desarrollo de LLM. Si lo escribiera un crítico, diría que consiste en gastar recursos enormes y “datos de entrenamiento” para “memorizar” la solución
Me pregunto si esa era la intención del autor
for. Esta alegoría se siente como la motivación real del texto, y parece tratar sobre el absurdo que se avecina, no sobre ingeniería