“Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered.”
This quote is not as well known as the end of it t highlights why you should prefer code which is more maintainable, as long as the performance of it does not
Maintainable code is a matter of taste. For example, we often teach students to dislike “goto” in C, but the best error handling code I in C) comes from the Linux kernel, and is using “goto”’s. Maintainable code in JavaScript is also a matter of tasteome might prefer ES6 features, might prefer functional programing approach with lambdas or even us some framework.
In most cases, we add more abstractions, we add more memory, and more codehus giving even more reasons . Today, I will introduce optimizations made it into IonMonkey, which are known Scalar Replacement and Branch Pruning.
Scalar Replacement
function complex(r, i) {
return { r: r, i: i };
}
function norm(c) {
return Math.sqrt(c.r * c.r + c.i * c.i);
}
function norm_multiply(a, b) {
var mul = complex(a.r * b.r - a.i * b.i, a.r * b.i + a.i * b.r);
return norm(mul);
}
function norm_multiply(a, b) {
var mul_r = a.r * b.r - a.i * b.i;
var mul_i = a.r * b.i + a.i * b.r;
return Math.sqrt(mul_r * mul_r + mul_i * mul_i);
}
This optimization works by looking at one object, then determin if thobject never escape, that all properties are known, and it is not mixed with other objects.
Once all these predicates are validated, this optimization emulates the content of the memory of the object while traversing the control flow graph of the programhereby replacing all property accesses local variables.
Once all property accesses are removed, the alloca object is used by a few recover instructions are capturing the object state at across the control flow graph. Each object state is an instruction serve no purpose during the execution, but uses the Recover Instructions mechanism to set the content of the properties on . At the end, the Sink / Dead Code Elimination phase convert the object allocation into a recovered instruction, as it is only used by the object state instructions.
Escape Analysis
One of the biggest limitation of Scalar Replacement is that it is limited to objects .
We have multiple way
- a function call.
escape({ bar: 1, baz: 2 });
- a returned value.
function next() { return { done: false, value: 0}; }
- an escaped object property.
escaped_obj = { property: obj }; escape(escaped_obj);
The problem of escaped object is that we have no idea how the object would be manipulated, thus we cannot safely replace the properties of the objectlocal variables.
The good news is that we already have optimization phase, called Inlining, already tak care of the code of smaller function in the body of the function, as long as they are frequently used.
Inlining has some limitations, such as the size of the function. , it might be possible scalar replacement never trigger if the object escapein an unused branch, such as in exception handling or logging code.
function doSomething(obj) {
if (theHighlyUnlikelyHappened())
throw new Error("aaaaaaahhh!!", obj);
}
Fortunately, this kind of problem addressed by Branch Pruning optimization decribed in the next section.
Mixing Objects
One other limitation of Scalar Replacement is that we have to be able to identify a single allocationdominat the rest of . For example the following code causes problem because at the end of the then-block, we do not know which branch the allocated object comes from.
function dummy() {
var obj = { done: false }; // (a)
if (len == max)
obj = { done: true }; // (b)
if (obj.done) // obj comes from either (a) or (b)
console.log("We are done! \o/");
}
This issue also appears in the case of returned object. When a function is inlined, all the return statements are funneled into block. Thus, calar eplacement is not capable mix multiple objects allocations. This problem occur in the
next
function of iteratorsfunction next() {
if (this.idx < this.len)
return { value: this.getValue(idx), done: false };
return { done: true };
}
, these can be transformed quite easily, by creating a single object allocation ahead of the condition and mutating the object, while returning the same object from all paths
function next() {
var obj = { value: undefined, done: false };
if (this.idx < this.len) {
obj.value = this.getValue(idx);
return obj;
}
obj.done = true;
return obj;
}
This problem actually occured in the self-hosted code of Array iterator
next
function, used by for-of. At first we rewrote it as shown in the example above, but to properly handle the security model of SpiderMonkey, this trick was not enoughan extra branch which add a new return with a different object, which ., this issue goes away with Branch Pruning, as we wsee below.Known Properties
Another limitation of Scalar Replacement is the to identify propert at compil time. This implies that one cannot expect to have a working Scalar Replacement a loop iterating over the properties of an object, or the indexes of an array.
function norm1(vec) {
var sum = 0;
for (var i = 0; i < vec.length; i++)
sum += vec[i];
return sum;
}
This is one case where Loop Unrolling optimization might be useful in the future.
Lambdas
Lambdas are one of the cases where Scalar Replacement makes a lot of sense, especially in SpiderMonkey the scope chain objects as the underlying representation.
Each time you execute code gives a lambda literal as argument, a new function is created. This new function holds a pointer to its function environment, which itself hold a pointer to the scope chain of the enclosing function.
In cases where none of the lambdas within a function escape, we can use scalar replacement to optimize scope chain accesses within the inlined code of the lambdas.
For example, in the following code, the Inlining will add the code for the
forEach
function, and code of the lambda given as argument to the forEach
function. Then Scalar Replacement will detect that the lambda escape, forEach
and the lambda are inlined, and replace the scope chain holding the captured sum
variable a local variable.function norm1(vec) {
var sum = 0;
vec.forEach((x) => { sum += x; });
return sum;
}
At the same time, the scope chain allocation as well as the new function allocation holding it moved to bailout path. Thus, we no longer do any allocation to execute this function. In this case, Scalar Replacement makes this
forEach
call as fast as the C-like for loop equivalent.Scalar Replacement has a lot of pitfalls, but when all the conditions are met, it can remove tons of allocations while allowing JavaScript developers to use higher level of abstraction.
As soon as we make generic function, it becomes quite hard to avoid these pitfalls. Just the fact that the code is present, even if it is never executed causes these pitfalls to appear.
Branch Pruning
The goal of Branch Pruning is to remove unused branches. This is similar to the badly named Profiler Guided Optimization) phase that we have in static compilers, except that instead of only moving infrequently used branches, we remove them from IonMonkey’s generated code.
To do so, we instrument SpiderMonkey to count the number of time each block of code g executed. Then, when compiling the code, we check based on hand-crafted heuristics wether a block of code should be removed or not. The heuristics select branches are too complex and have predecessors with a large numbers of executions. If a block should be removed, then we replace the branch a bailout will fall back the Baseline compiled code to resume the execution.
This optimization alone does not bring much performance improvement. At best, it can the compiler by removing chunk of the workload, and help the instruction cache of the CPU by removing instructions from the pipeline.
Branch Pruning the other phases of IonMonkey. By removing unused branches, we improve other optimizations such as Scalar Replacement, Loop Invariant Code Motion,
Thus code has to handle error cases, such asthe following, can be modified by Branch Pruning to re the block never executed .
function doSomething(obj) {
if (theHighlyUnlikelyHappened())
throw new Error("aaaaaaahhh!!", obj);
}
In this example, IonMonkey will convert the then-block into a path which no longer merges back in the control flow graph, and bailout to Baseline. In JavaScript terms, this would be similar to doing a yield while we are in IonMonkey’s compiled code and resuming the execution in Baseline’s compiled code. Baseline’s compiled code still has the instruction for running the exception handling code.
// IonMonkey's compiled code
function doSomething(obj) {
if (theHighlyUnlikelyHappened())
bailout; // yield and resume in Baseline compiled code.
}
Frameworks
In today’s usage of JavaScript, a single page uses multiple framework. These frameworks are likely made to handle more than one use case. As a user of these frameworks, you are probably only using a few, either by convention or by habit.
The prom of Branch Pruning is that all the branches of code that you are not using at all w be removed. Thus unused branches would not prevent optimizations.
For example, as mentioned in Scalar Replacement optimization, Array iterator’s
next
function has an extra branch to support the security model of SpiderMonkey. This adds an extra branch is unlikely to be used by most website, thus Branch Pruning is able to remove this branch and replace it a bailout.As this branch is replaced by a bailout, this waive the limitation preventing Scalar Replacement. Thus, the fact that Branch Pruning removes code enableScalar Replacement to reduce the memory allocations, and also optimize the execution for-of loops.
Future Work
Scalar Replacement and Branch Pruning, when combined, are able to remove a lot of code allocations, and able to improve the speed of frameworks such as ES6 for-of loops (only a factor of 2 behind a C-like for loop). To make for-of loops as fast as C-like for loops, we would to remove the bounds checks on the array element indexes, as well as add support for already allocated object to Scalar Replacement.
Branch Pruning heuristics are too conservative today, that we wnot attempt to remove branches unless we are confident that the branch is not going to be used. This problem comes from the fact that bailouts are costly. Hopefully, this waddressed in the future a project ThreeHeadedMonkey.
No responses yet
Post a comment