⏱️ Lectura: 13 min
Si alguna vez miraste el output de un compilador de C++ o Rust en Compiler Explorer, seguro te topaste con una escena que se repite hasta el hartazgo: casi cada función que retorna cero arranca con xor eax, eax. La instrucción xor eax eax es, sin discusión, el idiom universal para poner un registro en cero en x86. Pero hay una pregunta que pocos se hacen: ¿por qué xor y no sub? Matemáticamente son equivalentes, ocupan los mismos bytes y ejecutan en los mismos ciclos. Y sin embargo, uno ganó la batalla cultural y la otra quedó como curiosidad.
📑 En este artículo
- El problema: cómo poner un registro en cero
- Dos candidatas igualmente válidas: xor y sub
- Las diferencias sutiles: los flags de EFLAGS
- Por qué xor ganó: el efecto bola de nieve
- La jugada de Intel: zero-idiom detection en silicio
- Flujo interno de zero-idiom detection
- El detalle que selló la victoria: soporte asimétrico entre fabricantes
- El caso Itanium: cuando xor no alcanza
- ¿Esto me importa si no escribo assembly?
- Un experimento rápido en tu máquina
- Preguntas frecuentes
- ¿xor eax, eax es más rápido que sub eax, eax en CPUs modernas?
- ¿Por qué mov eax, 0 es peor si es más legible?
- ¿Qué pasa con arquitecturas que sí tienen registro cero, como ARM64?
- ¿El xor reg, reg afecta a los flags? ¿Puedo usarlo antes de un jz?
- ¿Hay casos donde deba evitar xor reg, reg y usar mov reg, 0?
- ¿Los procesadores RISC-V también tienen este tema?
- Referencias
Este artículo desarma esa decisión desde el principio: por qué mov eax, 0 no es la opción óptima, en qué se diferencian realmente xor y sub cuando pones un registro en cero, qué hacen las CPUs modernas detrás de escena con estos zero idioms, y por qué, para un desarrollador LATAM que escribe código de alto nivel en 2026, entender este detalle sigue siendo útil aunque nunca escribas assembly a mano.
El problema: cómo poner un registro en cero
En la arquitectura x86 no existe un registro dedicado al valor cero. A diferencia de MIPS, que tiene $zero siempre apuntando a 0, o de ARMv8, que tiene xzr/wzr, en x86 si queremos que eax valga cero tenemos que hacerlo ab initio: crear el cero con una operación.
La opción más intuitiva para un humano es la asignación directa:
mov eax, 0
Funciona, es clarísimo de leer, y es lo que cualquiera esperaría. El problema es el tamaño. En x86-64 esa instrucción se codifica con 5 bytes: un opcode y luego los 4 bytes de la constante inmediata 0x00000000. Cinco bytes para setear un cero es caro, sobre todo si se repite miles de veces en un binario. En una era donde el icache (caché de instrucciones) y la densidad del código afectan el rendimiento real, desperdiciar bytes es desperdiciar ancho de banda.
Por eso los compiladores buscaron alternativas más compactas. Y encontraron dos candidatas prácticamente empatadas.
Dos candidatas igualmente válidas: xor y sub
Cualquier operación matemática que aplicada a un valor consigo mismo dé cero es candidata. Las dos más obvias son:
xor eax, eax ; A XOR A = 0 siempre
sub eax, eax ; A - A = 0 siempre
Ambas se codifican en solo 2 bytes (opcode + ModR/M), sin inmediato. Ambas ejecutan en 1 ciclo en prácticamente cualquier x86 moderno. Ambas destruyen el contenido previo de eax independientemente de lo que hubiera. Son, desde el punto de vista del resultado, intercambiables.
Aquí está el ejemplo mínimo que lo muestra:
; Versión 1 — el idiom popular (2 bytes)
xor eax, eax
; Versión 2 — igual de válida (2 bytes)
sub eax, eax
; Versión 3 — la "obvia" (5 bytes)
mov eax, 0
Entonces, si son equivalentes en costo y resultado, ¿por qué nadie usa sub? La respuesta corta es: cultura y efecto bola de nieve. La respuesta larga requiere mirar los flags del procesador.
Las diferencias sutiles: los flags de EFLAGS
Cuando ejecutás una instrucción aritmética o lógica en x86, el procesador actualiza el registro EFLAGS, que contiene bits de estado que afectan saltos condicionales posteriores. Comparemos qué le pasa a cada flag después de cada idiom:
- OF (Overflow Flag) — ambos lo limpian.
- SF (Sign Flag) — ambos lo limpian.
- ZF (Zero Flag) — ambos lo setean (el resultado es cero).
- PF (Parity Flag) — ambos lo setean.
- CF (Carry Flag) — ambos lo limpian.
- AF (Auxiliary Carry Flag) — acá está la única diferencia:
xorlo deja indefinido, mientras quesublo limpia a cero.
Es decir, técnicamente sub eax, eax tiene un comportamiento más limpio con respecto a los flags, porque deja AF en un estado definido. Pero AF es un flag tan raramente consultado (se usa sobre todo en operaciones BCD, algo casi extinto) que en la práctica la diferencia es irrelevante.
📌 Nota: El AF (Auxiliary Carry) se usa para aritmética Binary-Coded Decimal. Si hoy escribís código moderno en C, Rust, Go, Java o Python, la probabilidad de que tu stack lo consulte es prácticamente cero.
Si ambos son equivalentes en tamaño, velocidad y flags útiles, ¿cómo se decidió la batalla? Con una mezcla de cultura, inercia y decisiones de los fabricantes de silicio.
Por qué xor ganó: el efecto bola de nieve
Raymond Chen, veterano de Windows y autor del blog The Old New Thing, plantea una hipótesis razonable: xor y sub arrancaron con popularidad similar, pero xor se adelantó por una pequeñez —quizás sonaba más “clever”— y una vez que los primeros compiladores empezaron a emitirlo, se disparó el efecto rebaño.
La dinámica es clásica en ingeniería: si un desarrollador está indeciso entre dos opciones igualmente válidas y ve que el compilador de Intel o GCC elige una, piensa “los que escribieron el compilador saben más que yo” y se inclina por esa. Cada vez que alguien hace esa elección, refuerza el patrón para el siguiente. En pocas iteraciones, xor eax, eax se convirtió en el idiom de facto, y sub quedó como curiosidad o firma personal de algún programador contestatario.
Un detalle delicioso que cuenta Chen: tenía un colega que usaba sub reg, reg por preferencia personal, y cuando Raymond leía código assembly podía reconocer inmediatamente quién era el autor por esa sola decisión. Es el equivalente a reconocer un estilo de escritura por la puntuación.
La jugada de Intel: zero-idiom detection en silicio
Acá es donde la historia se pone interesante para los que venimos del mundo high-level. A medida que xor reg, reg y sub reg, reg se volvieron tan frecuentes en el código generado, Intel decidió optimizarlos en el decoder del frontend. Las CPUs x86 modernas (desde hace muchas generaciones) detectan estos patrones y los tratan de forma especial:
- Rename al registro cero interno — en lugar de ejecutar realmente la operación en una unidad aritmética, la CPU redirige el registro destino a un “registro cero” virtual del archivo de renombre.
- Cero ciclos efectivos — como la operación no necesita ALU, en cierto sentido “no cuesta nada” ejecutarla.
- Ruptura de cadena de dependencias — normalmente, el resultado de un
xordepende de sus inputs. Pero al detectar que el input es el mismo registro, la CPU sabe que el output es cero sin importar el estado previo. Esto rompe la dependencia con la última escritura aeax, permitiendo al motor out-of-order paralelizar mejor.
Este último punto es el verdadero golpe de optimización. En un pipeline moderno, esperar a que se resuelva la última escritura de un registro para poder “reiniciarlo” a cero es una pérdida de ciclos enorme. El zero-idiom detection elimina esa espera.
💭 Clave: xor eax, eax no es solo “poner cero en eax”. Le dice al procesador “quiero un cero fresco y no me importa nada de lo anterior”. Esa segunda parte es la que permite a la CPU paralelizar agresivamente.
Flujo interno de zero-idiom detection
flowchart LR
A["Instrucción: xor eax, eax"] --> B["Frontend decoder"]
B --> C{"¿Es zero-idiom?"}
C -- "Sí" --> D["Rename a registro cero"]
C -- "No" --> E["Pipeline normal con ALU"]
D --> F["Rompe dependencia previa"]
F --> G["0 ciclos efectivos"]
E --> H["Espera dependencias"]
H --> I["Ejecuta en ALU"]
El detalle que selló la victoria: soporte asimétrico entre fabricantes
Si Intel hubiera añadido detección para xor y para sub simultáneamente, probablemente la batalla habría quedado en empate técnico. Pero pasó algo más interesante: no todos los fabricantes de x86 optimizaron ambas igual.
Según Agner Fog, autor de los manuales de optimización de CPU más respetados de la industria (agner.org/optimize):
- VIA Nano 2000 solo reconocía
xorcomo zero-idiom;subno obtenía el tratamiento especial hasta el Nano 3000. - AMD K10 y posteriores soportan ambos, pero los manuales oficiales de AMD de 2004 y 2014 ni siquiera mencionan
suben este contexto: recomiendan explícitamentexor. - Intel recomienda
xoren todas sus guías de optimización, aunque soporte ambos en hardware.
Esta asimetría es la que cierra el caso. Un compilador portable que quiera generar el código más rápido en todo el ecosistema x86 tiene que elegir el denominador común, y ese denominador es xor. sub podría ser igual de rápido en tu CPU, pero no podés garantizar que lo sea en la CPU de tu usuario.
El caso Itanium: cuando xor no alcanza
Un paréntesis histórico interesante: en Itanium (la arquitectura IA-64 de Intel que nunca terminó de despegar), el truco de xor no funciona. Itanium tiene un bit NaT (“Not a Thing”) en cada registro, que marca valores que todavía no están listos. Las operaciones aritméticas y lógicas propagan NaT, no lo resetean.
Por suerte, Itanium sí tiene un registro cero dedicado (como MIPS y ARMv8), así que la solución es simplemente copiar cero al destino:
mov r10 = r0 ; r0 es el registro cero físico
Es una muestra de cómo decisiones arquitectónicas (tener o no un registro cero) cambian los idioms idiomáticos de cada plataforma.
¿Esto me importa si no escribo assembly?
Legítima pregunta. Si escribís TypeScript, Python o Go, no vas a tipear xor eax, eax jamás. Pero este caso es un ejemplo canónico de cómo las convenciones de bajo nivel afectan al hardware que corre tu código high-level. Tres takeaways prácticos:
- Los compiladores toman decisiones por vos. GCC, Clang, rustc y MSVC emiten
xor reg, regpara zeroing por defecto. Si alguna vez ves un binario raro consub, sospechá assembly escrito a mano o un toolchain antiguo. - El hardware premia patrones reconocibles. El zero-idiom detection existe porque el patrón era estadísticamente dominante. Hay decenas de optimizaciones similares: move elimination, loop stream detection, branch prediction por patrones. Escribir código que “se parece al código normal” suele correr más rápido que código exótico.
- La cultura gana a la ingeniería pura. Dos soluciones técnicamente equivalentes pueden divergir dramáticamente en adopción por un detalle cultural. Pasa con
xor/sub, pero también pasa con tabs vs espacios, CamelCase vs snake_case, o React vs Vue. La victoria no siempre es por mérito puro.
💡 Tip: Si querés ver cómo tu compilador genera código, abrí godbolt.org, pegá una función que retorne 0 en C o Rust, y elegí un target x86-64. Vas a ver xor eax, eax en las primeras líneas. Es un reflejo de 40 años de cultura compilada en 2 bytes.
Un experimento rápido en tu máquina
Si querés ver este fenómeno sin salir de tu laptop, compilá este programa trivial en C:
// zero.c
int main(void) {
return 0;
}
Y luego inspeccioná el assembly generado en cada plataforma:
# Linux (gcc)
gcc -O2 -S zero.c -o zero.s && cat zero.s
# macOS (clang)
clang -O2 -S zero.c -o zero.s && cat zero.s
# Windows (MSVC desde Developer Command Prompt)
cl /O2 /FA zero.c
type zero.asm
En los tres casos vas a encontrar algo como xor eax, eax justo antes del ret. Tres compiladores distintos, tres sistemas operativos distintos, misma decisión. Esa uniformidad es precisamente lo que las CPUs modernas aprovechan para optimizar.
📖 Resumen en Telegram: Ver resumen
Preguntas frecuentes
¿xor eax, eax es más rápido que sub eax, eax en CPUs modernas?
En Intel y AMD modernas (últimos 15 años), ambas se tratan como zero-idiom y tienen el mismo costo efectivo. En CPUs más antiguas o menos comunes (como VIA Nano 2000), xor podía ser más rápido porque era el único reconocido. Por compatibilidad histórica, el idiom seguro es xor.
¿Por qué mov eax, 0 es peor si es más legible?
Ocupa 5 bytes en lugar de 2, no rompe dependencias con escrituras previas a eax, y no se beneficia del zero-idiom detection. En código caliente (hot path) acumular esos bytes impacta la caché de instrucciones. En código frío no importa, pero los compiladores optimizan para el caso general.
¿Qué pasa con arquitecturas que sí tienen registro cero, como ARM64?
En ARMv8/AArch64 existen xzr (64-bit) y wzr (32-bit), que siempre valen cero. Para poner un registro en cero basta un mov desde xzr, o directamente usar xzr como operando. No hay necesidad del truco xor porque el hardware provee el cero gratis.
¿El xor reg, reg afecta a los flags? ¿Puedo usarlo antes de un jz?
Sí afecta flags: setea ZF (Zero Flag) a 1 y limpia OF, SF, CF. Un jz inmediatamente después saltaría siempre, lo cual rara vez es útil. Si querés poner en cero sin tocar flags, necesitás otra estrategia (por ejemplo, mantener un registro previamente zereado y copiarlo con mov).
¿Hay casos donde deba evitar xor reg, reg y usar mov reg, 0?
Sí: cuando necesitás preservar los flags exactamente como estaban, ya que mov con inmediato no modifica EFLAGS. Es un caso de nicho pero real en rutinas críticas que ejecutan aritmética y consultan flags más adelante sin recomputarlos.
¿Los procesadores RISC-V también tienen este tema?
No. RISC-V tiene un registro dedicado x0 que siempre vale cero, igual que MIPS y ARM64. Para zeroing se usa add reg, x0, x0 o similar, y el decoder lo trata especial. RISC-V hereda esa decisión de diseño precisamente para evitar el baile cultural que tuvo x86.
Referencias
- The Old New Thing (Raymond Chen) — Why xor over sub for zeroing — artículo original que inspiró este análisis.
- Compiler Explorer (Matt Godbolt) — herramienta web para ver el assembly que genera cualquier compilador, ideal para verificar estos idioms.
- Agner Fog — Optimization manuals — referencia técnica sobre microarquitectura x86 y detalles de zero-idiom detection por fabricante.
- Wikipedia — x86 architecture — panorama general de la arquitectura, ISA y evolución histórica.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
0 Comentarios