-
-
Save albe/39c7b79f46daa49d2cf373ffab3c4513 to your computer and use it in GitHub Desktop.
const Benchmark = require('benchmark'); | |
const benchmarks = require('beautify-benchmark'); | |
const Suite = new Benchmark.Suite('fixed array'); | |
Suite.on('cycle', (event) => benchmarks.add(event.target)); | |
Suite.on('complete', () => benchmarks.log()); | |
const fileBuffer = Buffer.alloc(16 * 128); | |
let offs = 0; | |
for (let i = 0; i<128; i++, offs+=16) { | |
fileBuffer.writeUInt32LE(1 + i*4, offs); | |
fileBuffer.writeUInt32LE(2 + i*4, offs+4); | |
fileBuffer.writeUInt32LE(3 + i*4, offs+8); | |
fileBuffer.writeUInt32LE(4 + i*4, offs+12); | |
} | |
Suite.add('entry read', () => { | |
for (let i = 0; i<128; i++) { | |
let entry = new Array(4); | |
entry[0] = fileBuffer.readUInt32LE(i*16); | |
entry[1] = fileBuffer.readUInt32LE(i*16 + 4); | |
entry[1] = fileBuffer.readUInt32LE(i*16 + 8); | |
entry[1] = fileBuffer.readUInt32LE(i*16 + 12); | |
if (entry[0] !== i * 4 + 1) { | |
console.log('Invalid data at entry #' + i, entry[0]); | |
} | |
} | |
}); | |
Suite.add('entryview read', () => { | |
for (let i = 0; i < 128; i++) { | |
let entry = new Uint32Array(fileBuffer.buffer, fileBuffer.byteOffset + i * 16, 4); | |
if (entry[0] !== i * 4 + 1) { | |
console.log('Invalid data at entry #' + i, entry[0]); | |
} | |
} | |
}); | |
Suite.run(); |
> node --version | |
v12.18.3 | |
> node bench-array | |
2 tests completed. | |
entry read x 1,180,097 ops/sec ±1.49% (92 runs sampled) | |
entryview read x 198,301 ops/sec ±1.00% (90 runs sampled) | |
> WHY?! |
Maybe to clarify a bit: While JS engines probably could optimize out the typed array creation, at least V8 currently doesn’t. That comes with allocation and object creation overhead.
I once overhead a JS engine developer say (slightly tongue-in-cheek) that the only thing typed arrays are really better at than plain Arrays is fast passing of data between JS and native code :)
Another interesting observation: Creating a custom class, that just holds a reference to the buffer and offset, then uses readUInt32LE()
on demand will perform at ~2,8M ops/sec for the above benchmark. So whatever UInt32Array
does, it's more than just the object creation and referencing of the buffer for access (maybe some ref counting which involves an additional allocation?).
Note though that it will start to perform worse relative to amount of value accesses and slower than the array solution starting with accessing the four values more than once.
So that means: buffer read access < object property access and typed array instanciation < generic object instanciation
Addendum: The same with
DataView
instead ofUInt32Array
is slower by another factorAnd the read check does not really play a factor in all three cases. Why is
DataView
so much slower thanUInt32Array
?Also noteworthy: Doing
let entry = fileBuffer.slice(i*16, (i+1)*16);
will result in roughly same performance as theUInt32Array
case, which makes me believe they might do similar things. However, according do docs, again aslice
on aBuffer
should result in only a view on the region inside the buffers memory. So the performance penalty is not really understandable, unless some allocation+memcpy is in place.