12

Given

var obj = {};

var _a = 1;

obj._a = 1;

obj.aGetter = function() {
  return _a;
}

obj.aSetter = function(val) {
  _a = val;
}

Object.defineProperty(obj, 'a', {
  enumerable: true,
  get: function () {
    return _a;  
  },
  set: function(val) {
    _a = val;
  }     
});

using getter/setter functions

obj.aSetter(2);
obj.aGetter();

will have some decrease in Chrome/V8 performance (~3x) when compared to direct property access:

obj._a = 2;
obj._a;

This is be understandable. And using descriptor getter/setter

obj.a = 2;
obj.a;

will cause ~30x decrease in Chrome (41 to latest) performance - almost as slow as Proxy. While Firefox and older Chrome versions use descriptor getter/setter with no significant performance penalty.

What is the exact problem with descriptor getter/setter performance in recent Chrome/V8 versions? Is it a known issue that can be monitored?

The measurements were done with Benchmark.js (jsPerf engine). I'm unable to provide a link to jsPerf test to visualize the difference because jsPerf has been seriously screwed up with its anti-DDoS measures, but I'm sure there are existing ones that can prove a point.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 1
    How do the old Chrome versions compare to the new ones in the direct access - have they gotten faster, or accessor performance really decreased? – Bergi Mar 31 '16 at 16:33
  • Afaik, getters/setters are not optimised well in V8. – Bergi Mar 31 '16 at 16:34
  • @Bergi Descriptor accessors seem to be optimized quite well in GC <= 39 (object properties don't perform as good as in FF but anyway). But something changed in 41 (haven't got GC 40 to check it), that's the most ridiculous part. – Estus Flask Mar 31 '16 at 17:01
  • @estus please provide the whole benchmark. you don't have to use jsperf, you can use standalone benchmark.js. Depending on how benchmark is structured - explanation might be different. most probable cause is hidden class transition clash (you take objects with the same initial hidden class and assign different getters/setters under the same property name). To analyze the difference - we really need to see the whole code, as there are multiple ways to write this benchmark (some of them also incorrect). – Vyacheslav Egorov Apr 01 '16 at 14:51
  • @VyacheslavEgorov The whole code is in the question; the first piece of code is setup on the global scope, and the rest are actual iterated tests (checked them on local jsperf server). Again, it performs fine in earlier Chrome versions. – Estus Flask Apr 01 '16 at 15:08
  • 3
    @estus the commit that caused regression is https://codereview.chromium.org/714883003, it removed the code that was used to recover from transition clash. in general in V8 it is a good idea to put getters/setters on the prototype - not on to the immediate object to avoid this sort of situations. – Vyacheslav Egorov Apr 01 '16 at 21:20
  • 2
    @VyacheslavEgorov Good hint, the optimizations are indeed there when descriptor is defined on the prototype.Thanks for the research work, you can submit it as an answer if you wish to. – Estus Flask Apr 01 '16 at 22:27
  • +1 for submitting http://stackoverflow.com/questions/36338289/object-descriptor-getter-setter-performance-in-recent-chrome-v8-versions#comment60351462_36338289 as an answer — that's really good to know. – aendra Oct 10 '16 at 22:43

1 Answers1

15

The changes in performance are relevant to this Chromium issue (credits go to @VyacheslavEgorov).

To avoid performance issues, a prototype should be used instead. This is one of few reasons why singleton classes may be used to instantiate an object once.

With ES5:

var _a = 1;

function Obj() {}

Object.defineProperty(Obj.prototype, 'a', {
  enumerable: true,
  get: function () {
    return _a;  
  },
  set: function(val) {
    _a = val;
  }     
});

var obj = new Obj();
// or
var obj = Object.create(Obj.prototype);

Or with ES6 syntactic sugar:

class Obj {
  constructor() {
    this._a = 1;
  }

  get a() {
    return this._a;
  }

  set a(val) {
    this._a = val;
  }     
}

let obj = new Obj();
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Wow, it's a little unbelievable that this one simple step -- putting getters into prototype -- makes all the difference. – Anton Mar 03 '21 at 19:25
  • Did you do more research how slow prototype getters are compared to direct property access? Some OOP situations require a getter, because its overwritable, but it sucks when it slows things down performance-critical classes. – kungfooman Jul 18 '22 at 09:24
  • @kungfooman By doing a benchmark, check https://stackoverflow.com/questions/37695890/how-to-profile-javascript-now-that-jsperf-is-down . I didn't check this lately but expect this problem to be improved with time. Notice that the issue was only with getters in plain objects, the classes were ok. – Estus Flask Jul 18 '22 at 12:25