Search code examples
javascriptobjectv8

Javascript compare object size in memory


I have about a million rows in javascript and I need to store an object for metadata for each of the rows. Given the following two different object types:

{0: {'e', 0, 'v': 'This is a value'}

And:

{0: '0This is a value'}

What would be the difference in memory between a million objects of the first type and a million objects of the second type? That is:

[obj1, obj1, obj1, ...] // array of 1M
[obj2, obj2, obj2, ...] // array of 1M

Solution

  • V8 developer here. The answer is still "it depends", because engines for a dynamic language tend to adapt to what you're doing, so a tiny testcase is very likely not representative of the behavior of a real application. One high-level rule of thumb that will always hold true: a single string takes less memory than an object wrapping that string. How much less? Depends.

    That said, I can give a specific answer for your specific example. For the following code:

    const kCount = 1000000;
    let a = new Array(kCount);
    for (let i = 0; i < kCount; i++) {
      // Version 1 (comment out the one or the other):
      a[i] = {0: {'e': 0, 'v': 'This is a value'}};
      // Version 2:
      a[i] = {0: '0This is a value'};
    }
    gc();
    

    running with --expose-gc --trace-gc, I'm seeing:

    Version 1: 244.5 MB

    Version 2: 206.4 MB

    (Nearly current V8, x64, d8 shell. This is what @paulsm4 suggested you could do in DevTools yourself.)

    The breakdown is as follows:

    • the array itself will need 8 bytes per entry
    • an object created from an object literal has a header of 3 pointers and preallocated space for 4 named properties (unused here), total 7 * 8 = 56 bytes
    • its backing store for indexed properties allocates space for 17 entries even though only one will be used, plus header that's 19 pointers = 152 bytes
    • in version 1 we have an inner object that detects that two (and only two) named properties are needed, so it gets trimmed to a size of 5 (3 header, 2 for "e" and "v") pointers = 40 bytes
    • in version 2 there's no inner object, just a pointer to a string
    • the string literals are deduplicated, and 0 is stored as a "Smi" directly in the pointer, so neither of these needs extra space.

    Summing up:

    Version 1: 8+56+152+40 = 256 bytes per object

    Version 2: 8+56+152 = 216 bytes per object

    However, things will change dramatically if not all strings are the same, if the objects have more or fewer named or indexed properties, if they come from constructors rather than literals, if they grow or shrink over the course of their lifetimes, and a bunch of other factors. Frankly, I don't think any particularly useful insight can be gleaned from these numbers (specifically, while they might seem quite inefficient, they're unlikely to occur in practice in this way -- I bet you're not actually storing so many zeros, and wrapping the actual data into a single-property {0: ...} object doesn't look realistic either).

    Let's see! If I drop all the obviously-redundant information from the small test, and at the same time force creation of a fresh, unique string for every entry, I'll be left with this loop to fill the array:

    for (let i = 0; i < kCount; i++) {
      a[i] = i.toString();
    }
    

    which consumes only ~31 MB total. Prefer an actual object for the metadata?

    function Metadata(e, v) {
      this.e = e;
      this.v = v;
    }
    for (let i = 0; i < kCount; i++) {
      a[i] = new Metadata(i, i.toString());
    }
    

    Now we're at ~69 MB. As you can see: dramatic changes ;-)

    So to determine the memory requirements of your actual, complete app, and any implementation alternatives for it, you'll have to measure things yourself.