📜 ⬆️ ⬇️

Explore and optimize Object # toString performance in ES2015

Benedict Meirer of Google’s Munich office is working on optimizing JavaScript. In this material, he talks about the features of the implementation and operation of Object.prototype.toString() in the V8 engine. In particular, it’s about why this construct is important, how it has changed with the advent of ES2015 symbols, and the optimization approach that Mozilla engineers have suggested, which resulted in an approximately sixfold increase in performance of toString() in V8.

image

Introduction


The ECMAScript 2015 standard introduced the concept of so-called well-known symbols. These are special built-in characters that represent the internal mechanisms of the language that are not available to developers in implementations of ECMAScript 5 and previous versions of the standard.

Here are some examples:
')

Most of these new constructions have a complex and nontrivial effect on various parts of the language. This leads to significant changes in the performance profile due to the so-called monkey patches . We are talking about the possibility of changing some standard mechanisms by user code during program execution.

One of the most interesting examples of this is the new Symbol.toStringTag symbol, which is used to control the behavior of the built-in Object.prototype.toString () method. For example, a developer can now place a specific property in any object instance, after which this property will be used instead of the standard embedded tag when calling the toString method:

 class A { get [Symbol.toStringTag]() { return 'A'; } } Object.prototype.toString.call('');     // "[object String]" Object.prototype.toString.call({});     // "[object Object]" Object.prototype.toString.call(new A);  // "[object A]" 

This requires that the implementation of Object.prototype.toString() for ES2015 and later versions of the standard first converts its this value to an object using the abstract operation ToObject , and then searches for Symbol.toStringTag in the resulting object and in its prototype chain. This is what can be found in the relevant part of the language specification:


Fragment of the specification dedicated to Object.prototype.toString ()

Here you can see, firstly, the conversion using ToObject , and secondly, the Get call for @@toStringTag (this is a special internal syntax of the language specification for a well-known symbol called toStringTag ). Adding the Symbol.toStringTag construction to ES2015 significantly extends the capabilities of developers, but at the same time means a certain amount of resources.

ToString performance research goal


The performance of the Object.prototype.toString() method in Chrome and Node.js has already been investigated , as this method is extensively used for type checking with some popular frameworks and libraries. For example , the AngularJS framework uses this method to implement various support functions, including angular.isDate , angular.isArrayBuffer , and angular.isRegExp . For example:

 /** * @ngdoc function * @name angular.isDate * @module ng * @kind function * * @description * Determines if a value is a date. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ function isDate(value) { return toString.call(value) === '[object Date]'; } 

In addition, popular libraries such as lodash and underscore.js use Object.prototype.toString() to implement value checks. So, for example, the predicates _.isPlainObject and _.isDate from lodash are arranged:

 /** * Checks if `value` is classified as a `Date` object. * * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a date object, else `false`. * @example * * isDate(new Date) * // => true * * isDate('Mon April 23 2012') * // => false */ function isDate(value) { return isObjectLike(value) && baseGetTag(value) == '[object Date]' } 

Mozilla engineers working on the SpiderMonkey JavaScript engine found that the Symbol.toStringTag search Symbol.toStringTag in Object.prototype.toString() is a bottleneck in real-world application performance. This conclusion was made during the study of the benchmark Speedometer . By running only the AngularJS subtest from Speedometer using the V8 internal profiler (in order to enable it, you need to pass the command line key --no-sandbox --js-flags=--prof when you start Chrome), we found that most of the time is spent on executing the @@toStringTag search (in GetPropertyStub ) and on executing the ObjectProtoToString code, which implements the built-in Object.prototype.toString() method:


Profiling AngularJS subtest from Speedometer benchmark

Jan de Moyz from the SpiderMonkey development team created a simple microbenchmark to check the Object.prototype.toString() performance in arrays:

 function f() {   var res = "";   var a = [1, 2, 3];   var toString = Object.prototype.toString;   var t = new Date;   for (var i = 0; i < 5000000; i++) res = toString.call(a);   print(new Date - t);   return res; } f(); 

