Web App Development

V8 memory quiz

V8 is the JavaScript engine that Chrome and Node use to run JavaScript code. Take this quiz to discover some interesting implementation details about how V8 stores values in memory.

Reading this probably won’t help you fix any performance issues you encounter day-to-day.

1. How much memory is used per array element? (Divide total program memory by array length)

var a = []
// You'll see why we're using magic array length later on.
// MAGIC_ARRAY_LENGTH is a number over one million, so
// total memory consumption is dominated by array elements
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) {
    a.push(Math.random())
}

JavaScript numbers are 64-bit floating point values. There are 8 bits per byte, so each number takes up 64/8=8 bytes.

2. How much memory is used per array element?

var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) {
    a.push(Math.random())
}
a.push("this is a string")

Here the JavaScript engine encounters a problem: the array countains two different types of data, numbers and strings.

We want to store a reference to a string in the array. A reference is just a memory location (where the actual characters of the string are). A memory location is just a number. You can think of you system memory as huge array, where the reference is an array index.

So a string reference is a number, and a number is also a number. How can we tell which is which?

The answer is called a "boxed value". We wrap each number in an object and store the object reference in the array. Now each array element can be a reference.

For each number in the array we now have to store two things:

  • reference to the object (8 bytes)
  • the boxed number object (16 bytes)

Why is the number object 16 bytes large? First, it needs to store the number value itself (that 64-bit floating point value). Each JavaScript object also has an internal type called a "hidden class", which is another reference value.

Why does it take 8 bytes to store a reference? Remember how system memory is like an array? If you have a 32 bit address you can represent array indices up to 2^32. If you store one byte at each array index you can then address up to 2^32/(1024*1024*1024)=4GB of memory. Since most computers today have more than 4GB of memory you need to go one step further to 64 bit addresses (8 bytes per address).
The above is more of a common sense explanation, I'm pretty sure there's more nuance to this.

3. How much memory is used per array element?

var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) {
    a.push({})
}

How much space should V8 allocate for an empty object? It's a tricky question. Presumably the object won't stay empty forever.

In short, this is what V8 stores:

  • hidden class reference (8 bytes)
  • 4 empty slots to store property values (32 bytes)
  • empty slot for a reference to an overflow object, in case you assign more than 4 properties (8 bytes)
  • empty slot for an object that stores values for numeric property indices(8 bytes)

I've previously written a longer explanation of V8 object sizes.

So the answer is: 56 bytes per object, plus the 8 byte reference to the object inside the array.

4. How much memory is used per array element?

var Obj = function(){}
var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) {
    a.push(new Obj())
}

Again we have empty objects, but this time we use a constructor to create them. V8 can observe what happens during execution and it learns that Obj objects don't have any members.

So we can skip the empty slots for new properties and just have three 8 byte properties:

  • hidden class reference
  • slot for storing extra properies
  • slot for numeric indices

5. How much memory is used per array element?

var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) {
    a.push("Hello")
}

Each array element needs to hold a 64-bit memory reference to the string value. V8 will only create one string instance with the value "Hello", so given we have a large array the string size doesn't matter.

6. How much memory is used per array element?

var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH; i++) {
    a.push(true)
}

true is stored as an object reference just like a string. So again all we need to store is a 64 bit memory location. false, undefined and null are treated the same way.

7. What is the total memory consumption of this program?

var a = []
for (var i=0; i<1024 * 1024; i++) {
    a.push(Math.random())
}

We're storing a bit over one million numbers, and each number takes 8 bytes. So we should expect an array length of 8MB.

But that's not the case! You can always add more elements to a JavaScript array, but V8 doesn't want to resize the array every time you add an element. So it leaves some extra empty space at the end of the array.

In the previous examples we've been using MAGIC_ARRAY_LENGTH, which is just at the threshold before the array is expanded. MAGIC_ARRAY_LENGTH is 1304209, while 1024 * 1024 is 1048576. But in both cases the amount of space used by the array is the same.

8. What is the total memory consumption of this program?

var a = new Array(1024 * 1024)
for (var i=0; i<1024 * 1024; i++) {
    a[i] = Math.random()
}

Here V8 knows how big the array is going to be in the end, so it can allocate just the right amount of space.

9. What is the total memory consumption of this program?

var a = new Int16Array(1024 * 1024)
for (var i=0; i<1024 * 1024; i++) {
    a[i] = 1
}

Our array contains 16-bit integers, which take 2 bytes each. A bit over one million of those means we need 2MB.

If you’re interested in playing around with this in Chrome DevTools, it can be useful to wrap the value you’re interested in in a Holder class that you can then search for.

function Holder() {}
var holder = new Holder()
var MAGIC_ARRAY_LENGTH = 1304209
var a = []
for (var i=0; i<MAGIC_ARRAY_LENGTH;i++) {
    a.push(null)
}
holder.a = a

I’ve been trying unsuccessfully to figure out how V8 strings work in memory. If you can figure that out I’d love to read a blog post about it!


Follow me on Twitter