Durante décadas, la educación formal en informática ha inculcado un dogma casi incuestionable: el buen software modela el mundo real mediante jerarquías de clases orientadas a objetos. Nos enseñaron a crear sustantivos abstractos, empaquetar datos y métodos en una misma cápsula y ocultar los detalles de almacenamiento tras getters, setters e interfaces polimórficas.
Esta cosmovisión produce diagramas UML impecables en los libros de texto, pero choca brutalmente contra la realidad del silicio moderno. En lenguajes de alto rendimiento como Zig, C++ y Rust, la comunidad de ingenieros de sistemas está rescatando con fuerza los principios del Diseño Orientado a Datos (Data-Oriented Design, DOD)1 Mike Acton formuló las bases fundacionales del movimiento en su mítica conferencia de la CppCon 2014: "El propósito de todo programa es transformar datos de una forma a otra. Si no entiendes los datos, no entiendes el problema. Y si no entiendes el hardware, no puedes razonar sobre el coste de la transformación"..
El motivo de este viraje no es estético: es pura física de semiconductores.
El abismo de latencia entre el cómputo y la memoria
Una unidad aritmético-lógica (ALU) de un procesador contemporáneo tarda aproximadamente 0,5 nanosegundos (un solo ciclo de reloj) en sumar dos enteros de 64 bits. En contraste, acceder a un dato alojado en la memoria RAM principal (DRAM) requiere entre 70 y 100 nanosegundos.
Para la CPU, cruzar el bus hacia la memoria principal equivale a esperar más de doscientos ciclos ociosos sin hacer nada:
[Operación ALU en registro] : 0.5 ns (1 ciclo)
[Acierto en Caché L1] : 1.0 ns (4 ciclos)
[Acierto en Caché L2] : 3.5 ns (12 ciclos)
[Acierto en Caché L3] : 15.0 ns (40 ciclos)
[Fallo de caché a RAM (DRAM)]: 80.0 ns (200+ ciclos de espera)
Para mitigar este abismo, los procesadores no leen bytes individuales de la memoria: cada vez que el programa solicita un puntero, el controlador de hardware transfiere una línea de caché completa de 64 bytes contiguos (cache line)2 Ulrich Drepper y Casey Muratori han demostrado empíricamente que la mayor parte del software empresarial contemporáneo desperdicia más del 80% de su tiempo de ejecución con las unidades de cómputo paralizadas a la espera de que la memoria entregue líneas de caché solicitadas por persecución de punteros.. Si los bytes adyacentes contienen los siguientes datos que el algoritmo necesita procesar, la CPU opera a máxima velocidad. Si los datos están dispersos, la CPU se asfixia en fallos de caché continuos (cache misses).
La trampa del Array de Estructuras (AoS) en la POO clásica
La Programación Orientada a Objetos tradicional agrupa todos los atributos de una entidad en una misma estructura o clase:
// Modelo POO clásico: Array de Estructuras (AoS)
const Particle = struct {
x: f32, // 4 bytes
y: f32, // 4 bytes
z: f32, // 4 bytes
vx: f32, // 4 bytes
vy: f32, // 4 bytes
vz: f32, // 4 bytes
mass: f32, // 4 bytes
color_rgba: u32, // 4 bytes
id: u64, // 8 bytes
name: [32]u8, // 32 bytes
}; // Total: 68 bytes por partícula (alineado a 72 o 128 bytes)
var particles: [100_000]Particle = undefined;
Imaginemos que necesitamos actualizar las posiciones de las partículas aplicando la velocidad en cada fotograma del motor de simulación. La lógica solo necesita operar sobre x, y, z y vx, vy, vz (24 bytes).
Al iterar sobre el array de partículas particles[i].x += particles[i].vx, el hardware carga una línea de 64 bytes que viene saturada de campos irrelevantes para este cálculo: color_rgba, id y los 32 bytes del name. El ancho de banda del bus de memoria se satura transfiriendo datos inútiles que la CPU descarta de inmediato. Peor aún: como cada partícula ocupa más de 64 bytes, cada una de las 100.000 iteraciones desencadena un fallo de caché forzado.
La solución del diseño orientado a datos: Estructura de Arrays (SoA)
El diseño orientado a datos cambia la perspectiva: los datos no se organizan según su filiación conceptual en el mundo real, sino según su frecuencia y patrón de acceso temporal en la memoria.
En lugar de un Array de Estructuras (AoS), organizamos la memoria como una Estructura de Arrays (Structure of Arrays, SoA):
// Modelo orientado a datos: Estructura de Arrays (SoA)
const ParticleSystem = struct {
x: [100_000]f32,
y: [100_000]f32,
z: [100_000]f32,
vx: [100_000]f32,
vy: [100_000]f32,
vz: [100_000]f32,
mass: [100_000]f32,
// Los datos no necesarios para la cinemática se aíslan:
color_rgba: [100_000]u32,
id: [100_000]u64,
name: [100_000][32]u8,
};
Al actualizar las coordenadas horizontales, el bucle recorre el vector contiguo x y el vector vx:
for (system.x, system.vx) |*pos_x, vel_x| {
pos_x.* += vel_x * dt;
}
El impacto en el hardware es demoledor:
1. Aprovechamiento total de la línea de caché: Cada línea de 64 bytes cargada desde memoria contiene exactamente dieciséis flotantes f32 contiguos que van a ser procesados uno detrás de otro. El desperdicio de ancho de banda se reduce a cero.
2. Prebúsqueda por hardware (Hardware Prefetching): Como el acceso a la memoria es estrictamente lineal y predecible, los prefetchers de la CPU detectan el patrón y cargan las siguientes líneas de caché antes de que el código las solicite. La latencia de la RAM se amortiza completamente.
3. Vectorización SIMD automática: El compilador puede empaquetar ocho operaciones de suma en una sola instrucción vectorial AVX2 de 256 bits (_mm256_add_ps), procesando ocho partículas por ciclo de reloj3 En la arquitectura AoS clásica, vectorizar con SIMD exige intercalar operaciones costosas de empaquetado (gather/scatter), neutralizando las ganancias del paralelismo de datos en las unidades vectoriales..
El software es transformación de buffers
La resistencia más común frente al diseño orientado a datos es la queja sobre la supuesta pérdida de encapsulación: "¿Dónde pongo mis métodos si no tengo objetos?".
La respuesta de Mike Acton y de los ingenieros de sistemas contemporáneos es desmitificadora: el trabajo de un programador no es crear modelos ontológicos abstractos; es escribir transformaciones mecánicas sobre flujos de bytes. Los métodos adheridos a objetos dispersos en el montón mediante punteros (vtable) son una ilusión de organización que destruye el rendimiento del silicio.
Diseñar respetando los datos y la jerarquía de memorias no es una micro-optimización prematura; es comprender el medio físico sobre el que opera el software. Cuando adaptas las estructuras a la arquitectura del procesador en lugar de obligar al procesador a emular tu jerarquía de clases idealizada, el código se vuelve más simple, predecible y órdenes de magnitud más rápido.