In fact, the implementation of this micro-benchmark using the internal profiler built into V8 (you can enable it in the d8 shell using the --prof command line --prof ) has already shown the essence of the problem. The main resources are spent on searching Symbol.toStringTag in the array [1, 2, 3] . Approximately 73% of the total execution time is spent on a non-resultant property search (in the GetPropertyStub function, which implements a universal property search), another 3% is spent in the built-in function ToObject , which, in the case of arrays, is an empty operation (arrays, from the point of view JavaScript are already objects).


Study of microbenchmark developed in Mozilla using a profiler (before optimization)

Interesting characters


For SpiderMonkey, a solution to the above problem was proposed, which consists of adding a so-called interesting symbol to the objects. This symbol is a property of any hidden class , telling whether objects with this hidden class can have a property with the name @@toStringTag or @@toPrimitive . Thanks to this approach, the resource-intensive Symbol.toStringTag search can, in general, be avoided, since this search does not produce results anyway. The implementation of this proposal led to an approximately twofold increase in the microbench performance with an array for SpiderMonkey.

As I explored some of the uses for AngularJS , I found it very lucky to find this idea, and decided to try to implement it in V8. I began to reflect on the architecture of the solution, and, as a result, ported it to V8, while limiting Symbol.toStringTag to only Symbol.toStringTag and Object.prototype.toString() . The fact is that I did not find (have not yet found) evidence that Symbol.toPrimitive is an important source of trouble in Chrome or Node.js. The basic idea here is that, by default, we suppose that instances of objects do not have interesting characters , and each time we add a new property to an instance, we check whether the name of this property is a similar symbol. If so, we set a specific bit in the hidden object instance classes.

 const obj = {}; Object.prototype.toString.call(obj);  //     obj[Symbol.toStringTag] = 'a'; Object.prototype.toString.call(obj);  //     

Take a look at this simple example. Here the object obj begins its existence without possessing an interesting symbol . Therefore, the Object.prototype.toString() call follows a new, fast execution path when you can skip the Symbol.toStringTag search (this is also because the Object.prototype also does not have an interesting symbol ). The second call performs the usual slow search operation, since obj now has an interesting character .

Optimization results


The implementation of this mechanism in V8 improved the performance of the above microbenchmark by about 5.8 times. Tests were conducted under Linux, on the HP Z620 Workstation. Checking performance using the profiler, we can see that the program is no longer wasting time in GetPropertyStub . Instead, the built-in method Object.prototype.toString() creates, as expected, the main load on the system.


A study of the microbench developed in Mozilla using a profiler (after optimization)

We also tested the optimized engine using a benchmark , which is a little closer to reality. When measuring the performance of Object.prototype.toString() , different values ​​are transferred, including primitives and objects that have the specially set Symbol.toStringTag property. As a result, the latest version of V8 was 6.5 times faster than V8 6.1.


The result of the new microbenchmark on different versions of V8

Measuring the impact of optimization on the Speedometer browser benchmark, and, in particular, on the AngularJS subtest, showed an increase in speed in all tests by 1% and a convincing growth of 3% when performing the AngularJS subtest.


Impact of optimization on the benchmark Speedometer

Results


Even highly optimized built-in JavaScript functions, such as Object.prototype.toString() , still have the potential for further optimization. In particular, the optimization described above can improve performance up to 6.5 times. You can make sure of this if you go deep enough into the results of various performance tests, such as the AngularJS subtest from the Speedometer benchmark.

I would like to thank Jan de Moyce and Tom Schuster for their research and for the great idea with interesting characters .

It should be noted that JavaScriptCore , the JavaScript engine used by WebKit , caches the results of consecutive Object.prototype.toString() calls to the hidden object instance class (this cache appeared at the beginning of 2012, before the ES2015 specification was released). This is a very interesting strategy, but its scope is limited (that is, it is useless when applied to other well-known symbols such as Symbol.toPrimitive or Symbol.hasInstance ). In addition, it requires a very complex logic of cache invalidation to ensure a timely response to changes in the prototype chain. That is why, at least for the moment, I made the choice not to favor the solution for the V8 based on the cache.

Dear readers! Do you profile your JavaScript applications? In your opinion, which standard JS mechanisms implemented in V8 need to be optimized?

Source: https://habr.com/ru/post/336306/


All Articles