JavaScript engines - how do they even?

0 0

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.