0

I don't understand why the heap size twice as big as it's supposed to be.

I created a perfect binary tree. I guess v8 knows that each node has 3 fields.

function buildTree(depth) {
  if (depth === 0) return null;

  return {
    value: 0,
    left: buildTree(depth - 1),
    right: buildTree(depth - 1),
  };
}

I suppose 1 number takes 8 bytes, 2 object references take 8 bytes each. Hence a tree node takes 24 bytes in total.

Then I run the code below:

const tree = buildTree(25);
// 2 ** 25 - 1 ≈ 33_500_000 nodes
// 33_500_000 nodes × 24 bytes = 840_000_000 bytes

const { heapUsed } = process.memoryUsage();
const expectedSize = (N * 24 / 1e6).toFixed(2) + " MB";
const actualSize = (heapUsed / 1e6).toFixed(2) + " MB";

console.table({ expectedSize, actualSize });

// ┌──────────────┬──────────────┐
// │ expectedSize │ '805.31 MB'  │
// │  actualSize  │ '1614.08 MB' │
// └──────────────┴──────────────┘

Then I try to run this code in Chrome. I just open devtools, run code in Console tab, then switch to Memory tab and see 807 MB as I expect.

How does v8 work? Why does it need 2 times more memory? What is the difference comparing to Chrome?

5
  • 1
    There's also any overhead of the objects... Commented Oct 18, 2023 at 0:20
  • 1
    JavaScript isn't like C, with clearly defined memory usage. It's really up to the interpreter to allocate memory. Note that builtin functions will also take up memory space, and any garbage collection references to the objects, and so on. Commented Oct 18, 2023 at 0:23
  • 1
    The question should be "how on Earth Chrome manages to handle objects without overhead" - with each of 3 properties needing some sort of "object prototype" reference for the tree and the number... So I can see 5 *8 = 40 bytes as absolute minimum... (maybe number allocated as Int32 could save it down to 36... but nowhere close to 24 OP is observing in Chrome) Commented Oct 18, 2023 at 0:28
  • @AlexeiLevenkov 24 is not what OP is actually observing, it's what they were naively expecting Commented Oct 18, 2023 at 0:31
  • @Bergi "...see 807 MB as I expect..." which is pretty close to 24 bytes per object (assuming they got all numbers right) Commented Oct 18, 2023 at 0:35

1 Answer 1

10

(V8 developer here. I've reopened the question because the key point, "why does this object tree need twice as much memory in Node as it does in Chrome?", hasn't been answered by the linked question.)

There are two parts to this.

Part 1: Object layout. Objects in virtual machines for languages like JavaScript have internal object headers. Skipping over the details here, the upshot is that in V8, each JS object has 3 pointers in its internal header. The specific objects your example is creating then need another 3 pointers for their properties (value, left, right), for a total of 6 pointers per object.

Part 2: Pointer compression. V8 supports "pointer compression" on 64-bit platforms, where it uses only 32 bits for each on-heap pointer (i.e. both the pointer and its target are residing on the garbage-collected heap). That technique has two obvious implications:
(1) it limits the addressable size of the garbage-collected heap to 2**32 == 4 GiB.
(2) it cuts memory consumption of pointers in half. Memory consumption of data, such as strings and numbers, is not affected; in practice that means that overall memory consumption of applications is typically reduced by around a third.

And now we can explain what's happening:
In Chrome, pointer compression is enabled, so each 6-field object needs 6*4 bytes, so your example needs 2**25 * 6*4 = 805 million bytes in total.
The Node.js team decided that supporting heaps larger than 4 GiB was more important than saving a third of the memory consumption, so when building Node they compile V8 with pointer compression turned off, so each 6-field object needs 6*8 bytes, so your example needs 2**25 * 6*8 = 1.6 billion bytes in total.


Side note with one extra detail: the numbers being stored in the objects in this example are actually always the same number 0, so V8 uses its pointer tagging technique to store it as a "fake pointer" of sorts. That's neither 64-bit IEEE format, nor are they separate objects; look for the term "smi tagging" if you want to learn more. If you modified the example to store value: Math.random() in the objects, you'd see memory consumption increase by 12 bytes per object in Chrome, or 16 bytes per object in Node.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for reopening, I did not spot the actual question but only looked at the expected-vs-actual console.table nodejs output
The 12/16 bytes in your last sentence, is that "1 pointer + 8 bytes" for float64 heap objects?
@Bergi correct, that's the size of a HeapNumber.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.