[Translated] ultra fast analyzer (II): parsing an inert

This is how to parse the JavaScript series as quickly as possible the second part of the V8. In the first part we have explained how to make the V8 scanner faster.

Analytical compiler (V8, a bytecode compiler Ignition step) providing a source code into an intermediate representation. Parsed and compiled in a critical process web page to start rendering, rather than the need to provide all of these features to the browser immediately during startup page. Although developers can use asynchronous and delay the script, but that's not been able to effect. In addition, many web pages provide only certain features of code used during the operation of a small portion of the page, the user may not be able to use these features.

Eager to compile unnecessary code may bring the actual resource consumption:

  • CPU cycles used to create the code to start when the page is actually delayed the validity of the code.
  • Memory for code object, at least in the byte code Refresh determine a current does not take up, and is allowed to occupy the memory garbage collection time.
  • When the top-level script execution is complete, the final compiled code is cached on disk take up disk space.

For these reasons, all major browsers implement the inert resolved . The parser generates for each function is not an abstract syntax tree, and then compiled to bytecodes, but "pre-analysis" as a function of actually encountered, but not all resolved. This is by switching to the use of pre-parser to do, it is a copy of a parser, only the most basic work, otherwise skip function. Verify that it skips the pre-parser functions are syntactically valid, and generates all the information needed to compile the correct external function. After calling a function when pre-resolved, according to need, it is completely parsed and compiled.

Variable assignment

So that the main problem is complicated by pre-analytical variables are allocated.

In consideration for performance reasons, management activation function on the machine stack. For example, if a function gparameter 1and 2calls the function f:

function f(a, b) {
  const c = a + b;
  return c;
}

function g() {
  return f(1, 2);
  // 这里返回的是 `f` 的指针调用,返回结果指向这儿
  // (因为当 `f` 返回时,它会返回到这里)。
}
复制代码

First receiver (such as fthe thisvalue, that is globalThis, because it is an arbitrary function call) pushed onto the stack, then the function is called f. Then parameters 1and 2pushed onto the stack. At this time the function fis called. To make the call, we first stored on the stack gstatus: return finstruction pointer ( rip; we need to return to what kind of code) and "frame pointer" ( ; fpreturn stack should be what kind of). Then we enter f, it is a local variable cdistribution space, as well as any temporary space it may need. This ensures that if a function is called out of scope, then the function uses the data will not be used: it simply is popped from the stack.

Function call fstack layout when the dispensing parameters on the stack a, band local variables c.

The problem with this scenario is that the function can refer to variables outside the function declaration. Internal function, you can create more than their call, a longer period:

function make_f(d) { // ← `d` 的声明
  return function inner(a, b) {
    const c = a + b + d; // ← `d` 的引用
    return c;
  };
}

const f = make_f(10);

function g() {
  return f(1, 2);
}
复制代码

In the example above, from innerthe make_fdeclared local variable dis referenced make_fbefore returning calculated. To achieve this, the use of lexical closures language virtual machine assigned variables in the structure referred to as a "context" in reference to the variable reference function from inside the heap.

Call make_fstack layout when the parameters are copied to the assignment in the context of the heap for subsequent innercapture dused.

This means that for variables declared in a function, we need to know whether internal function variable was referenced in order to decide whether stored on the stack, or stored in the context heap allocation. When we calculate a literal function, we assign a closure, which points to the function of the code and the current context: context includes variable values ​​may need to access it.

Simply put, we at least need to track references the pre-parser variables.

If we only track references, it will overestimate variable references. Variables declared in the external function may be covered by internal re-statement of function, so that the pointing function declaration inside from the internal reference, rather than an external declaration. If there is no limit to save external variables in the context of the performance will be affected. Therefore, the pre-analytical variables are allocated properly executed, we need to ensure the pre-analytic function properly tracking variable references and declarations.

Top-level code is an exception to this rule. Top-level script always allocated heap memory, because the variable cross-scripting is visible. A simple way to achieve this is close to a better architecture is simple to run pre-parser, without the need to quickly resolve tracking variables topmost function; and only use the full parser for internal functions, and compile them to skip this step . While this is higher than the pre-analytical costs, because we do not have to build the entire AST, but let's start it up and running. These happen to be done in V8 V8 v6.3 / Chrome 63 and later versions of.

Information inform the pre-parser variable

Pre-parser tracking variable assignments and references are very complicated, because in JavaScript, it is not clear from the very beginning the meaning of the expression. For example, suppose we have a parameterized dfunction f, it has an internal function g, this expression seems possible references d.

