Franziska Hinkelmann - JavaScript engines
how do they even? FRANZISKA: Good morning, are you pumped about
low-level JavaScript stuff. That's what we are going to do now. I'm Franziska Hinkelmann
on the V8 team, in Munich, not that far away. V8 is being developed in Germany for the most
part. V8 is the JavaScript engine, and my work focuses a lot on performance optimisations
for ES6 and ES6 next features and I talk to you about JavaScript engines now. Engines:
why would you care about engines at all? Well, if you have any JavaScript source code and
you run it, it's always the engine that runs it for you. It doesn't matter if you run it
in the browser, or node node.js or an IoT device, to go from something you write to
executing that, that's what the engines are doing. JavaScript engines are the heart of
everything that we do. JavaScript engines have been evolved a lot in the last 22 years.
We can run massive complex frameworks and enterprise node.js service and there is a
lot of cool technology in JavaScript engines. I hope in the next 20 minutes to give you
a bit of an idea what's happening inside those engines, what is making your code run so fast?
I will be talking a little bit about performance, and I just want to point out when I talk about
performance, I mean specifically only JavaScript performance, like computing and running actual
JavaScript. I'm not talking about all the other things that are super important for
performance like DOM and rendering and network latency. When I say performance, I mean computing
and running JavaScript. There are several JavaScript engines, all the major browsers
have their own JavaScript engine, and it is really good there are several engines because
more engines mean competition. Competition really means better performance and better
adherence to the standard. In the major browsers, just to drop a few names, up there, JavaScript
- Spidermonkey is in Firefox by Mozilla and
V8 is in Chrome. If you run node.js, you know you need an engine. By default, node.js comes
with V8 but there is a Chakra core build of node.js and there you get node.js with a Microsoft
Chakra engine, and there is a SpiderNode project using Spidermonkey. Again, if you're working
on IoT, if you work on small devices, you might want to trade in performance for memory
size. The performance on the browsers are fast, they take up a lot of memory. On IoT
devices, you can take smaller engines somewhat slower but they fit, like Duktape or Jerryscript.
The ECMAScript is designed by the TC39 committee. They discuss additions and changes to the
language and formalise it as a standard, and then we engine implementers implement those
standards to give you JavaScript. That's really cool before we have a TC39 panel here this
afternoon, so some committee members are here to answer your questions, and they still take
questions, so you can Tweet some questions for the panel this afternoon. If you want
to know what is happening in the language, what is the take on a few changes, like that.
All right, so JavaScript is a standard. We implement that. And the engine is the thing
responsible for using the rules on the standard and then to run your JavaScript. What is a
really cool thing about JavaScript, besides the awesome community and JSConf? One thing
that I think is really cool is that if you write JavaScript code, and you have variables,
you can just say var x equals something. You don't have to worry about what that x actually
is. You can use var or const but you don't have to distinguish upfront if you have a
number, a string, or an array. If you have ever written C++, the rules are really, really
strict, and you first have to figure out and read up a lot about integers just to get your
first "hello world" program running. If you write C++ and want to define a variable that
values 17, you have to specify and know what you want to specify. In this case, I'm specifying
aspect and integer which can be a whole number, positive or negative. They can only be - within
a certain range, so, if your number gets too big, it doesn't fit into an integer any more.
If you write JavaScript, don't care about any of that. That makes it really simple for
us. We don't have to worry about this. It makes it easy to get started, it makes it
easier to explain it, it makes prototyping usually faster, so that's a really cool thing
for a language. And we call this the language is dynamically typed, so a language like C++
where you have to define this is considered statically typed. It is not only about the
basic types, strings where you think you can figure out where they are and say not not
much more work. This is for more complex objects. When you have any objects in JavaScript, you
can add and delete properties as you wish, as you need, you don't have to make that clear
beforehand. So this object here as the properties x and y but if needed, it can delete a property,
it can add a property. I have access to all the properties on the prototype change which
I can also change. So, that's something that makes it easy to work with objects, and sometimes,
it would be even impossible to specify beforehand what exactly your object is like. If you get
a bunk of Jason over the net and turn it into an object, sometimes, you don't know actually
what the properties will be. For us as developers, that's super useful. It makes it a little
bit easier. If you're a compiler, though, this is not good, because you give so little
information to the compiler, the compilers have a hard time generating machine code which
is fast if they have no information. That's why the point in C++ you specify all that
because the compiler needs this information upfront so it can compile your code into an
executable. C++ is statically typed not to make it hard for developers because that allows
you to generate fast machine code when you compile C++. But know know JavaScript is pretty
fast, right? We have huge libraries, huge frameworks, we run all these JavaScript tools
to transpile our code. JavaScript is really fast, even though it is dynamically typed,
and we have all this freedom when we are using objects and types. And the trick that all
modern JavaScript engines use is so-called just In Time compilation, abbreviated as JIT
compilation which means "just in time. What that means is we're not first compiling ahead
of tame, finished a compilation and then run the code, we are mixing these two steps together
and we're using information from running the code to recompiling the code. So we are compiling
the source code just in time as we need it, we collect some information when we run it,
and then we recompile this source code. If you think about C++ again which is compiled
ahead of time, it is two separate steps. You first compile it, you get an executable, and
then you run that executable. In JavaScript, that is one step. If you start a node - note
process, you say note server just - it is all together because compilation and execution
goes at the same time and there is feedback going back and forth to speed up the execution.
What modern engines have is they don't have one compiler, they have at least two compilers
where one of them is an optimising compiler. The main concept I want you to take away here
is we have an optimising compiler that is recompiling hot functions, so a function that
you're using a lot that is worth speeding up is considered hot, that is recompiled by
the optimising compiler which means we compile the code, we run it a few times, we collect
information about the types and then we say, "Oh, this function is not, let's make it faster
by using the information that we have got at so far." So when we're recompiling, when
we're optimising, we're recompiling assuming that we will see similar types as before,
so we bake in this information in the optimised machine code. Now since JavaScript does dynamically
type, no-one is forcing you to keep that same type, and you can change the kind of inputs
you give to your functions, so it might happen that, at some point, you run this optimised
function on different kind of objects and then you have to de-optimise, you can use
this optimised code for that, and you fall back to the baseline compiler. So, compile,
run a few times, optimise, assuming certain conditions, run the optimised code, if the
conditions fail, go back to the basic code. Now, so you start with JavaScript source code,
then the parser generates an abstract syntax tree. I will not talk about the parser because
my co-worker Marja will tell you how we parse JavaScript and how you can write it to make
it a little faster. The source code is consumed by the parser and then we generate an abstract
syntax stream. Then a compiler is using that abstract syntax stream to make the machine
code. We collect the information and pass it on to the optimising compiler to generate
faster machine code. Every once in a while, we have to bail out de-optimised, do an OSI
exit to go back to the slower baseline machine code. In the V8 engine, the baseline compiler
is an interpreter called Ignition, and the optimising compiler is called TurboFan. If
you hear about them not in relation to cars, it is about the compiler pipeline in V8! It
used to be crank shaft, we fixed it out, ignition, make Chrome and node fast. In Spidermonkey
the optimising compiler is Ironmonkey, and there are a few more around where Safari,
they don't have one optimising compilers but two, so a low-level interpreter and a DFG
optimising compiler and B3, and Chakra also has an optimising compiler. The optimising
compiler uses previously seen type information. If you change your objects all the time, then
we cannot generate good optimised code or if you've generated, you have to de-optimise
a lot. De-optimisation always means a small performance hit. From the high-level concept,
and now I want you show you on a really concrete example. I'm going to show you the optimised
machine code for this on an Intel processor. I'm using a very simple example. It is a load
function that takes the parameter and all it does is is it returns object at x. The
property examine is, you do it all - property axis in JavaScript is fairly complicated for
the compiler because if you have an object that a compiler doesn't know anything about
it and you want x, you don't know where is this x? Does this object have an x? Is it
may be under prototype chain? How are the properties stored for the object? Where in
the memory is the value for x? In implement, this does quite a lot of work to do something
like "object that x". So, one small thing I have to explain before we get started is
how objects are represented internally. We represent object types incrementally by transitioning
for every property to a new type. So, if you have an empty object literal, it is represented
by just object, basically. If you have a literal with a property x, then we transition from
the empty literal type to the next type that's a literal with an x property. And then if
you have more properties, we transition over to more types of objects, so that's an internal
representation since you don't have to specify a class or anything in JavaScript, you can
just modify object types as you want. Internally, we keep track of a type of objects. And because
of these transitions, it is actually making a difference if your object has x defined
first or y defined first, so just because two objects have the same properties, they're
not the same type internally. All right, so I'm running the load function a few times,
and I'm always running them with these objects here. They look similar, but it is not the
same objects. The X and Y values are obviously different. But all these objects have the
same shape. Internally, they all correspond to this kind of object. So, if I'm running
the function a lot, eventually, the compiler says, "Hey, this is a hot function, let's
optimise it" and this is what it is being optimised to. So this is assembly code. But
I will explain to you what is happening here. So, at the top, I left out a little bit of
stuff, this is where we said up the stack - set up the stack when we enter the function.
The important thing is here: this address corresponds to the type of the object that
we fed the function with. So, internally, this address represents an object that has
an x and a y. So this is optimised code that was generated after we have run the function
a few times. And it has memorised this type, and now when we run this function again in
assembly code low-level, register level, we load this type, and then we do a comparison.
We are comparing our parameter where it has the same type as what we saved before. We
run it and say does the new parameter look like the things we've seen in the past? If
the comparison is true, we move over here where we just - where we now are getting the
value of x. So this is the address of the object plus 17, which means take a memory
offset off the object because we know at this position, it is the x value. So this short
cut corresponds to the value for x. And we can just take that from memory and be done
with it. That is getting object of x. We don't have to look in the prototype chain or see
if there are side effects or anything. Just say if this kind of object comes, then the
value is here in memory. Now, if we are calling the optimised code with an object that looks
different than this kind of object, then the comparison of the object types is going to
fail, and we have to do a jump down to 5a and 5a is a de-optimisation bail-out. On that
schematic, de-optimisation bail-out is the point from where we go from the fast optimised
code back to the slow baseline code because we don't have optimised code to handle the
code if we run it with different kind of objects, because if we say, "For any kind of objects,
the x value is over here in memory", that would be wrong. When you write JavaScript
code, it is actually compiled to machine code like this, that looks different depending
on your system's architecture, that this is what is happening at the very basis when you're
running JavaScript code. When I ran this function with objects that have a different type - like
these objects here have different properties so we don't consider them the same type, one
has an a, one doesn't, it has a b property instead - to optimise the machine code looks
very similar to what we've just seen, instead of one type, we have four types now, one that
corresponds to every input that we have recorded before. So we do four comparisons, if we match
either one of those with the new object we've put in, we say short cut, take the value of
a memory from here, we are done with it. If none of them matches, then we jump, and, again,
we are jumping to a de-optimisation. So, depending on the input values that we've observed, the
optimised code looks different - like the first one had one comparison, this had four
comparisons. Now, you're saying, okay, so you're just adding comparisons for every single
input type, but you can see this doesn't sail because then you have these if compares, if
compares, it would take forever and really blow up memory. What we do is, if you have
more than four types, we actually don't compare to all the types any more, this address here
does not correspond to a specific type. This one just points to string x because we wanted
the property x, and we have to call into function which is now looking up property x in a big
pool of 3,000 entries. You can imagine in machine code, it looks short, but this is
an expensive call here. It is much more expensive than just saying, "Move this from memory over
here." As a performance tip at this really, really low level of engine-level performance
things, one thing that helps de-optimising compilers or the JavaScript engines in general
is if you are always using the same type of objects. So, if your objects represent the
same thing anyways, and if it is possible in not making your code terribly unreadable,
try to make them the same type for the engine. So, for example, in this case, this is exactly
the same information as to slides before, except that I'm always adding b, c. D, as
undefined to all the objects. Now, for the compiler, this is considered one object type,
and the optimised code is nice and short, there is exactly one type of object that corresponds
to this bigger object with all the parameters, and then there is one comparison saying is
this the same? Use the value done. This is the general idea. Store information or collect
information by running, recompile, assuming we get the exact same type of inputs - like
different values but similar types - and then the resulting code is really fast as long
as you don't change types. We recently implemented this speed-up for an ES6 feature. In ES6,
you have the option to define object literals with computer property names. In ES5 when
you had a variable as a key, you first had to create the object and then you could set
that property. So, if x is a variable and you want o of x, you have to create the literal.
In ES6, you can do this in one step. Just use the brackets inside the object notation.
We saw in benchmarks that this right-hand side is a lot slower than the ES5 equivalent.
You saw in the last talk the list of benchmarks, the yellow and green thing. This one was red
because it was so much slower than the ES5 counterpart. But we applied the same principle.
A lot of times in this kind of code, the x is a symbol, and every function runs is the
same symbol, so we applied this principle here. We run the code a few times, we memorise
what x is and optimise to a fast pass saying if it is the same symbol we've seen all the
time, then this is the kind of object we are creating instead of to make the these expensive
object transitions when we are creating this every time. So by applying these optimisation
principles, we've got a ten-times speed-up on that benchmark, and the yellow-green benchmark
list is on par with the ES5 equivalent. You can use this ES6 feature without having to
worry that it would slow down your performance if that is really critical to you. So far,
the high-level overview, you can't put too much into 25 minutes. I hope I was able to
give you some idea of what is going on. If you want to dig deeper into that, of course
you can try it out yourself and make your own experiments and see what is going on.
All the engines are open source, or all the engines I mentioned are open source. You can
get the source code, look at that, of course. But you can play around with it. You probably
have Node installed anyways, so use Note or Chrome and palace in a few - if you pass in
print opt code you will see the optimised code that I have just shown you. So, because
of how JavaScript is dynamically typed, we have to use JIT compilation to get any kind
of speed speed. Because of how the optimising compilers under JIT work, your JavaScript
code, if it's statically typed, then that's the best thing you can do for the compilers.
Thank you. [Cheering]. >> Thank you very much to Franziska for the
excellent introduction to how the engine works in JavaScript - very interesting topic. I
think when we work practically with JavaScript, we don't know what is going on on the system
level, we don't know how our code is being compiled. We're going to start again in just
a couple of minutes with our next talk, and, if anyone is here what is curious going on
in the other track, we have Ben Vinegar talking about source maps. Graph QL has put fliers
out on the table. They have a discount code. It is another conference that's going to be
taking place just across the river on 21st of this month. We will be starting in just
a couple of minutes.