Page MenuHomePhabricator
Paste P19931

rl-optim mw.loader.makeQueryString
ActivePublic

Authored by Krinkle on Feb 2 2022, 12:18 PM.
Tags
None
Referenced Files
F34940780: rl-optim mw.loader.makeQueryString
Feb 2 2022, 5:52 PM
F34940493: rl-optim mw.loader.makeQueryString
Feb 2 2022, 12:19 PM
F34940491: rl-optim mw.loader.makeQueryString
Feb 2 2022, 12:18 PM
var now = typeof performance !== 'undefined' ? performance.now.bind( performance ) : require('perf_hooks').performance.now;
function makeQueryString_0( params ) {
return Object.keys( params ).map( function ( key ) {
return encodeURIComponent( key ) + '=' + encodeURIComponent( params[ key ] );
} ).join( '&' );
}
function makeQueryString_1( params ) {
var chunks = [];
for ( var key in params ) {
chunks.push( encodeURIComponent( key ) + '=' + encodeURIComponent( params[ key ] ) );
}
return chunks.join( '&' );
}
function makeQueryString_2( params ) {
var chunks = [];
for ( var key in params ) {
chunks.push( key + '=' + encodeURIComponent( params[ key ] ) );
}
return chunks.join( '&' );
}
var iterations = 10000;
var dataset = [];
var i = iterations;
while (i--) {
var modules = Array(10 + (i % 20))
.fill(null)
.map((val, j) => Math.random().toString(32).slice(2, 10 + (j % 5)))
.join('|');
dataset.push( {
lang: 'en',
modules: modules,
skin: 'vector',
version: Math.random().toString(32).slice(2, 7)
} );
}
var time_0 = 0;
var time_1 = 0;
var time_2 = 0;
var i = iterations;
while (i--) {
let data = dataset[i];
let start;
start = now();
makeQueryString_0( data );
time_0 += ( now() - start );
start = now();
makeQueryString_1( data );
time_1 += ( now() - start );
start = now();
makeQueryString_2( data );
time_2 += ( now() - start );
}
console.log( { time_0, time_1, time_2 } );
// Firefox 96
// { time_0: 48, time_1: 39, time_2: 24 }
// Safari 15.2
// { time_0: 37, time_1: 24, time_2: 13 }
// Node.js v17.4.0
// { time_0: 37.837, time_1: 32.148, time_2: 23.877 }

Event Timeline

Krinkle edited the content of this paste. (Show Details)

For a further optimization using string concatenation instead of an array, I ran a modified version of the code above, posting here for reference

var now = performance.now.bind( performance );

function makeQueryString_1( params ) {
	var chunks = [];
	for ( var key in params ) {
		chunks.push( encodeURIComponent( key ) + '=' + encodeURIComponent( params[ key ] ) );
	}
	return chunks.join( '&' );
}

function makeQueryString_3( params ) {
	var str = '';
	for ( var key in params ) {
		str += encodeURIComponent( key ) + '=' + encodeURIComponent( params[ key ] ) + '&';
	}
	return str.slice( 0, str.length - 1 );
}

var iterations = 10000;
var dataset = [];

var i = iterations;
while (i--) {
	var modules = Array(10 + (i % 20))
		.fill(null)
		.map((val, j) => Math.random().toString(32).slice(2, 10 + (j % 5)))
		.join('|');
	dataset.push( {
		lang: 'en',
		modules: modules,
		skin: 'vector',
		version: Math.random().toString(32).slice(2, 7)
	} );
}

var time_1 = 0;
var time_3 = 0;

var i = iterations;
while (i--) {
	let data = dataset[i];
	let start;

	start = now();
	makeQueryString_1( data );
	time_1 += ( now() - start );

	start = now();
	makeQueryString_3( data );
	time_3 += ( now() - start );
}

console.log( { time_1, time_3 } );

On chrome 103.0.5060.134, I got {time_1: 29.49999988079071, time_3: 19.100000500679016}

The difference does not appear to be consistent for me.

var now = typeof performance !== 'undefined' ? performance.now.bind( performance ) : require('perf_hooks').performance.now;

	function makeQueryString_0( params ) {
		return Object.keys( params ).map( function ( key ) {
			return encodeURIComponent( key ) + '=' + encodeURIComponent( params[ key ] );
		} ).join( '&' );
	}

	function makeQueryString_1( params ) {
		var chunks = [];
		for ( var key in params ) {
			chunks.push( encodeURIComponent( key ) + '=' + encodeURIComponent( params[ key ] ) );
		}
		return chunks.join( '&' );
	}

	function makeQueryString_2( params ) {
		var chunks = [];
		for ( var key in params ) {
			chunks.push( key + '=' + encodeURIComponent( params[ key ] ) );
		}
		return chunks.join( '&' );
	}

	function makeQueryString_3( params ) {
		var str = '';
		for ( var key in params ) {
			str += encodeURIComponent( key ) + '=' + encodeURIComponent( params[ key ] ) + '&';
		}
		return str.slice( 0, str.length - 1 );
	}

	var iterations = 10000;
	var dataset = [];

	var i = iterations;
	while (i--) {
		var modules = Array(10 + (i % 20))
			.fill(null)
			.map((val, j) => Math.random().toString(32).slice(2, 10 + (j % 5)))
			.join('|');
		dataset.push( {
			lang: 'en',
			modules: modules,
			skin: 'vector',
			version: Math.random().toString(32).slice(2, 7)
		} );
	}

	var time_0 = 0;
	var time_1 = 0;
	var time_3 = 0;

	var i = iterations;
	while (i--) {
		let data = dataset[i];
		let start;

		start = now();
		makeQueryString_0( data );
		time_0 += ( now() - start );

		start = now();
		makeQueryString_1( data );
		time_1 += ( now() - start );

		start = now();
		makeQueryString_3( data );
		time_3 += ( now() - start );
	}

	console.log( { time_0, time_1, time_3 } );
	// Firefox 103
	// { time_0: 45, time_1: 43, time_3: 39 }

	// Safari 15.5
	// { time_0: 37.000, time_1: 21.000, time_3: 31.999 } # <!-- higher

	// Chrome 103
	// { time_0: 30.099, time_1: 18.200, time_3: 17.000 }

	// Node.js v18.4.0
	// { time_0: 33.814, time_1: 29.383, time_3: 26.932 }

	// Samsung Galaxy S20, Android 10, Chrome Mobile 103
	// { time_0: 55.999, time_1: 46.100, time_3: 50.300 } # <!-- higher

