JavaScript Engine Fundamentals
Overview
Javascript was originally designed as a simple scripting language for web browsers. Nowadays ECMAScript (Specification of Javascript) is a fully-featured general-purpose programming language. It empowers a wide spectrum of different purpose applications. We can even find JS solutions in Space.
This article gives outline of some JS language fundamentals, that can help improve your code.
Engines
There are few JS engines which in different way resolve ECMASCRIPT implementation.
The main engine features are: compilation, run and code optimization.
Javascript Engine Pipeline
An engines follow in high level same steps:
- Parser build AST (lexical, syntactic, semantic analysis)
- Interpreter generates Bytecode
- Bytecode is translated with JIT (Just In Time) compiler into machine code (only popular code)
JS Optimization - Engine trade-offs
All JS engines have to deal with tradeoffs between Low and High code optimization.
Only once function becomes hot (often used) it is optimized by the compiler. The main reason why JS engines optimize only hot functions is that:
- More compiled code => More memory used
- Compiling code => Extra time
- Less compilation => Slower run
Objects
Each object is:
- Internally defined as dictionaries.
- Each object variable:
- Has a defined name in string form
- Contain additional property values:
Property | Example value | Description |
---|---|---|
Value | “some value” | Value |
[[Writable]] | true | Allow to change the value of the variable |
[[Enumerable]] | true | Allow to iterate over a variable (eg. in a loop) |
[[Configurable]] | true | Allow to delete an object variable |
To check it, we can use Object.getOwnPropertyDescriptors
function:
Object.getOwnPropertyDescriptors({key1: 'val1', key2: 'val2'})
Result:
{…}
key1: {…}
configurable: true
enumerable: true
value: "val1"
writable: true
<prototype>: Object { … }
key2: {…}
configurable: true
enumerable: true
value: "val2"
writable: true
<prototype>: Object { … }
<prototype>: Object { … }
Arrays
- Similar to objects + auto-updating
length
property - All values in the array field have the same internal properties.
Let’s see how property descriptors look for the array:
Object.getOwnPropertyDescriptors(['val1', 'val2'])
Result:
{…}
0: {…}
configurable: true
enumerable: true
value: "val1"
writable: true
<prototype>: Object { … }
1: {…}
configurable: true
enumerable: true
value: "val2"
writable: true
<prototype>: Object { … }
length: {…}
configurable: false
enumerable: false
value: 2
writable: true
<prototype>: Object { … }
<prototype>: Object { … }
Hidden Classes / Shapes / Structures / Types / Maps
When dynamically instantiating an object, these made in the background hidden class / shape / structure / type / map.
The structure instead of object value has Offset:
Property | Example value |
---|---|
Offset | 0 |
[[Writable]] | true |
[[Enumerable]] | true |
[[Configurable]] | true |
Different terminology
Each engine has a different name for the same thing:
- Academic => Hidden classes
- JavaScriptCore => Structures
- Chakra => Types
- V8 => Maps
- SpiderMonkey => Shapes
Let’s call it: “shape” in this article.
How does it work?
- When dynamically instantiating an object, these made in the background object shape
- Objects with the same values can have the same internal shape (Order is important)
- Each object variable has a pointer to index or offset of the memory location where the value of the variable is written.
- Empty object => Empty shape
- Adding variables => Building new shapes on the fly
- Object shape transition is maintained (due to object mutation)
- In case we have identical parent objects transition chain changes to transition tree
- Chained shapes increase access time. To avoid it internal
shape table
is created which connects variable to a specific shape of an object. - Some engines clear the offset field when deleting a single variable, which slows down the access times
Inline caching
Inline Caching - When calling function, interpreter remembers the location of the shape from the object and memory offset of value. It enables faster accesses. Caching is not effective if sequentially access different objects.
Types
- Monomorphic IC - objects accessed in the sequence have the same shape.
- Polymorphic IC - objects that are accessed in the sequence have a different shape (4 or less).
- Megamorphic IC - objects that are accessed in the sequence have a different shape (4 or more), code optimization turned off.
Optimization
Let’s imagine Megamorphic IC situation that we call the function with different shape every function call. Also, all 5 shapes have some common keys:
<html>
<script type="text/javascript">
function perfTest() {
var t0 = performance.now();
const X1 = { a: "A", b: "A", c: "A" };
const X2 = { a: "B", b: "B", d: "B" };
const X3 = { a: "C", b: "C", e: "C" };
const X4 = { a: "D", b: "D", f: true };
const X5 = { a: "E" };
const object = [X1, X2, X3, X4, X5, X1, X2, X3];
const get_a = (obj) => obj.a;
for (var i = 0; i < 10000000000; i++) get_a(object[i & 7]);
var t1 = performance.now();
document.write("Execution took " + (t1 - t0) + " milliseconds.");
}
window.onload = function () {
perfTest();
};
</script>
</html>
Execution time:
Execution took 26 9154.5499999993 milliseconds.
Now let’s try to optimize code to make the same shape from all 5 objects by adding null
values to not covered keys:
<html>
<script type="text/javascript">
function perfTest() {
var t0 = performance.now();
const X1 = { a: "A", b: "A", c: "A", d: null, e: null, f: null };
const X2 = { a: "B", b: "B", c: null, d: "B", e: null, f: null };
const X3 = { a: "C", b: "C", c: null, d: null, e: "C", f: null };
const X4 = { a: "D", b: "D", c: null, d: null, e: null, f: true };
const X5 = { a: "E", b: null, c: null, d: null, e: null, f: null };
const object = [X1, X2, X3, X4, X5, X1, X2, X3];
const get_a = (obj) => obj.a;
for (var i = 0; i < 10000000000; i++) get_a(object[i & 7]);
var t1 = performance.now();
document.write("Execution took " + (t1 - t0) + " milliseconds.");
}
window.onload = function () {
perfTest();
};
</script>
</html>
Execution time:
Execution took 3 1370.180000005348 milliseconds.
Optimized code execution time drastically decreased in comparison of unoptimized. From 26 sec to 3 sec.
Prototypes
- Prototypes are specially treated objects.
- Each JS object has an inheritance chain up to the general
Object.prototype
- JS Engines in background use object shapes for object prototypes
- Prototypes also use Inline Cache to have quick access over Shape chain
ValidityCell
guarantee that cached prototype didn’t change- Once
ValidityCell
became invalid inline caching is not used anymore
Classes
Classes are syntax for the prototype.
Garbage Collection
- Garbage Collector - the mechanism for real-time deletion of unused dynamic memory
- Stop a main thread to rearrange references
- Responsible for memory optimization
- Usually use Mark & Sweep or derivatives
- Marking - Mark all alive (which has at least one reference) objects
- Sweeping - Free memory from all objects left
- Usually, objects are divided into generations:
- Young - Most instances die young, so over the young generation, GC runs more times
- Old - Old objects live much longer e.g. global objects
- Optimally GC run when idle time detected e.g. on watching a video, reading longer text
Memory Leak - Accidental Global Variables
When variable inside a function is defined without var
prefix it cause that object is assigned to window
function foo(arg) {
bar = "hidden global variable";
}
If this
was used it may also cause the same issue:
function foo() {
this.variable = "potential accidental global";
}
foo();
To avoid it 'use strict';
the clause can be added at the beginning of JS file.
To avoid it 'use strict';
the clause can be added at the beginning of JS file.
Why JS is not server side compiled ?
You can ask why not to send compiled/bytecode to the browser. This could solve the problem with code optimization. To get the answer we need to understand for what purpose JS was designed and what are pros of this solution.
Simple glue language
The idea of JS was to be the simple scripting language for non-developers. JS had to be like glue between Java applets.
We aimed to provide a “glue language” for the Web designers and part time programmers who were building Web content from components such as images, plugins, and Java applets. We saw Java as the “component language” used by higher-priced programmers, where the glue programmers – the Web page designers – would assemble components and automate their interactions using JS.
In this sense, JS was analogous to Visual Basic, and Java to C++, in Microsoft’s programming language family used on Windows and in its applications. This division of labor across the programming pyramid fosters greater innovation than alternatives that require all programmers to use the “real” programming language (Java or C++) instead of the “little” scripting language.
Brendan Eich JS Initial Designer - https://a-z.readthedocs.io/en/latest/javascript.html
Embedding into HTML
JavaScript was designed to be embedded directly in HTML,
like <input type="button" onclick="alert('hello world')">
. This requires JS to be in a textual format.
Easier development
We don’t need any compiler to JS development only text editor and browser are needed.
Standardization problems
Each browser implements EcmaScript in its own way, the only high-level specification defined by committees is agreed. There is no standardization for bytecode or more optimized code.
Minified and Compressed code
As EcmaScript can be compressed and minified quite well it’s not big loose to not compile on server side.
Performance advice
- Always initialize objects in the same way
- Instead of delete property, prefer to use
null
orundefined
- Don’t mix object shapes in an array
- Keep order on object creation - Order in which the variables are defined is important
- Don’t change prototypes if possible (or change as early as possible)
- Avoid the allocation of global objects
Sources
- https://mathiasbynens.be/notes/shapes-ics
- https://mathiasbynens.be/notes/prototypes
- https://www.ecma-international.org/ecma-262/11.0/index.html#sec-overview
- https://a-z.readthedocs.io/en/latest/javascript.html
- https://softwareengineering.stackexchange.com/questions/402250/why-is-javascript-not-compiled-to-bytecode-before-sending-over-the-network
- https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/
Tags: JavaScript Engine Fundamentals js
Maybe you want to share? :)