HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMajor

Should I put default values of attributes on the prototype to save space?

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
thespaceputattributesdefaultsaveprototypeshouldvalues

Problem

Suppose I've got a JavaScript function that I'm treating like a class - that is, I want to make many instances of it:

function Blerg() {
  this._a = 5;
}

Blerg.prototype.getA = function() {
  return this._a;
}

Blerg.prototype.setA = function(val) {
  this._a = val;
}


This class has one attribute, a, which the constructor instantiates to a default value of 5. It is accessed with the getter and setter.

Now suppose I have such a class but it has 30+ attributes (a, b, c, etc), and suppose like a that these all have unique default values, but also that it is uncommon for them to be changed from the default.

Suppose also that I need to make 10,000 or more Blerg instances, and so a design goal is to save space.

I'm wondering if it is a good idea to put all of the default values for my 30+ attributes on the prototype of my class instead. That way, when I create 10,000+ instances of my class, none of them have a, b, c, etc attributes, but calling getA() will still return the correct default value.

So I present this modified Blerg function, Blerg2:

function Blerg2() {
  // nothing!
}

Blerg2.prototype._a = 5;

Blerg2.prototype.getA = function() {
  return this._a;
}

Blerg2.prototype.setA = function(val) {
  this._a = val;
}


Are there downsides to taking this approach?

Some notes

The prototype way seems faster to create, see: http://jsperf.com/blergs

For that matter, the worst-case scenario does not look that bad: http://jsperf.com/blergs/2

And creating many of them using the code in the jsperf test (in its own html page) and adding:

var a = [];
for (var i = 0; i < 400000; i++) {
  a.push(new Blerg()); // or Blerg2
}


Suggests that the heap size for the objects in question is cut in half by using Blerg2.

Solution

related jsperf http://jsperf.com/12312412354

Well this is a really bad idea. Objects are always considered having different class if they don't have exactly the same set of properties in the same order. So
a function that accepts these objects will in best case be polymorphic and in worst case megamorphic all the while you are thinking
you are passing it same class of objects. This is fundamental to all JS engines although the specifics that follow focus on V8.

Consider:

function monomorphic( a ) {
    return a.prop + a.prop;
}

var obj = {prop: 3};
while( true ) {
    monomorphic( obj );
}


Now, since the passed object a always has the same class, we will get really good code:

; load from stack to eax
15633697    23  8b4508         mov eax,[ebp+0x8] 
    ;; test that `a` is an object and not a small integer
1563369A    26  f7c001000000   test eax,0x1 
156336A0    32  0f8485000000   jz 171  (1563372B) ;;deoptimize if it is not
    ;; test that `a`'s class is as expected
156336A6    38  8178ffb9f7902f cmp [eax+0xff],0x2f90f7b9
156336AD    45  0f857d000000   jnz 176  (15633730) ;;deoptimize if it is not
    ;; load a.prop into ecx, as you can see it's like doing struct->field in C
156336B3    51  8b480b         mov ecx,[eax+0xb]
    ;; this will untag the tagged pointer so that integer arithmetic can be done to it
156336B6    54  d1f9           sar ecx,1
    ;; perform a.prop + a.prop
    ;; note that if it was a.prop + a.prop2
    ;; then it wouldn't need to do all those checks again
    ;; so for one time check inside the function we can load all the properties
    ;; quickly
156336B8    56  03c9           add ecx,ecx


Notice what happened here, V8 saw that we always pass the same class of object to the function
monomoprhic and generated really tight code that assumes we will always get that class of object
in the future as well.

Now let's do:

function polymorphic( a ) {
    return a.prop + a.prop;
}

var obj = {prop: 3};
var obj2 = {prop: 3, prop2: 4};
while( true ) {
    polymorphic( Math.random() < 0.5 ? obj : obj2 );
}


Now the function must consider 2 different classes of objects. The classes are different but similar enough
that the client code can stay as it is as both classes contain a field prop.

Let's see:

; load from stack to eax
04C33E17    23  8b4508         mov eax,[ebp+0x8]
    ;; test that `a` is an object and not a small integer
04C33E1A    26  f7c001000000   test eax,0x1
04C33E20    32  0f8492000000   jz 184  (04C33EB8) ;; deoptimize if not
    ;; test that `a`'s class is one of the expected classes
04C33E26    38  8178ffb9f7401c cmp [eax+0xff],0x1c40f7b9
04C33E2D    45  0f840d000000   jz 64  (04C33E40) ;; if it is, skip the second check and go to the addition code
    ;; otherwise check that `a`'s class is the second one of the expected classes
04C33E33    51  8178ff31f8401c cmp [eax+0xff],0x1c40f831
04C33E3A    58  0f857d000000   jnz 189  (04C33EBD) ;; deoptimize if not
    ;; load a.prop into ecx
    ;; if you are still reading this you will probably notice that this
    ;; is actually relying on the fact that both classes declared the prop field
    ;; first
04C33E40    64  8b480b         mov ecx,[eax+0xb]
    ;; this will untag the tagged pointer so that integer arithmetic can be done to it
