Skip to content

Instantly share code, notes, and snippets.

@ArcaneEngineer
Last active September 21, 2024 17:34
Show Gist options
  • Save ArcaneEngineer/634ac013242950f1df63c82302626863 to your computer and use it in GitHub Desktop.
Save ArcaneEngineer/634ac013242950f1df63c82302626863 to your computer and use it in GitHub Desktop.
Array types performance test: regular vs SMI (31-bit) vs Uint32Array
//Ensure we have reproducible read / write sequence via pseudo-random index sequence.
export const MAX_RAND_INT = 4294967296;
export function splitmixUInt32(seed)
{
return function() {
seed |= 0;
seed = seed + 0x9e3779b9 | 0;
let t = seed ^ seed >>> 16;
t = Math.imul(t, 0x21f0aaad);
t = t ^ t >>> 15;
t = Math.imul(t, 0x735a2d97);
return ((t = t ^ t >>> 15) >>> 0);
}
}
let randomUInt32 = splitmixUInt32(777);
//This test is to find performance differences between Array of SMIs vs. Uint32Array.
//Each case uses the same 31-bit number range for testing.
//https://dev.to/alirezaebrahimkhani/be-careful-about-arrays-v8-engine-advice-1pmk
//Another benefit of Uint32Array over array of SMI is that you are not guessing at what V8 is generating,
//and committing mistakes on array access (both write and read-out-of-bounds) will not wreck access times
//for as long as that array continues to exist -- which _is_ the case with array of SMI.
let bufferWidthPow = 12;
let bufferSizePow = bufferWidthPow + bufferWidthPow;
let bufferWidth = Math.pow(2, bufferWidthPow);
let bufferSize = bufferWidth * bufferWidth;
const iterations = 100000000;
let regArray = new Array(bufferSize); //uncertain element type
let smiArray = new Array(bufferSize).fill(1); //pre-filling assures the "SMI-ness" of the array.
let u32Array = new Uint32Array(bufferSize);
let REG = 0;
let SMI = 1;
let U32 = 2;
let ARRAY_COUNT = 3;
let arraysByName = {};
arraysByName[REG] = regArray;
arraysByName[SMI] = smiArray;
arraysByName[U32] = u32Array;
let arrayNames = {};
arrayNames[REG] = 'regular';
arrayNames[SMI] = 'SMI';
arrayNames[U32] = 'Uint32';
//order
let RND = 0;
let SEQ = 1; //should make POS and NEG
let ORDER_COUNT = 2;
let orderNames = {}
orderNames[RND] = 'random';
orderNames[SEQ] = 'sequential';
//access type
let READ = 0;
let WRIT = 1;
let ACCESS_COUNT = 2;
let accessNames = {}
accessNames[READ] = 'read';
accessNames[WRIT] = 'write';
let funcsByOrderByAccess = {};
funcsByOrderByAccess[RND] = {};
funcsByOrderByAccess[RND][READ] = randomRead;
funcsByOrderByAccess[RND][WRIT] = randomWrite;
funcsByOrderByAccess[SEQ] = {};
funcsByOrderByAccess[SEQ][READ] = sequentialRead;
funcsByOrderByAccess[SEQ][WRIT] = sequentialWrite;
function getNextRandom31BitValue()
{
return randomUInt32() >> 1; //lose a bit.
}
function getNextRandomSizeIndex()
{
let bitsDiff = 32 - bufferSizePow;
return randomUInt32() >>> bitsDiff; //>>> ensures interpretation as positive index.
}
function getNextRandomWidthIndex()
{
let bitsDiff = 32 - bufferWidthPow;
return randomUInt32() >> bitsDiff;
}
function randomWrite(array, i)
{
i = getNextRandomSizeIndex(); //overwrite incoming.
return array[i] = getNextRandom31BitValue();
}
function randomRead(array, i)
{
i = getNextRandomSizeIndex(); //overwrite incoming.
return array[i];
}
function sequentialWrite(array, i)
{
return array[i] = getNextRandom31BitValue();
}
function sequentialRead(array, i)
{
return array[i];
}
function test(order, access, arrayType)
{
let array = arraysByName[arrayType];
let func = funcsByOrderByAccess[order][access];
let time0 = ( performance || Date ).now();
let result = 0;
for (let i = 0; i < iterations; i++)
{
result = func(array, i);
}
let time1 = ( performance || Date ).now();
let timeDiff = (time1 - time0) / iterations;
console.log("tested", arrayNames[arrayType], "array", orderNames[order], accessNames[access], "array; milliseconds per op", timeDiff, "(result:", result,")");
return timeDiff;
}
function runTests()
{
console.log("testing over", bufferSize,"elements and", iterations, "iterations...")
let tableStr = "Times in milliseconds by category:\n\n";
for (let arrayName in arrayNames)
{
tableStr += arrayNames[arrayName] + "\n";
tableStr += " rand".padStart(11, " ") + "sequ".padStart(11, " ")+"\n";
for (let accessName in accessNames)
{
let rowStr = accessNames[accessName].slice(0, 4)+" ";
for (let orderName in orderNames)
{
const timeDiff = test(orderName, accessName, arrayName);
rowStr += (timeDiff.toFixed(6).padStart(10, " "))+" ";
}
tableStr += rowStr + "\n";
}
tableStr += "\n"
}
console.log(tableStr);
}
runTests()
@ArcaneEngineer
Copy link
Author

ArcaneEngineer commented May 31, 2024

Produces (on Acer Nitro 5 N22C2):

testing over 16777216 elements and 100000000 iterations...
tested regular array random read array; milliseconds  per op 0.000041334000000059605 (result: undefined )
tested regular array sequential read array; milliseconds  per op 0.0000036620000000298023 (result: undefined )
tested regular array random write array; milliseconds  per op 0.000025979000000059604 (result: -1006424367 )
tested regular array sequential write array; milliseconds  per op 0.000017737999999970197 (result: -675941488 )
tested SMI array random read array; milliseconds  per op 0.000049202999999970194 (result: 1 )
tested SMI array sequential read array; milliseconds  per op 0.000003606000000089407 (result: undefined )
tested SMI array random write array; milliseconds  per op 0.00002686 (result: -215325660 )
tested SMI array sequential write array; milliseconds  per op 0.000027367000000029803 (result: -437391294 )
tested Uint32 array random read array; milliseconds  per op 0.00004778 (result: 0 )
tested Uint32 array sequential read array; milliseconds  per op 0.0000040859999999403955 (result: undefined )
tested Uint32 array random write array; milliseconds  per op 0.000025734000000059605 (result: 1003264032 )
tested Uint32 array sequential write array; milliseconds  per op 0.000028177999999970198 (result: 492969257 )

Times in milliseconds by category:

regular
       rand       sequ
read   0.000041   0.000004 
writ   0.000026   0.000018 

SMI
       rand       sequ
read   0.000049   0.000004 
writ   0.000027   0.000027 

Uint32
       rand       sequ
read   0.000048   0.000004 
writ   0.000026   0.000028 

Summary:

SMI doesn't appear to be worth the effort is we consider the problem over 100M iterations.m
Oddly, over 1M SMI array appears 2-3x faster than regular arrays on random access. It is mildly faster than the others on sequential access, not enough to make any noticeable difference. This may however be statistical errors and based on the fact that regular arrays are the first to run on page load.
Uint32Array is so close to SMI in overall performance that if you really need all 32 bits per element, you may as well use it (although some costs associated with casting between signed and unsigned can be assumed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment