Last active
October 11, 2025 06:17
-
-
Save roninjin10/35cdb0afbe31594b62612754661f2208 to your computer and use it in GitHub Desktop.
javascript benchmarks aos vs soa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { bench, describe } from 'vitest'; | |
| const NUM_ENTITIES = 100000; | |
| // ============================================================================ | |
| // IMPLEMENTATION 1: Array of Structs (AoS) - Traditional OOP approach | |
| // ============================================================================ | |
| // Using objects with MANY unused fields | |
| const entitiesAoS_Objects = []; | |
| for (let i = 0; i < NUM_ENTITIES; i++) { | |
| entitiesAoS_Objects.push({ | |
| id: i, // UNUSED in calculation | |
| x: Math.random() * 100, | |
| y: Math.random() * 100, | |
| z: Math.random() * 100, | |
| vx: Math.random() * 10 - 5, | |
| vy: Math.random() * 10 - 5, | |
| vz: Math.random() * 10 - 5, | |
| mass: Math.random() * 100, // UNUSED in calculation | |
| health: Math.random() * 100, // UNUSED in calculation | |
| team: Math.floor(Math.random() * 4), // UNUSED in calculation | |
| lastUpdateTime: Date.now() // UNUSED in calculation | |
| }); | |
| } | |
| function updatePositionsAoS_Objects(entities, dt) { | |
| for (let i = 0; i < entities.length; i++) { | |
| const entity = entities[i]; | |
| entity.x += entity.vx * dt; | |
| entity.y += entity.vy * dt; | |
| entity.z += entity.vz * dt; | |
| } | |
| } | |
| // ============================================================================ | |
| // IMPLEMENTATION 2: Array of Structs (AoS) - Using ES6 Classes | |
| // ============================================================================ | |
| class Entity { | |
| constructor(id) { | |
| this.id = id; // UNUSED | |
| this.x = Math.random() * 100; | |
| this.y = Math.random() * 100; | |
| this.z = Math.random() * 100; | |
| this.vx = Math.random() * 10 - 5; | |
| this.vy = Math.random() * 10 - 5; | |
| this.vz = Math.random() * 10 - 5; | |
| this.mass = Math.random() * 100; // UNUSED | |
| this.health = Math.random() * 100; // UNUSED | |
| this.team = Math.floor(Math.random() * 4); // UNUSED | |
| this.lastUpdateTime = Date.now(); // UNUSED | |
| } | |
| updatePosition(dt) { | |
| this.x += this.vx * dt; | |
| this.y += this.vy * dt; | |
| this.z += this.vz * dt; | |
| } | |
| } | |
| const entitiesAoS_Classes = []; | |
| for (let i = 0; i < NUM_ENTITIES; i++) { | |
| entitiesAoS_Classes.push(new Entity(i)); | |
| } | |
| function updatePositionsAoS_Classes(entities, dt) { | |
| for (let i = 0; i < entities.length; i++) { | |
| entities[i].updatePosition(dt); | |
| } | |
| } | |
| // ============================================================================ | |
| // IMPLEMENTATION 3: Array of Structs (AoS) - Using typed array with interleaved data | |
| // ============================================================================ | |
| // Each entity: [id, x, y, z, vx, vy, vz, mass, health, team, lastUpdateTime] = 11 floats | |
| const FIELDS_PER_ENTITY = 11; | |
| const entitiesAoS_TypedArray = new Float64Array(NUM_ENTITIES * FIELDS_PER_ENTITY); | |
| for (let i = 0; i < NUM_ENTITIES; i++) { | |
| const offset = i * FIELDS_PER_ENTITY; | |
| entitiesAoS_TypedArray[offset + 0] = i; // id (UNUSED) | |
| entitiesAoS_TypedArray[offset + 1] = Math.random() * 100; // x | |
| entitiesAoS_TypedArray[offset + 2] = Math.random() * 100; // y | |
| entitiesAoS_TypedArray[offset + 3] = Math.random() * 100; // z | |
| entitiesAoS_TypedArray[offset + 4] = Math.random() * 10 - 5; // vx | |
| entitiesAoS_TypedArray[offset + 5] = Math.random() * 10 - 5; // vy | |
| entitiesAoS_TypedArray[offset + 6] = Math.random() * 10 - 5; // vz | |
| entitiesAoS_TypedArray[offset + 7] = Math.random() * 100; // mass (UNUSED) | |
| entitiesAoS_TypedArray[offset + 8] = Math.random() * 100; // health (UNUSED) | |
| entitiesAoS_TypedArray[offset + 9] = Math.floor(Math.random() * 4); // team (UNUSED) | |
| entitiesAoS_TypedArray[offset + 10] = Date.now(); // lastUpdateTime (UNUSED) | |
| } | |
| function updatePositionsAoS_TypedArray(entities, dt) { | |
| for (let i = 0; i < NUM_ENTITIES; i++) { | |
| const offset = i * FIELDS_PER_ENTITY; | |
| entities[offset + 1] += entities[offset + 4] * dt; // x += vx * dt | |
| entities[offset + 2] += entities[offset + 5] * dt; // y += vy * dt | |
| entities[offset + 3] += entities[offset + 6] * dt; // z += vz * dt | |
| } | |
| } | |
| // ============================================================================ | |
| // IMPLEMENTATION 4: Struct of Arrays (SoA) - Data-oriented approach | |
| // ============================================================================ | |
| const entitiesSoA = { | |
| id: new Float64Array(NUM_ENTITIES), // UNUSED - separate array | |
| x: new Float64Array(NUM_ENTITIES), | |
| y: new Float64Array(NUM_ENTITIES), | |
| z: new Float64Array(NUM_ENTITIES), | |
| vx: new Float64Array(NUM_ENTITIES), | |
| vy: new Float64Array(NUM_ENTITIES), | |
| vz: new Float64Array(NUM_ENTITIES), | |
| mass: new Float64Array(NUM_ENTITIES), // UNUSED - separate array | |
| health: new Float64Array(NUM_ENTITIES), // UNUSED - separate array | |
| team: new Float64Array(NUM_ENTITIES), // UNUSED - separate array | |
| lastUpdateTime: new Float64Array(NUM_ENTITIES) // UNUSED - separate array | |
| }; | |
| for (let i = 0; i < NUM_ENTITIES; i++) { | |
| entitiesSoA.id[i] = i; | |
| entitiesSoA.x[i] = Math.random() * 100; | |
| entitiesSoA.y[i] = Math.random() * 100; | |
| entitiesSoA.z[i] = Math.random() * 100; | |
| entitiesSoA.vx[i] = Math.random() * 10 - 5; | |
| entitiesSoA.vy[i] = Math.random() * 10 - 5; | |
| entitiesSoA.vz[i] = Math.random() * 10 - 5; | |
| entitiesSoA.mass[i] = Math.random() * 100; | |
| entitiesSoA.health[i] = Math.random() * 100; | |
| entitiesSoA.team[i] = Math.floor(Math.random() * 4); | |
| entitiesSoA.lastUpdateTime[i] = Date.now(); | |
| } | |
| function updatePositionsSoA(entities, dt) { | |
| // Destructure arrays to avoid repeated property lookups | |
| const { x, y, z, vx, vy, vz } = entities; | |
| // Single loop processing all dimensions at once | |
| for (let i = 0; i < NUM_ENTITIES; i++) { | |
| x[i] += vx[i] * dt; | |
| y[i] += vy[i] * dt; | |
| z[i] += vz[i] * dt; | |
| } | |
| } | |
| // ============================================================================ | |
| // BENCHMARKS | |
| // ============================================================================ | |
| const dt = 0.016; // ~60fps frame time | |
| describe('AoS vs SoA Position Update', () => { | |
| bench('1. Array of Structs (Objects)', () => { | |
| updatePositionsAoS_Objects(entitiesAoS_Objects, dt); | |
| }); | |
| bench('2. Array of Structs (Classes)', () => { | |
| updatePositionsAoS_Classes(entitiesAoS_Classes, dt); | |
| }); | |
| bench('3. Array of Structs (Typed Array)', () => { | |
| updatePositionsAoS_TypedArray(entitiesAoS_TypedArray, dt); | |
| }); | |
| bench('4. Struct of Arrays (SoA)', () => { | |
| updatePositionsSoA(entitiesSoA, dt); | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment