Imagine I have a function which accesses a constant (never mutated) variable (lookup table or array, for example). The constant is not referenced anywhere outside the function scope. My intuition tells me that I should define this constant outside the function scope (Option A below) to avoid (re-)creating it on every function invocation, but is this really the way modern Javascript engines work? I'd like to think that modern engines can see that the constant is never modified, and thus only has to create and cache it once (is there a term for this?). Do browsers cache functions defined within closures in the same way?
Are there any non-negligible performance penalties to simply defining the constant inside the function, right next to where it's accessed (Option B)? Is the situation different for more complex objects?
// Option A:
function inlinedAccess(key) {
const inlinedLookupTable = {
a: 1,
b: 2,
c: 3,
d: 4,
}
return 'result: ' + inlinedLookupTable[key]
}
// Option B:
const CONSTANT_TABLE = {
a: 1,
b: 2,
c: 3,
d: 4,
}
function constantAccess(key) {
return 'result: ' + CONSTANT_TABLE[key]
}
I created a jsperf test which compares different approaches:
Object
- inlined (option A)Object
- constant (option B)Additional variants suggested by @jmrk:
Map
- inlinedMap
- constantswitch
- inlined valuesInitial findings (on my machine, feel free to try it out for yourself):
Constants are block-scoped, much like variables declared using the let keyword. The value of a constant can't be changed through reassignment (i.e. by using the assignment operator), and it can't be redeclared (i.e. through a variable declaration).
Just like we saw for variables, there was no difference in performance for functions declared as const variables using the arrow syntax compared to those declared using the traditional function keyword syntax.
Clearly, performance-wise on Chrome, let on the global scope is slowest, while let inside a block is fastest, and so is const.
`const` is a signal that the identifier won't be reassigned. `let` is a signal that the variable may be reassigned, such as a counter in a loop, or a value swap in an algorithm. It also signals that the variable will be used only in the block it's defined in, which is not always the entire containing function.
V8 developer here. Your intuition is correct.
TL;DR: inlinedAccess
creates a new object every time. constantAccess
is more efficient, because it avoids recreating the object on every invocation. For even better performance, use a Map
.
The fact that a "quick test" yields the same timings for both functions illustrates how easily microbenchmarks can be misleading ;-)
b: new Array(100),
.'result: ' + ...
contribute quite a bit to the overall time; you can drop that to get a clearer signal.Map
lookups for varying keys are faster than object property lookups. Using objects as maps is so 2010, modern JavaScript has proper Map
s, so use them! :-)Array
element lookups are even faster, but of course you can only use them when your keys are integers.Let's put all of those thoughts into code:
function inlinedAccess(key) {
const inlinedLookupTable = {
a: 1,
b: new Array(100),
c: 3,
d: 4,
}
return inlinedLookupTable[key];
}
const CONSTANT_TABLE = {
a: 1,
b: new Array(100),
c: 3,
d: 4,
}
function constantAccess(key) {
return CONSTANT_TABLE[key];
}
const LOOKUP_MAP = new Map([
["a", 1],
["b", new Array(100)],
["c", 3],
["d", 4]
]);
function mapAccess(key) {
return LOOKUP_MAP.get(key);
}
const ARRAY_TABLE = ["a", "b", "c", "d"]
function integerAccess(key) {
return ARRAY_TABLE[key];
}
function switchAccess(key) {
switch (key) {
case "a": return 1;
case "b": return new Array(100);
case "c": return 3;
case "d": return 4;
}
}
const kCount = 10000000;
let result = null;
let t1 = Date.now();
for (let i = 0; i < kCount; i++) {
result = inlinedAccess("a");
result = inlinedAccess("d");
}
let t2 = Date.now();
for (let i = 0; i < kCount; i++) {
result = constantAccess("a");
result = constantAccess("d");
}
let t3 = Date.now();
for (let i = 0; i < kCount; i++) {
result = mapAccess("a");
result = mapAccess("d");
}
let t4 = Date.now();
for (let i = 0; i < kCount; i++) {
result = integerAccess(0);
result = integerAccess(3);
}
let t5 = Date.now();
for (let i = 0; i < kCount; i++) {
result = switchAccess("a");
result = switchAccess("d");
}
let t6 = Date.now();
console.log("inlinedAccess: " + (t2 - t1));
console.log("constantAccess: " + (t3 - t2));
console.log("mapAccess: " + (t4 - t3));
console.log("integerAccess: " + (t5 - t4));
console.log("switchAccess: " + (t6 - t5));
I'm getting the following results:
inlinedAccess: 1613
constantAccess: 194
mapAccess: 95
integerAccess: 15
switchAccess: 9
All that said: these numbers are "milliseconds for 10M lookups". In a real-world application, the differences are probably too small to matter, so you can write whatever code is most readable/maintainable/etc. For example, if you only do 100K lookups, the results are:
inlinedAccess: 31
constantAccess: 6
mapAccess: 6
integerAccess: 5
switchAccess: 4
By the way, a common variant of this situation is creating/calling functions. This:
function singleton_callback(...) { ... }
function efficient(...) {
return singleton_callback(...);
}
is much more efficient than this:
function wasteful(...) {
function new_callback_every_time(...) { ... }
return new_callback_every_time(...);
}
And similarly, this:
function singleton_method(args) { ... }
function EfficientObjectConstructor(param) {
this.___ = param;
this.method = singleton_method;
}
is much more efficient than this:
function WastefulObjectConstructor(param) {
this.___ = param;
this.method = function(...) {
// Allocates a new function every time.
};
}
(Of course the usual way of doing it is Constructor.prototype.method = function(...) {...}
, which also avoids repeated function creations. Or, nowadays, you can just use class
es.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With