Skip to content

Latest commit

 

History

History
465 lines (347 loc) · 19.4 KB

language-features.md

File metadata and controls

465 lines (347 loc) · 19.4 KB

Language Features

This document lists JavaScript language features and provides info with regard to their performance. In some cases it is explained why a feature used to be slow and how it was sped up.

The bottom line is that most features that could not be optimized previously due to limitations of crankshaft are now first class citizens of the new compiler chain and don't prevent optimizations anymore.

Therefore write clean idiomatic code as explained here, and use all features that the language provides.

Table of Contents generated with DocToc

Function Bind

Why Was Bind Slow?

  • performance of Function.prototype.bind and bound functions suffered from performance issues in crankshaft days
  • language boundaries C++/JS were crossed both ways which is expensive (esp. calling back from C++ into JS)
  • two temporary arrays were created on every invocation of a bound function
  • due to crankshaft limitations this couldn't be fixed easily there

What Changed?

  • entirely new approach to how bound function exotic objects are implemented
  • crossing C++/JS boundaries no longer needed
  • pushing bound receiver and bound arguments directly and then calling target function allows further compile time optimizations and enables inlining the target function into the caller
  • TurboFan inlines all mononomorphic calls to bind itself
  • resulted in ~400x speed improvement
  • the performance of the React runtime, which makes heavy use of bind, doubled as a result

Recommendations

  • developers should use bound functions freely wherever they apply without having to worry about performance penalties
  • the two below snippets perform the same but arguably the second one is more readable and for the case of arr.reduce is the only way to pass this as it doesn't support passing it as a separate parameter like forEach and map do
// passing `this` to map as separate parameter
arr.map(convert, this)

// binding `this` to the convert function directly
arr.map(convert.bind(this))

Resources

