1 puntos por GN⁺ 2 일 전 | 2 comentarios | Compartir por WhatsApp
  • 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

 
GN⁺ 1 일 전
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 de fread(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 cada fread llevaba a 65K llamadas al API de Windows ReadFile
    Mi código enganchaba la llamada al sistema ReadFile, y como esa llamada enganchada era más pesada que la ReadFile original, la carga se volvía tan lenta que era inutilizable
    La 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 ReadFile enganchado funcionara más rápido
    Lo 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

    • A comienzos y mediados de los 90 trabajé como diseñador de tarjetas/chips gráficos para Mac, y nuestro chip era el más rápido, pero algunos programas no aprovechaban nada por culpa de métodos tontos
      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*2 cada vez que escribía un carácter
      Habí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
    • Me hizo pensar en el parche comunitario de GTA Online de hace unos años
      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
    • Me pregunto qué software estaba tan mal implementado como para que fread(data, 1, 65536, fptr); terminara descompuesto en 65,536 lecturas de 1 byte
      Si 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 fread no hay manera de comunicárselo al llamador
    • Espera, ¿entonces fread(data, 1, sizeof(buffer), f); está mal?
      Yo siempre lo llamo así porque quiero leer sizeof(buffer) bytes individuales
      El 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 fread en la biblioteca estándar de Windows de hace 15 años, o si en realidad mi razonamiento está equivocado
    • Algunas partes de Windows Explorer, al borrar archivos, hacen una enorme cantidad de llamadas a ReadFile de 4 bytes sobre archivos tipo base de datos de seguimiento
      Si 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

    • También hay casos opuestos. Una actualización de seguridad de Windows rompió GTA San Andreas, que dependía de comportamiento indefinido
      https://silentsblog.com/2025/04/23/gta-san-andreas-win11-24h...
    • También da la impresión de que los drivers gráficos hacían mucho esto, o quizá todavía lo hacen
      Por lo menos meten optimizaciones separadas para cada juego y probablemente ajustan configuraciones y funciones que los desarrolladores del juego no lograron optimizar bien
    • La parte más interesante, si no recuerdo mal, es que para hacerlo funcionar incluyeron todo el asignador de memoria de Windows 3.11
      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
    • Escuché una historia de Sun; no sé si será cierta, pero era tan graciosa que daba para seguir repitiéndola
      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 recompilaron
      Pero 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

    • Tengo entendido que los drivers de GPU también hacen algo parecido, metiendo un montón de ajustes por juego para que corran más rápido
      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
    • Los paquetes de drivers de GPU ya son casi una colección de soluciones alternativas para código malo de motores de juego
      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.exe
    • Alguien también tiene que implementar en Linux las mismas soluciones alternativas que hacen los drivers de Windows, y la capa de traducción es un buen lugar para meter ese tipo de tratamiento
      Por ejemplo, se puede ver en https://github.com/HansKristian-Work/vkd3d-proton/blob/938d7...
    • Una gran parte de las actualizaciones de drivers de GPU son en realidad correcciones específicas para juegos, y con Windows Update pasa igual
      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

    • Si no recuerdo mal, en Windows hay que probar todas las páginas de la pila en orden
      No puedes simplemente restar un valor a ESP/RSP; si no tocas cada página en secuencia, ocurre un page fault u otra excepción
  • Siendo 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

  • 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és return this->field
    Con 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

    • Si no recuerdo mal, una de las viejas anécdotas parecidas de Raymond Chen era sobre SimCity 2000
      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

    • Si reduces a la mitad la cantidad de cómputo y a una cuarta parte la memoria, dentro de 10 años el rendimiento será el doble que el de hoy
      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

    • Sí, pero tenía entendido que DEC había hecho el traductor FX!32 para Alpha
      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?

    • Hay varios puntos a considerar
      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
 
GN⁺ 2 일 전
Opiniones en Lobste.rs
  • Me gustó bastante un comentario donde le explicaban a Raymond Chen el loop unrolling

    • Ese comentario podría haber estado escrito no solo para el autor del blog, sino también para el lector común
      No todos los que leen este tipo de textos conocen todo el contexto, así que muchos agradecen pistas para aprender más
    • No vi ese comentario, pero parece que lo borraron. Aun así, si era Raymond Chen, ese tipo es una verdadera leyenda
      https://joelonsoftware.com/2004/06/…
    • Me recordó a aquella vez, hace décadas en Slashdot, cuando alguien trató de explicarle temas de Perl a larry@wall.org
  • Creo que esto fue en Alpha. Se metió muchísimo trabajo en el emulador de x86 para esa plataforma