04C33E43    67  d1f9           sar ecx,1
    ;; do the addition
04C33E45    69  03c9           add ecx,ecx


Ok so the situation is still pretty good but here we are relying on the fact that properties are in same order
and that there are only 2 different classes.

Let's do the same in different order so that V8 can't use the same instruction (mov ecx,[eax+0xb]) for both objects:

function polymorphic( a ) {
    return a.prop + a.prop;
}

var obj = {prop: 3};
var obj2 = {prop2: 4, prop: 3};
while( true ) {
    polymorphic( Math.random() < 0.5 ? obj : obj2 );
}


And:

06C33E84    36  8b4508         mov eax,[ebp+0x8]
    ;; small integer check
06C33E87    39  f7c001000000   test eax,0x1
06C33E8D    45  0f84d3000000   jz 262  (06C33F66)
    ;; class check 1
06C33E93    51  8178ffb9f75037 cmp [eax+0xff],0x3750f7b9
06C33E9A    58  7505           jnz 65  (06C33EA1)
06C33E9C    60  8b480b         mov ecx,[eax+0xb]
06C33E9F    63  eb10           jmp 81  (06C33EB1)
    ;; class check 2
06C33EA1    65  8178ff31f85037 cmp [eax+0xff],0x3750f831
06C33EA8    72  0f85bd000000   jnz 267  (06C33F6B)
06C33EAE    78  8b480f         mov ecx,[eax+0xf]

06C33EB1    81  f6c101         test_b cl,0x1
06C33EB4    84  0f851e000000   jnz 120  (06C33ED8)
06C33EBA    90  d1f9           sar ecx,1
06C33EBC    92  89ca           mov edx,ecx
06C33EBE    94  03d1           add edx,ecx


Ok just as expected, just using different offsets depending on the class.

So you can see where this is going, if you end up with 10 different classes then you will
just get 30 instructions (instead of 3) whenever a function will need to lookup a property. Which is still
much better than a

Code Snippets

function monomorphic( a ) {
    return a.prop + a.prop;
}

var obj = {prop: 3};
while( true ) {
    monomorphic( obj );
}
; load from stack to eax
15633697    23  8b4508         mov eax,[ebp+0x8] 
    ;; test that `a` is an object and not a small integer
1563369A    26  f7c001000000   test eax,0x1 
156336A0    32  0f8485000000   jz 171  (1563372B) ;;deoptimize if it is not
    ;; test that `a`'s class is as expected
156336A6    38  8178ffb9f7902f cmp [eax+0xff],0x2f90f7b9
156336AD    45  0f857d000000   jnz 176  (15633730) ;;deoptimize if it is not
    ;; load a.prop into ecx, as you can see it's like doing struct->field in C
156336B3    51  8b480b         mov ecx,[eax+0xb]
    ;; this will untag the tagged pointer so that integer arithmetic can be done to it
156336B6    54  d1f9           sar ecx,1
    ;; perform a.prop + a.prop
    ;; note that if it was a.prop + a.prop2
    ;; then it wouldn't need to do all those checks again
    ;; so for one time check inside the function we can load all the properties
    ;; quickly
156336B8    56  03c9           add ecx,ecx
function polymorphic( a ) {
    return a.prop + a.prop;
}

var obj = {prop: 3};
var obj2 = {prop: 3, prop2: 4};
while( true ) {
    polymorphic( Math.random() < 0.5 ? obj : obj2 );
}
; load from stack to eax
04C33E17    23  8b4508         mov eax,[ebp+0x8]
    ;; test that `a` is an object and not a small integer
04C33E1A    26  f7c001000000   test eax,0x1
04C33E20    32  0f8492000000   jz 184  (04C33EB8) ;; deoptimize if not
    ;; test that `a`'s class is one of the expected classes
04C33E26    38  8178ffb9f7401c cmp [eax+0xff],0x1c40f7b9
04C33E2D    45  0f840d000000   jz 64  (04C33E40) ;; if it is, skip the second check and go to the addition code
    ;; otherwise check that `a`'s class is the second one of the expected classes
04C33E33    51  8178ff31f8401c cmp [eax+0xff],0x1c40f831
04C33E3A    58  0f857d000000   jnz 189  (04C33EBD) ;; deoptimize if not
    ;; load a.prop into ecx
    ;; if you are still reading this you will probably notice that this
    ;; is actually relying on the fact that both classes declared the prop field
    ;; first
04C33E40    64  8b480b         mov ecx,[eax+0xb]
    ;; this will untag the tagged pointer so that integer arithmetic can be done to it
04C33E43    67  d1f9           sar ecx,1
    ;; do the addition
04C33E45    69  03c9           add ecx,ecx
function polymorphic( a ) {
    return a.prop + a.prop;
}

var obj = {prop: 3};
var obj2 = {prop2: 4, prop: 3};
while( true ) {
    polymorphic( Math.random() < 0.5 ? obj : obj2 );
}

Context

StackExchange Code Review Q#28344, answer score: 24

Revisions (0)

No revisions yet.