function f(d) {
  function g() {
    const a = ({ d }
复制代码

It may end up references d, because we see them tokenas part of the destructor of an assignment.

function f(d) {
  function g() {
    const a = ({ d } = { d: 42 });
    return a;
  }
  return g;
}
复制代码

It may eventually be a parameter having destructor darrow function, in this case, fis dnot greferenced.

function f(d) {
  function g() {
    const a = ({ d }) => d;
    return a;
  }

  return [d, g];

}
复制代码

Initially, we pre-parser as a standalone copy of the parser to achieve, not too many things in common, which leads to two parsers over time and produce different. By pre-parser-based parser and ParserBaserewrite realize template recursive mode , we try to let the largest possible share while maintaining the performance benefits of an independent copy. This greatly simplifies the tracking of all the variables added to the pre-parser, because most implementations can be shared between the parser and pre-parser.

In fact, ignoring variable declarations and references even top-level function is not correct. ECMAScript specification required to detect various types of variables to resolve the conflict in the first script. For example, if a variable in the same scope to be declared twice, then it is considered to be a pre syntax error . Because we just skip the pre-parser variable declarations, it would be wrong to allow the code through the preparation phase. This time, we think of performance optimization has violated the norms. However, pre-parser now correctly tracking variables, we eliminate this type of behavior and other violations of norms of multivariate analysis, and there is no significant performance overhead.

Skip internal function

As we mentioned before, when the function after the first call to the pre-parsing, we perform a comprehensive resolution, and will generate AST compiled to bytecode.

// 这是顶层作用域
function outer() {
  // 预解析完成
  function inner() {
    // 预解析完成
  }
}

outer(); // 全面解析并且编译 `outer`,而不是 `inner`。
复制代码

This function directly to the external context, which contains the values required to use the internal function declaration variables. In order to allow inert compiled function (and supported debuggers), pointing to a context called ScopeInfometadata objects. ScopeInfoObjects described in the context of the listed variables. This means that the compiler internal function, we can calculate the location of the variable in the context of the chain.

However, to calculate the delay itself is compiled function needs context, we need to perform the scope resolution once again: we need to know the delay in compiling nested function in the function references a variable delay function declaration. We can pre-compiled again calculated by. This is the V8 until the V8 v6.3 / Chrome 63 are implemented. But this is not the ideal performance optimization, because it makes the relationship between the size of the source code and parse costs becomes nonlinear: We will prepare as much as possible nested functions. In addition to the dynamic nature of nested procedures, typically JavaScript code packer encapsulated in " function can be called directly the expression " (IIFEs) in the most JavaScript program having a plurality of nested layers.

Each re-parsing will increase by at least the cost of analytic functions

In order to avoid non-linear performance overhead, even the implementation of the global scope of our resolve in the pre-parsing. We store sufficient metadata so that you can simply skip the internal functions, without having to pre-parsed. One way is to store the variable name inside the function references. This is a large overhead storage, and we are still requires repetitive work: We have pre-analytical variables during the execution of the resolution.

Instead, we will serialize a number of variables, which are assigned as a dense array tag for each variable. When we resolve a function of the delay, in accordance with its pre-parser to see re-created variables, and we can simply apply metadata variables. This function has now been compiled, no longer need to allocate variable metadata, and can be garbage collected. Since we only need this function metadata actually contains an internal function, so most of these functions do not even need metadata, which significantly reduces memory overhead.

By tracking metadata functions have been pre-parsed, we can completely skip the internal function.

Skip performance impact caused by the internal function is non-linear, like a pre-parsing overhead caused by internal functions of the same again. Some sites will be promoted to the top all the functions scope. Because of their nesting level is always 0, so the cost will always be zero. However, many modern websites, in fact, have a deep nesting. On these sites, when the V8 v6.3 / Chrome 63 starts this feature, we see a significant performance boost. The main advantage is that the code now nesting depth of the site is no longer important: any function that occurs only once up to the pre-parsing, once fully resolve [1] .

The main thread and the main thread of non-resolution time, before and after the start "Skip internal function" optimization.

Possibly-Invoked Function Expression

As described above, the packetizer module code by encapsulating them in a closure called immediately, and the plurality of modules are combined into one file. This provides isolation between modules to allow them to be like the script to run as a unique code. These functions are inherently nested scripts; these functions are called immediately upon execution of the script. Packaging typically provide function expression can be called directly (IIFEs; pronounced "iffies") as a function of the brackets: (function(){…})().

Because these functions during script execution is immediate need to use, so the pre-treatment of these functions is not the best method. During the execution of the script in the top level, we need to compile the function immediately and fully parse and compile the function. This means that we try to resolve executed when acceleration starts in front of the faster, more bound to create unnecessary overhead startup.

You may ask, why not simply compile a function call it? While developers in the calling function is easy to notice, but the parser is not the case. Parser needs to make a decision - even before the start parsing function! - Is eager compiled functions or postpone compilation. Syntax that ambiguity simply fast scan to the end of the function becomes difficult, and the cost and quickly preresolved similar to the conventional.

Thus there are two V8 simple pattern, it can be identified as possibly-invoked function expression (PIFEs; pronounced "piffies"), and faster compile a parsing function According to this mode:

  • If the function is a function expression in parentheses, such as shape (function(){…}), we assume it is called. We look to see the beginning of this model, that is (function.
  • From V8 v5.7 / Chrome 57 start, we also examined by the UglifyJS generated pattern !function(){…}(),function(){…}(),function(){…}(). We saw !function, or ,functionif it is currently followed by a PIFE, then this test will come into play.

Because premature V8 compiler PIFEs, so they can be used as a control feedback information [2] , the feedback information tells the browser which function needs to start.

When the V8 is still repeat resolve internal functions, some developers have noted the influence of JS resolve to start running quite large. This optimize-jspackage is based on the static inference function converted to PIFEs. When you create a package, which has great influence on the performance of the V8 load. By the V8 v6.1 running optimize-jsbenchmarks provided, we reproduce these results, just look at minimizing the script after compression.

Premature PIFEs parsed and compiled to make hot and cold loading loading (loading of the first page and the second page, the total time resolved measurement of the execution time + + compiler, etc.) slightly faster. However, due to the significant improvement of the parser, which gives the V8 v7.5 brings performance increase than the V8 v6.1 for performance gains is much smaller.

Now, however, we will not repeat resolve internal functions, and because the parser is fast enough, the optimize-jsperformance obtained is also greatly reduced. In fact, the default configuration v7.5 has been optimized to run much faster than the version on v6.1. Even in v7.5, for the code required during startup, you can still use a small amount PIFEs: we avoid the pre-analytical, because we knew from the start that need this feature.

optimize-jsBenchmark results do not fully represent the reality of the situation. Synchronization script is loaded, the entire analytical + compilation time is calculated as the load time. In real-world scenario, you might use <script>the tag to load the script. This makes Chrome's pre-loader can be calculated in the script before find it and download it without blocking the thread note, parse and compile the script. We decided ahead of time to compile all the things that are outside of the main thread is automatically compiled, and should be the minimum calculated only at startup. Using non-main thread running script compiler will magnify the impact PIFEs brings.

But there are still costs, especially the cost of memory, so too early to compile everything is not a good idea:

Compiled ahead of all JavaScript code that will pay a lot of memory overhead.

Although the need to add brackets to the function during the start is a good way (for example, after the start of the analysis based on), but the use of optimize-jsthe package to apply a simple static inference is not a good idea. For example, it is assumed that a function is called during the initial compilation, and this function is a parameter to a function. However, if such a function of the entire module implementation requires a long time to compile, it will eventually compile too much stuff. Premature compiler is not conducive to performance: no delay compiled V8 will significantly reduce load times. In addition, optimize-jssome of the advantages derived from UglifyJS compressors and other problems, they are removed from the bracket portion not PIFEs PIFEs thereby remove this form can be applied as a common module defined - helpful hints on the style module. This could be a problem compressor should be repaired, so you can get the best performance on premature compiled PIFEs browser.

Epilogue

Inert resolve to speed up the startup speed, and reduced memory overhead of the application, the code for these application delivery will be better than they need. Variable declarations and can refer to the correct track in the pre-parser is necessary, this can be carried out quickly and pre-parsed correctly (according to specification). In the pre-allocated variables parser also allows us serialized variable information assigned for use in the subsequent parser, so that we can completely avoid pre-resolved internal function again, to avoid non-linear analysis of the behavior of deeply nested function.

Can be recognized by the parser PIFEs avoids the need to pre-parsing overhead initialization code is brought immediately to start the process. Careful use PIFEs guide profiles, or by a packer, may be provided with a cold start of the deceleration. However, it should avoid unnecessary function will be triggered enclosed in parentheses infer this way, because it leads to more code is compiled prematurely, leading to poorer performance and start to use more memory.


  1. Due to memory reasons, do not use the V8 for some time to refresh the byte code . If you need to use the code later, we will re-parse and compile it. Because we allow variable metadata failed during compilation, which will lead to resolve internal function again recompiled delay time. At this point we re-create the metadata for its internal function, so no pre-analytic function inside a function within it again. ↩︎

  2. PIFEs can also be seen as an expression based on a function brief information. ↩︎

If you find there is a translation error or other areas for improvement, welcome to Denver translation program to be modified and translations PR, also obtained the corresponding bonus points. The beginning of the article Permalink article is the MarkDown the links in this article on GitHub.


Nuggets Translation Project is a high-quality translation of technical articles Internet community, Source for the Nuggets English Share article on. Content covering Android , iOS , front-end , back-end , block chain , product , design , artificial intelligence field, etc., you want to see more high-quality translations, please continue to focus Nuggets translation program , the official micro-blog , we know almost columns .

Guess you like

Origin juejin.im/post/5cf33bd751882579e53f0130