instanceof and @@hasInstance

  • latest JS allows overriding behavior of instanceOf via the @@hasInstance well known symbol
  • naively this requires a check if @@hasInstance is defined for the given object every time instanceof is invoked for it (in 99% of the cases it won't be defined)
  • initially that check was skipped as long as no overrides were added EVER (global protector cell)
  • Node.js Writable class used @@hasInstance and thus incurred huge performance bottleneck for instanceof ~100x, since now checks were no longer skipped
  • optimizations weren't possible in these cases initially
  • by avoiding to depend on global protector cell for TurboFan and allowing inlining instancof code this performance bottleneck has been fixed
  • similar improvements were made in similar fashion to other well-known symbols like @@iterator and @@toStringTag

Recommendations

  • developers can use instanceof freely without worrying about non-deterministic performance characteristics
  • developers should think hard before overriding its behavior via @@hasInstance since this magical behavior may confuse others, but using it will incur no performance penalties

Resources

Reflection API

  • Reflect.apply and Reflect.construct received 17x performance boost in V8 v6.1 and therefore should be considered performant at this point

Resources

Array Builtins

  • Array builtins like map, forEach, reduce, reduceRight, find, findIndex, some and every can be inlined into TurboFan optimized code which results in considerable performance improvement

  • optimizations are applied to all major non-holey elements kinds for all Array builtins

  • for all builtins, except find and findIndex holey floating-point arrays don't cause bailouts anymore

  • V8: Behind the Scenes (February Edition) - 2017

  • V8 Release 6.1 - 2017

  • V8 release v6.5 - 2018

const

  • const has more overhead when it comes to temporal deadzone related checks since it isn't hoisted
  • however the const keyword also guarantees that once a value is assigned to its slot it won't change in the future
  • as a result TurboFan skips loading and checking const slot values slots each time they are accessed (Function Context Specialization)
  • thus const improves performance, but only once the code was optimized

Recommendations

  • const, like let adds cost due to TDZ (temporal deadzone) and thus performs slightly worse in unoptimized code
  • const performs a lot better in optimized code than var or let

Resources

Iterating Maps and Sets via for of

  • for of can be used to walk any collection that is iterable
  • this includes Arrays, Maps, and Sets

Why was it Slow?

  • set iterators where implemented via a mix of self-hosted JavaScript and C++
  • allocated two objects per iteration step (memory overhead -> increased GC work)
  • transitioned between C++ and JS on every iteration step (expensive)
  • additionally each for of is implicitly wrapped in a try/catch block as per the language specification, which prevented its optimization due to crankshaft not ever optimizing functions which contained a try/catch statement

What Changed?

  • improved optimization of calls to iterator.next()
  • avoid allocation of iterResult via store-load propagation, escape analysis and scalar replacement of aggregates
  • avoid allocation of the iterator
  • fully implemented in JavaScript via CodeStubAssembler
  • only calls to C++ during GC
  • full optimization now possible due to TurboFan's ability to optimize functions that include a try/catch statement

Recommendations

  • use for of wherever needed without having to worry about performance cost

Resources

Iterating Maps and Sets via forEach and Callbacks

  • both Maps and Sets provide a forEach method which allows iterating over it's items by providing a callback

Why was it Slow?

  • were mainly implemented in C++
  • thus needed to transition to C++ first and to handle the callback needed to transition back to JavaScript (expensive)

What Changed?

  • forEach builtins were ported to the CodeStubAssembler which lead to a significant performance improvement
  • since now no C++ is in play these function can further be optimized and inlined by TurboFan

Recommendations

  • performance cost of using builtin forEach on Maps and Sets has been reduced drastically
  • however an additional closure is created which causes memory overhead
  • the callback function is created new each time forEach is called (not for each item but each time we run that line of code) which could lead to it running in unoptimized mode
  • therefore when possible prefer for of construct as that doesn't need a callback function

Resources

Iterating Object properties via for in

Incorrect Use of For In To Iterate Object Properties

var ownProps = 0
for (const prop in obj) {
  if (obj.hasOwnProperty(prop)) ownProps++
}
  • problematic due to obj.hasOwnProperty call
    • may raise an error if obj was created via Object.create(null)
    • obj.hasOwnProperty becomes megamorphic if objs with different shapes are passed
  • better to replace that call with Object.prototype.hasOwnProperty.call(obj, prop) as it is safer and avoids potential performance hit

Correct Use of For In To Iterate Object Properties

var ownProps = 0
for (const prop in obj) {
  if (Object.prototype.hasOwnProperty.call(obj, prop)) ownProps++
}

Why was it Fast?

  • crankshaft applied two optimizations for cases were only enumerable fast properties on receiver were considered and prototype chain didn't contain enumerable properties or other special cases like proxies
  • constant-folded Object.hasOwnProperty calls inside for in to true whenever possible, the below three conditions need to be met
    • object passed to call is identical to object we are enumerating
    • object shape didn't change during loop iteration
    • the passed key is the current enumerated property name
  • enum cache indices were used to speed up property access

What Changed?

  • enum cache needed to be adapted so TurboFan knew when it could safely use enum cache indices in order to avoid deoptimization loop (that also affected crankshaft)
  • constant folding was ported to TurboFan
  • separate KeyAccumulator was introduced to deal with complexities of collecting keys for for-in
  • KeyAccumulator consists of fast part which support limited set of for-in actions and slow part which supports all complex cases like ES6 Proxies
  • coupled with other TurboFan+Ignition advantages this led to ~60% speedup of the above case

Recommendations

  • for in coupled with the correct use of Object.prototype.hasOwnProperty.call(obj, prop) is a very fast way to iterate over the properties of an object and thus should be used for these cases

Resources

Object Constructor Subclassing and Class Factories

  • pure object subclassing class A extends Object {} by itself is not useful as class B {} will yield the same result even though class A's constructor will have different prototype chain than class B's
  • however subclassing to Object is heavily used when implementing mixins via class factories
  • in the case that no base class is desired we pass Object as in the example below
function createClassBasedOn(BaseClass) {
  return class Foo extends BaseClass { }
}
class Bar {}

const JustFoo = createClassBasedOn(Object)
const FooBar = createClassBasedOn(Bar)
  • TurboFan detects the cases for which the Object constructor is used as the base class and fully inlines object instantiation

Recommendations

  • class factories won't incur any extra overhead if no specific base class needs to be mixed in and Object is passed to be extended from
  • therefore use freely wherever if mixins make sense

Resources

Tagged Templates

Resources

Typed Arrays and ArrayBuffer

Recommendations

  • TypedArrays should be used wherever possible as it allows V8 to apply optimizations faster and more aggressively than for instance with plain Arrays
  • any remaining bottlenecks will be fixed ASAP as TypedArrays being fast is a prerequisite of Webgl performing smoothly

Resources

Object.is

  • one usecase of Object.is is to check if a value is -0 via Object.is(v, -0)
  • previously implemented as C++ and thus couldn't be optimized
  • now implemented via fast CodeStubAssembler which improved performance by ~14x

Resources

Regular Expressions

  • migrated away from JavaScript to minimize overhead that hurt performance in previous implementation
  • new design based on CodeStubAssembler
  • entry-point stub into RegExp engine can easily be called from CodeStubAssembler
  • make sure to neither modify the RegExp instance or its prototype as that will interfere with optimizations applied to regex operations
  • named capture groups are supported starting with V8 v6.4

Resources

Destructuring

  • array destructuring performance on par with naive ES5 equivalent

Recommendations

  • employ destructuring syntax freely in your applications

Resources

Promises Async/Await

  • native Promises in V8 have seen huge performance improvements as well as their use via async/await
  • V8 exposes C++ API allowing to trace through Promise lifecycle which is used by Node.js API to provide insight into Promise execution
  • DevTools async stacktraces make Promise debugging a lot easier
  • DevTools pause on exception breaks immediately when a Promise reject is invoked

Resources

Generators

  • weren't optimizable in the past due to control flow limitations in Crankshaft
  • new compiler chain generates bytecodes which de-sugar complex generator control flow into simpler local-control flow bytecodes
  • these resulting bytecodes are easily optimized by TurboFan without knowing anything specific about generator control flow

Resources

Proxies

  • proxies required 4 jumps between C++ and JavaScript runtimes in the previous V8 compiler implementation
  • porting C++ bits to CodeStubAssembler allows all execution to happen inside the JavaScript runtime, resulting in 0 jumps between runtimes
  • this sped up numerous proxy operations
    • constructing proxies 49%-74% improvement
    • calling proxies up to 500% improvement
    • has trap 71%-428% improvement, larger improvement when trap is present
    • set trap 27%-438% improvement, larger improvement when trap is set

Recommendations

  • while the use of proxies does incur an overhead, that overhead has been reduced drastically, but still should be avoided in hot code paths
  • however use proxies whenever the problem you're trying to solve calls for it

Resources