JS does *not* offer zero-cost abstractions!

Arpad “Swatinem” Borsos

https://slides.com/swatinem/zero-cost

Disclaimers

We have four major engines

v8 (Google)

SpiderMonkey (Mozilla)

jsc [javascript core] (Apple)

Chakra (Microsoft) [now open source, yay! \\o//]

 

+ some less widely known ones

Different

Optimizations,

Performance Characteristics

All code will be “modern JS” (ES6, ES2015, whatever you want to call it)

 

There will be no numbers, I’m sorry.

Zero cost?

JS is

just-in-time compiled

very dynamic

Rule of thumb:

if it looks like a function call

it is a bloody function call!

nothing is less than something

or

doing no work is better that doing little work

developer

productivity

runtime

performance

or

Example 1: private members

function MyClass(a) {
    return {
        getA() { return a; }
    };
}

// this creates a fresh function object every time!

const a = new MyClass(1), b = new MyClass(1);
a.getA !== b.getA;

// also, things are *impossible* to debug / inspect

Example 1: private members

const _a = Symbol("a");

class MyClass() {
    constructor(a) {
        this[_a] = a;
    }
    getA() {
        return this[_a];
    }
}

// method is shared using the prototype chain
// you can still inspect privates in devtools

// BUT! prototype lookup

Example 2: closures

function asyncAction(payload) {
    return (dispatch, getState) => {
        setTimeout(() => dispatch({
            type: "SOME_ACTION",
            payload,
        }), getState().delay);
    };
}

// This allocates new function objects every time!

Example 2: closures

function asyncAction(payload) {
    return (dispatch, getState) => { // here
        setTimeout(() => dispatch({ // and here!
            type: "SOME_ACTION",
            payload,
        }), getState().delay);
    };
}

asyncAction(1) !== asyncAction(2);

// nothing we can really do about it,
// in this case at least

Example 3: Promises

fetch("foo")
.then(req => req.json())
.then(json => {
    doSomeProcessing(json);
});

// Promises are awesome!
// (apart from being uncancelable, which sucks :-(

Example 3: Promises

const p1 = fetch("foo");
const p2 = p1.then(req => req.json());
const p3 = p2.then(json => {
    doSomeProcessing(json);
});

// .then allocates intermediate promise objects

p1 !== p2 && p2 !== p3

Example 3: Promises

const toJSON = req => req.json();

// later inside a function:

fetch("foo")
.then(toJSON)
.then(doSomeProcessing);

// arrow functions are awesome!
// they are lightweight to type <3
// but they have a runtime cost

Example 4: JSX

const Component = ({onClick, children}) => (
    <h1 onClick={() => onClick()}>
        <span>Oh hi!</span>
        {children}
    </h1>
);

// "declarative" templates ?

Example 4: JSX

const Component = ({onClick, children}) => {
    return React.createElement("h1",
        // call, allocates new object

        { // allocates new object
            onClick: () => onClick(),
            // allocates new function object
        },

        React.createElement("span", null, "Oh hi!"),
        // call, allocates new object

        children);
};

Example 4.1: Alternatives?

// from: https://github.com/maxogden/yo-yo

const list = items => yo`<ul>
    ${items.map(item => yo`<li>${item}</li>`)}
</ul>`;

// tagged template strings are function calls
// also, template strings are *strings*!!!
// do you really want to parse strings
// *every single time*?

// also that particular library uses *real*
// dom elements to do the diffing *facepalm*

Example 4: JSX

compilers can help, for example by moving constant elements like the `<span>` out of the function, to avoid calling and allocating every time

 

avoid inline arrow functions, or inline `.bind()`

move binding to the constructor if possible, sadly not for stateless function components :-(

Example 5: Meta programming

// from https://github.com/paldepind/union-type/
const Action = Type({
    Up: [], Right: [], Down: [], Left: []});

const advancePlayer = (action, player) =>
    // call, allocates new object
    Action.case({
        // all of the below: allocate new functions
        // captures arguments in closure

        Up: () => ({x: player.x, y: player.y - 1}),
        Right: () => ({x: player.x + 1, y: player.y}),
        Down: () => ({x: player.x, y: player.y + 1}),
        Left: () => ({x: player.x - 1, y: player.y}),
        _: () => player,
    }, action);

Example 5: Meta programming

// from https://github.com/paldepind/union-type/

// better:
// allocates objects and functions only once!
// can still have some overhead depending on impl

const advancePlayer = Action.caseOn({
    Up: (player) => ({x: player.x, y: player.y - 1}),
    Right: (player) => ({x: player.x + 1, y: player.y}),
    Down: (player) => ({x: player.x, y: player.y + 1}),
    Left: (player) => ({x: player.x - 1, y: player.y}),
    _: (player) => player
});

Example 6: For-of

// yesterday
for (let i = 0; i < iterable.length; i++) {
    const elem = iterable[i];
    // ugly to write
}

// or
iterable.forEach(elem => {
    // uses closure
    // does not work with `NodeList` / `arguments`
});

// today
for (const elem of iterable) {

}

Example 6: For-of

// desugars into, roughly:
const iter = iterable[Symbol.iterator]();
while (true) {
    const {elem, done} = iter.next();
    if (done) { break; }
}

// awesome!
// works with `NodeList`, `arguments`
// supports custom iterators and generators

// sadly, 3-8x slower than straight for loop
// *sadface*

Exercise

[1, 2, 3, 4, 5]
    .map(x => x * 2)
    .filter(x => x % 2)
    .reduce((a, x) => a + x, 0);

// whats the runtime cost of this code?

Conclusion

Developer productivity (convenience) is king!

Keep it simple!

 

Compilers can help a lot, like moving closures and constant react elements out of functions when possible

+ bundling and dead code elimination

 

Just wanted to raise awareness.

Know what the runtime cost of the code is that you write!

Outlook?

JS (especially asmjs and wasm) is a compiler target:

Bundlers (!!!), jsx, babel, typescript,

C++ and other LLVM languages via emscripten, etc…

 

Lets embrace it and do expensive things at compile time and not at run time!

a small rant

Why are JS devs so afraid of doing things at compile time?

Compile times?

# incremental dev builds with hot reloading

webpack built 1e4b27cb4e32b7bb3979 in 264ms
webpack building...
webpack built 992e6902639bbce8f527 in 2012ms
webpack building...
webpack built e113b1cb712bb9eec0f4 in 1147ms
webpack building...
webpack built 2ff58130c885d6d68090 in 2413ms
webpack building...
webpack built 68ebf18365f67cfd64d9 in 1346ms

# production build with uglify

npm run build 2> /dev/null  59,42s user 2,41s system 102% cpu 1:00,16 total

static analysis

+ another rant…

 

import/export modules is the best thing that happened, because they are statically analyzable

 

but node core developers want to force `*.mjs` on us :-(

// lib/foo.js

export default (a, b) => a + b;

// module.js

import add from "lib/foo.js"

add(1, 2);

the thing about types

TypeScript and Flow becoming more popular!

 

Can avoid real problems/bugs (undefined not a function), …

Can optimize code based on type guarantees…

// like rewriting
for (const elem of array) {

}

// into
for (let i = 0; i < array.length; i++) {
    const elem = array[i];
}

Why isn’t anyone doing it?

what React should have been

// a compiler plugin!
// it kind of is due to jsx, but it does not go far enough!

// transform this:
const FnComponent = props => (
    <h1>{props.title}</h1>
);

// into something similar to this:
class FnComponent {
    constructor() {
        this.elem = document.createElement('h1');
        this.text = document.createTextNode();
        this.elem.appendChild(this.text);
    }
    render(props) {
        this.text.data = props.title;
    }
    // …
}

// not creating / comparing useless js objects all the time
// just updates the data that’s needed

// generate different code for client or SSR *at compile time*!

JS does *not* offer zero-cost abstractions

By Arpad Borsos

JS does *not* offer zero-cost abstractions

Just raising awareness about runtime cost of things you do in JS.

  • 1,279
Loading comments...

More from Arpad Borsos