El equipo del emulador x86 encontró código tan malo que lo arregló durante la emulación
(devblogs.microsoft.com)- El emulador x86-32 generaba código nativo mediante traducción binaria para ejecutar código x86-32 en otros procesadores, y ofrecía una gran mejora de rendimiento frente al enfoque de intérprete
- Este emulador puede entenderse como una arquitectura que trata x86-32 como si fuera bytecode y hace que el emulador funcione como un compilador JIT
- Un programa tenía que asignar e inicializar aproximadamente 64 KB de memoria en la pila, y la forma habitual era hacer primero un stack probe, luego reducir el puntero de pila e inicializar la memoria con un bucle pequeño
- El compilador de ese código generó, en lugar de un bucle, 65,536 instrucciones individuales de escritura de bytes, y como cada instrucción ocupaba 4 bytes, se necesitaban 256 KB de código para inicializar 64 KB de datos
- El equipo del emulador añadió al traductor código especial para detectar esta función y reemplazarla por un bucle corto equivalente
Contexto: emulador x86-32 y traducción binaria
- Windows llegó a incluir un emulador de procesador x86-32 para sistemas que se ejecutaban en procesadores distintos de x86-32
- El texto original no especifica a qué procesador se aplicó este caso
- Ese emulador usaba traducción binaria para generar código nativo con un comportamiento equivalente al código x86-32 original
- Este método ofrecía una mejora de rendimiento considerable frente a la emulación basada en intérprete
- Puede entenderse viendo x86-32 como bytecode y al emulador como un compilador JIT
Código problemático: inicialización de 64 KB de memoria en la pila
- Un programa necesitaba asignar aproximadamente 64 KB de memoria en la pila e inicializarla
- La forma estándar era realizar primero un stack probe para comprobar si se podían usar esos 64 KB de memoria
- Después, lo normal era restar 65,536 al puntero de pila e inicializar la memoria con un bucle pequeño y ajustado
Desenrollado excesivo del bucle por parte del compilador
- El compilador que generó ese código no creó un bucle para inicializar cada byte
- En su lugar, expandió el bucle en 65,536 instrucciones individuales de “escritura de un byte en memoria”
- Cada instrucción tenía una longitud de 4 bytes
- Como resultado, se necesitaron 256 KB de código para inicializar 64 KB de datos
Respuesta del equipo del emulador
- El equipo del emulador añadió al traductor código especial para detectar esta función
- La función detectada se reemplazaba por un bucle corto que realizaba un comportamiento equivalente
- En vez de traducir sin más el código original del programa, este proceso transformaba durante la emulación los patrones de código ineficientes en una forma más compacta
2 comentarios
Comentarios de Hacker News
Me hizo recordar cuando, hace 15 años, trabajaba en una tecnología que descargaba juegos bajo demanda enganchando llamadas del SO
Algunos juegos tardaban originalmente 15~20 segundos en cargar, pero al aplicar esta tecnología pasaban a tardar 3~5 minutos aunque los datos ya estuvieran completamente descargados
Al investigar, descubrimos llamadas como
fread(data, 65536, 1, fptr);en lugar defread(data, 1, 65536, fptr);, de modo que en ese momento, para archivos de varios MB, eso terminaba convertido en 65K lecturas de 1 byte, y cadafreadllevaba a 65K llamadas al API de WindowsReadFileMi código enganchaba la llamada al sistema
ReadFile, y como esa llamada enganchada era más pesada que laReadFileoriginal, la carga se volvía tan lenta que era inutilizableLa solución sencilla era cambiar los argumentos de ciertas llamadas, y la solución larga fue agregar una caché interna para que, con datos ya presentes en disco, el
ReadFileenganchado funcionara más rápidoLo curioso es que, al probarlo con más juegos, había muchos así, y después del arreglo con caché cargaban incluso más rápido que antes. Sinceramente, con solo cambiar los argumentos podrían haber leído todo en segundos, así que hasta me pregunté si los desarrolladores lo hacían a propósito para que pareciera que pasaban muchas cosas durante la carga
PageMaker invalidaba la caché de fuentes en cada vuelta del loop principal, y la combinación de Quark con ATM hacía algo tipo
n*2cada vez que escribía un carácterHabía hardware dedicado para acelerar el renderizado de texto, pero el software lo arruinaba todo; hasta pensamos en hacer un plugin para corregirlo, pero mantenerlo era complicado, así que al final fuimos detrás de los fabricantes de las apps para explicarles el problema
Excel también borraba de blanco la misma zona hasta 9 veces antes de dibujar un píxel negro; eso sí lo hicimos muy rápido, y no se lo dijimos
En esa época los framebuffers de 24 bits eran tan lentos que, antes de que existiera hardware de aceleración gráfica, la gente volvía a 8 bits para poder trabajar, y lograr que 24 bits/true color se usara de forma cotidiana fue un gran avance
El juego sufría tiempos de carga de más de 10 minutos, siguió así durante años y con el tiempo se puso peor
Alguien lo analizó y descubrió que el 80% del tiempo de carga se iba en leer el archivo con la lista de la tienda dentro del juego, que si mal no recuerdo pesaba varias decenas de MB
Para cada entrada usaban literalmente el algoritmo de Schlemiel the Painter, leyendo desde el inicio un byte a la vez
Con un parche pequeño que solo recordaba la posición del último elemento encontrado, el tiempo total de carga se redujo 80%, pasando de más de 10 minutos a menos de 3
fread(data, 1, 65536, fptr);terminara descompuesto en 65,536 lecturas de 1 byteSi el código pidió hasta 65,536 elementos de 1 byte, no entiendo por qué habría que dividir eso en 65,536 llamadas
Además, ese cambio también altera el comportamiento. La llamada original puede leer entre 0 y 65,536 bytes, mientras que la nueva solo puede leer 0 o 65,536 bytes
Viendo el código fuente de algunas implementaciones, parece que aunque la entrada no sea un múltiplo entero del tamaño del elemento, el búfer de salida igual se llena con elementos parciales, pero con el valor de retorno de
freadno hay manera de comunicárselo al llamadorfread(data, 1, sizeof(buffer), f);está mal?Yo siempre lo llamo así porque quiero leer
sizeof(buffer)bytes individualesEl tamaño del búfer es un valor incidental, no el tamaño del elemento que quiero leer del archivo, así que semánticamente me parece más raro expresar “leer un elemento de tamaño
sizeof(buffer)”Me pregunto si este era un caso de una mala implementación de
freaden la biblioteca estándar de Windows de hace 15 años, o si en realidad mi razonamiento está equivocadoReadFilede 4 bytes sobre archivos tipo base de datos de seguimientoSi borras muchos archivos, eso se acumula rapidísimo
SimCity tenía un bug de use-after-free, y Microsoft lo parchó en Windows 95
Desde el punto de vista del cliente, era mucho más fácil que Maxis tuviera que intercambiar copias del juego para corregirlo
https://silentsblog.com/2025/04/23/gta-san-andreas-win11-24h...
Por lo menos meten optimizaciones separadas para cada juego y probablemente ajustan configuraciones y funciones que los desarrolladores del juego no lograron optimizar bien
No sé bien cómo funcionan las asignaciones a nivel de SO, pero me sorprende que no existan wrappers aparte, como dgVoodoo o dxWrapper, para este tipo de problemas
Bastantes juegos viejos de Windows como Need for Speed 1~4 se niegan a correr en SO modernos por estrategias de gestión de memoria bastante agresivas
Cuando decidieron que una beta temprana del sistema operativo ya era lo bastante estable y suficientemente probada como para lanzarla a clientes, dicen que cambiaron la cadena de versión de algo como
"SunOS2.1BETA"a"SunOS2.1FCS"y recompilaronPero al pasar una cadena de versión de 12 caracteres a una de 11, se desalineó alguna estructura de datos importante en alguna parte del kernel, y por culpa de accesos de memoria desalineados en 68k todo el SO se volvió muchísimo más lento
Parece que esto se ve más seguido desde que Proton y Wine se volvieron importantes en la comunidad Linux
Algunos juegos salen con ports para PC en un estado tan malo que la capa de compatibilidad puede meter hotfixes para mejorar el rendimiento
Mientras tanto, los usuarios que usan ese software en la plataforma original tienen que seguir sufriendo, y me viene a la mente Elden Ring
Se siente como un enfoque frágil que componentes externos, que deberían ser independientes del software en ejecución, terminen incluyendo todo tipo de manejos ad hoc para arreglar problemas que deberían corregirse del lado de los usuarios del driver original
Un empleado de Nvidia dijo hace tiempo que una de las formas más fáciles de sacar unos cuantos frames más en una PC vieja era cambiarle el nombre al ejecutable del juego a
hl2.exePor ejemplo, se puede ver en https://github.com/HansKristian-Work/vkd3d-proton/blob/938d7...
Windows 95 parchó ese bug para que SimCity funcionara
No es exacta la explicación de que la forma estándar de asignar 64 KB en la pila consiste en usar un stack probe para verificar si esos 64 KB son posibles, luego restar 65536 al stack pointer e inicializarlo con un bucle pequeño
En realidad, la forma común de asignar 64 KB en la pila es simplemente asumir que se puede, restar 64 KB al stack pointer y esperar que salga bien
La mayoría de las asignaciones en la pila del mundo real no se verifican
No puedes simplemente restar un valor a
ESP/RSP; si no tocas cada página en secuencia, ocurre un page fault u otra excepciónSiendo justos, también es posible que el desarrollador hubiera activado al compilar una bandera de optimización que desenrolla todos los bucles sin excepción
Estoy de acuerdo en que es absurdo que un compilador siquiera soporte una bandera así, pero eran los años 80 y 90
https://www.reddit.com/r/cpp/comments/1i36ahd/is_this_an_msv...
Mientras hacía un transpilador que convierte ensamblador de Nand2tetris a WebAssembly, me topé con un bug de corrupción de memoria realmente desesperante
Ya no podía resolverlo, así que revisé un programa escrito por otra persona que usaba en las pruebas y resultó que hacía
dealloc(this)y despuésreturn this->fieldCon el asignador original eso funcionaba porque liberar no tocaba la memoria, pero mi asignador sobrescribía ese campo con información de contabilidad durante la liberación, así que el valor devuelto dejaba de ser el esperado y poco después el programa se caía
A diferencia del caso del texto original, yo sí tenía margen para simplemente corregir el programa de prueba
Usaba el truco de “liberar memoria y volver a usarla inmediatamente”, que funcionaba bien en DOS, pero desde Windows 95 era un gran tabú
El juego era tan común que Windows tuvo que meter una regla especial para que corriera
Dave Jones dio en una conferencia del kernel de Linux una serie de charlas llamada “Why user space sucks”, y había muchos ejemplos de este tipo
Normalmente eran casos de llamadas al sistema tontas y redundantes
Pero desde la perspectiva de alguien que mira mucho trazado de instrucciones, creo que también se podría escribir algo sobre por qué el código del kernel de Linux deja bastante que desear
Lo que más me molesta últimamente es la forma en que Linux recorre bitmasks de CPU: es una tarea bastante común, pero por una cadena de cambios y decisiones desafortunadas hoy se necesitan más de 16 instrucciones para encontrar el siguiente bit
El conjunto de instrucciones x86 tiene una sola instrucción para hacer eso, pero el código creció tanto que hasta terminó moviéndose a una función aparte, con overhead adicional
No puedo dejar de pensar en todo el código no optimizado que hay alrededor
Durante los últimos 20 o 30 años, el rendimiento de procesadores y memoria mejoró más rápido de lo que nosotros corregíamos las ineficiencias que creamos, así que aceptamos silenciosamente la idea de que no hace falta eficiencia en todas partes
Puede que compiladores, emuladores y algunas partes importantes del código se hayan hecho con otra mentalidad, pero la app o sitio web promedio simplemente desperdicia recursos por todos lados y espera que todo salga bien
Si la IA, que tiene fama de proponer soluciones ineficientes incluso para problemas simples, termina escribiendo aún más código, parece que este problema se volverá más común
Ojalá se optimice del lado de la IA y la gente que la usa, que es donde nace el problema, en vez de arreglarlo con heurísticas en compiladores, motores y kernels de la plataforma
Yo trabajo con sistemas embebidos de la vieja escuela, y la hinchazón del software de escritorio está a un nivel de locura
Las funciones que de verdad necesitan refactorización pueden reducirse de tamaño y mejorar su rendimiento al mismo tiempo, y hay ingenieros mucho más eficientes que yo
Supongo que la arquitectura nativa en cuestión habría sido Alpha
Parecía la que tenía mejor soporte
Quizá Raymond se refería a esa gente y no quería mencionar que no eran de Microsoft
Desenrollar bucles es una optimización básica del compilador y, según el código máquina y el conjunto de instrucciones del procesador, puede ser más rápido incluso considerando el costo de control de ejecutar condicionales, saltos y movimientos entre registros
En este texto falta el análisis de por qué sería así
Si a alguien no le gustó y le resultó molesto, eso también parece una razón igual de tonta
A simple vista, 256 KB de código de inicialización suenan absurdos, pero ¿y si en realidad era más rápido?
Aquí el artículo probablemente se refiere a un bucle de inicialización compacto cuyo cuerpo quizá sea de una sola instrucción
Las optimizaciones de hardware que hacen que un bucle así se ejecute de forma equivalente a su versión desenrollada son tan básicas que ya se daban casi por sentadas incluso en CPUs de hace 30 años
Hablando en serio, es una optimización del nivel de algo que uno mismo podía implementar en una clase universitaria de “Introducción a Verilog”, sin siquiera ser ingeniero de hardware
También importa con qué frecuencia se ejecuta este código. Si corre una sola vez durante la carga del programa, a nadie le importan 2 microsegundos menos de tiempo de carga
Si fuera una ruta crítica sensible al tiempo, como el bucle del juego o el hilo de renderizado de la GUI, entonces la optimización sí importa, pero aun así hay que tener en cuenta el punto anterior sobre el hardware
En esa época el almacenamiento tampoco era barato. 256 KB de código son el 18% de un disquete de 1.44 MB y el 35% de uno de 720 KB
Opiniones en Lobste.rs
Me gustó bastante un comentario donde le explicaban a Raymond Chen el loop unrolling
No todos los que leen este tipo de textos conocen todo el contexto, así que muchos agradecen pistas para aprender más
https://joelonsoftware.com/2004/06/…
Creo que esto fue en Alpha. Se metió muchísimo trabajo en el emulador de x86 para esa plataforma