Each of these is through an empty browser window with one tab at https://www.wikipedia.org and then pasting it from the console once (mobile device via BrowserStack and its Chrome DevTools).

Feel free to experiment a bit further. Some interesting reading background at https://stackoverflow.com/q/7299010/319266 and https://stackoverflow.com/a/7299478/319266 as well. It appears that in modern browsers, the array-join trick should not longer be needed, but I've not seen it play out per the above.

Remember that an important aaspect of the above benchmark, which makes it representative, is that we don't do the same thing multiple times, e.g. each dataset is different. To use more iterations, I generate more data instead of repeating a small set of data multiple times. This is because otherwise what we end up measuring is how well the compiler/CPU is memorising and inlining similar computations instead of measuring the cost of those computations.

Another run, based on an idea from @thiemowmde at https://gerrit.wikimedia.org/r/c/mediawiki/core/+/817340/4/resources/src/startup/mediawiki.loader.js#1124

var now = typeof performance !== 'undefined' ? performance.now.bind(performance) : require('perf_hooks').performance.now;

function isEven(num) {
	return (num % 2) === 0;
}

function makeQueryString_0(params) {
	return Object.keys(params).map(function (key) {
		return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
	}).join('&');
}

function makeQueryString_1(params) {
	var chunks = [];
	for (var key in params) {
		chunks.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
	}
	return chunks.join('&');
}

function makeQueryString_2(params) {
	var chunks = [];
	for (var key in params) {
		chunks.push(key + '=' + encodeURIComponent(params[key]));
	}
	return chunks.join('&');
}

function makeQueryString_3(params) {
	var str = '';
	for (var key in params) {
		str += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) + '&';
	}
	return str.slice(0, str.length - 1);
}

function makeQueryString_4(params) {
	var str = '';
	for (var key in params) {
		str += (str ? '&' : '') + encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
	}
	return str;
}

var iterations = 10000;
var warmup = 100;
var dataset = [];

var i = iterations;
while (i--) {
	var modules = Array(10 + (i % 20))
		.fill(null)
		.map((val, j) => Math.random().toString(32).slice(2, 10 + (j % 5)))
		.join('|');
	dataset.push({
		lang: 'en',
		modules: modules,
		skin: 'vector',
		version: Math.random().toString(32).slice(2, 7)
	});
}

var cases = [
	makeQueryString_0,
	makeQueryString_1,
	makeQueryString_3,
	makeQueryString_4,
];
var times = {};
cases.forEach((fn) => {
	times[fn.name] = 0;
});

var i = warmup;
while (i--) {
	let data = dataset[i];
	cases.forEach((fn) => {
		fn(data);
	});
}

var i = dataset.length;
while (i--) {
	let data = dataset[i];

	(isEven(i) ? cases : cases.reverse()).forEach((fn) => {
		let start = now();
		fn(data);
		times[fn.name] += (now() - start);
	});
}

console.log(times);
// Firefox 103, macOS
{ makeQueryString_0: 36, makeQueryString_1: 41, makeQueryString_3: 38, makeQueryString_4: 33 }

// Chrome 103, macOS
{makeQueryString_0: 24.099, makeQueryString_1: 19.599, makeQueryString_3: 18.899, makeQueryString_4: 18.600}

// Safari 15.6, macOS
{makeQueryString_0: 29.000, makeQueryString_1: 19, makeQueryString_3: 24.999, makeQueryString_4: 29.000}

// Samsung Galaxy S20, Android 11, Chrome Mobile 103 (BrowserStack, real device)
{makeQueryString_0: 54.599, makeQueryString_1: 52.200, makeQueryString_3: 52.000, makeQueryString_4: 50.800}

// Huawai P30, Android 9, Chrome Mobile 103 (BrowserStack, real device)
{makeQueryString_0: 61.199, makeQueryString_1: 58.999, makeQueryString_3: 58.600, makeQueryString_4: 48.600}

Modern JIT engines are nuts. 😵‍💫 You would expect that collecting individual strings in an array before concatenating them would to be at least 2 times the effort. But no. It barely makes a difference.

Thanks for the numbers! Really fascinating stuff.