Reputation: 85
I'm trying to understand the process of how a piece of JavaScript code is executed. So far, I've managed to have most of the layout pictured out, but there's a few gaps that I wish to cover.
I know that a computer's CPU only understands 0's and 1's. So eventually, any code we write (in a high level language) gets transformed into 0's and 1's and is then executed by the CPU. In the case of JavaScript, the main character that makes this journey possible is the JavaScript engine. So that's what I looked into and just picked chrome's V8 to help me picture the whole thing.
So the JavaScript engine first parses the code and eventually generates AST's (Abstract Syntax Tree). Those are then transformed into Bytecode by Ignition, which is v8's bytecode generator AND also the bytecode interpreter. Next comes the step where the code is actually executed and here's where i have trouble understanding what's going on. I've found out that the same Ignition "executes the bytecodes" and at the same time an Optimizing Compiler, Turbofan in this case, improves the speed of execution by better handling repeating code and then returning the optimized code as Machine Code back.
I thought that executing the bytecodes means converting them to machine code which the CPU will then run, but that's not the case. Since Turbofan is only an optimizing compiler, I've wondered what is it that converts the bytecodes to machine code? I've then found that V8 doesn't compile all functions to machine code, only those that run hot enough for optimized compilation to (likely) be worth the time investment
So, what does it mean for bytecode to be "executed"? The CPU doesn't understand bytecode and the bytecode is not transformed into machine code either. Can anyone explain in simple terms what's going on?
Upvotes: 5
Views: 1566
Reputation: 40501
An interpreter is a program that executes another program; that doesn't require translating that other program to machine code first.
The bytecode interpreter in V8 consists of machine instructions itself, so the CPU executes the interpreter.
To illustrate, imagine we wanted to implement our own programming language. To keep it simple, suppose this language's purpose was to execute arithmetic instructions written in plain English; and we're writing an interpreter for it in JavaScript.
A valid program in our language would be "three plus two"
. A first version of an interpreter for it might be something like:
function interpret(program) {
let instructions = program.split(" ");
let current = 0;
function LiteralValue(inst) {
switch (inst) {
case "one": return 1;
case "two": return 2;
case "three": return 3;
// TODO: add other numbers
}
}
for (let i = 0; i < instructions.length; i++) {
switch (instructions[i]) {
case "one":
case "two":
case "three":
// TODO: add other numbers
current = LiteralValue(instructions[i]);
break;
case "plus":
current = current + LiteralValue(instructions[i+1]);
i++; // We've just consumed the next instruction.
break;
// TODO: add support for "minus" etc.
}
}
return current;
}
This isn't a very good interpreter, but it demonstrates the principle of executing a program by interpreting it: an interpreter "looks at the program", sees what the program wants to get done, and does that. It doesn't convert the program to machine code first; it sees "plus"
and executes +
.
One could say that "plus"
is one of our "bytecodes" (so dead simple that it's actually just the same as the keyword), and the snippet current = current + ...
is its "bytecode handler".
Since we used JavaScript for this example, which itself is executed by an interpreter (at least before optimization kicks in), we even get three levels of stacking here: "five plus two"
is a program in our custom language that's executed by another program (the function interpret(...)
) that's executed by another program (the JS engine in your browser) that's finally executed by the CPU.
Upvotes: 4