Search code examples
javascriptv8

JavaScript: V8 question: are small integers reused?


According to this article from V8's blog and this existing question, we know that small integers are encoded into the pointers directly with pointer tagging.

The "trick" of Smis is that they're not stored as separate objects: when you have an object that refers to a Smi, such as let foo = {smi: 42}, then the value 42 can be smi-encoded and stored directly inside the "foo" object (whereas if the value was 42.5, then the object would store a pointer to a separate "HeapNumber"). But since the object is on the heap, so is the Smi.

This means that if I have two different objects with the same object values {smi: 42}. The smi 42 should be located in two different memory location on heap since the two objects are on heap and the value 42 is encoded directly into the pointer, instead of having an additional storage.

But this contradicts with the Chrome devtool's memory profiling result. Given this snippet

<body>
    <button id='btn'>btn</button>
    <script>  
    const btn = document.querySelector('#btn')
    function MyObject() {
        this.number = 3.14
        this.smi = 123
        this.undefined = undefined
        this.true = true
        this.false = false
        this.null = null
        this.string = 'foo'
    }
    let obj1
    let obj2
    btn.onclick = () => { 
        obj1 = new MyObject()
        obj2 = new MyObject()
    }
    </script>
</body>

I thought smi:123 should be located in two different locations while double number = 3.14 should point to the same number object.

But this is not what happened - smi are in the same memory location and double are not. enter image description here


Solution

  • An additional comment to @JonasWilms' great answer:

    If you actually care about what happens under the hood, I suggest you learn how to use a (native, not DevTools) debugger and inspect the actual memory. DevTools are designed to help you understand what your app is doing, and while that has a lot of overlap with surfacing what's happening under the hood, it's not exactly the same thing, and there may well be certain details (like how Smis are presented) where this difference shows. I think heap snapshots use a raw_address → object ID map, and what you see is the natural result of putting Smis into such a map.

    For a simplified version of your test case (just function MyObject() { this.number = 12.5; this.smi = 23; }), this is what you'd see in memory:

    (gdb) x/5xw 0x1f010810aedc
    0x1f010810aedc: 0x082c7db1  // pointer to map
                    0x08002249  // pointer to properties (empty array)
                    0x08002249  // pointer to elements (empty array)
                    0x0810af71  // pointer to a HeapNumber
                    0x0000002e  // Smi: 23 << 1 == 46 == 0x2e
    (gdb) x/5xw 0x1f010810afa4
    0x1f010810afa4: 0x082c7db1  // map (same as other object)
                    0x08002249  // properties
                    0x08002249  // elements
                    0x0810afd9  // pointer to a HeapNumber
                    0x0000002e  // Smi: 23 << 1
    

    On another note, HeapNumbers can and do get reused. We don't see that in this example because of yet another mechanism: object properties use mutable HeapNumbers. Being mutable makes them unshareable, and in this toy example the whole approach just wastes time and memory; but it turns out to be a beneficial tradeoff for typical usage patterns, because it allows updating the value (in particular, from optimized code) without allocating a new HeapNumber every time.
    If you simply had a function f() { return 12.5; }, it would actually return the same reused HeapNumber every time it's called.