Unexpected numbers in the heap

83 views
Skip to first unread message

Andrei Kashcha

unread,
Apr 19, 2013, 10:42:11 PM4/19/13
to v8-u...@googlegroups.com
I've been puzzling over V8 GC behavior for the past few days. I made a really simple program which copies content of one array into another (https://gist.github.com/anvaka/5423226 ), and I'm seeing lots of garbage collection in the d8 console with --trace_gc flag. But, with very simple tweaks garbage is not produced:

This does not happen when arrays contain integer numbers
This does not happen when any of the array's elements is printed to the console before or after the copy cycles
This does not happen when the copy is executed within a function scope.

Looking at the GC histograms "garbage" is comprised of nothing but HEAP_NUMBER_TYPE. When debugging the v8 source code I indeed see tones of calls to Runtime_AllocateHeapNumber in the runtime.cc file, but unfortunately my assembly programming skills do not allow me to fully understand what's going on here. 

Can someone shed some light on this behavior?

Toon Verwaest

unread,
Apr 20, 2013, 4:13:00 AM4/20/13
to v8-u...@googlegroups.com
Hi Andrei,

V8 has an optimization that turns arrays into unboxed double arrays. If you build a debug build of V8 you could do %DebugPrint(arr1) for example, and you'll see that the elements kind is set to FAST_DOUBLE_ELEMENTS (or FAST_HOLEY_DOUBLE_ELEMENTS).

This means that doubles are stored unboxed in the array, i.e., there's no heap-number wrapping around them. When you read such a number in optimized code, it goes straight to a double-register. When you write it back to a double array, it gets written directly inline into the array. This is a lot faster than having to allocate heap-numbers and wrap/unwrap them for computation.

There's a catch however: fullcodegen, our slow compiler used to gather typefeedback, does not support reading raw doubles. It can only work with heap numbers. This means that when you read a double from a raw-double array in fullcodegen, for example to copy it, you have to allocate a heap-number and wrap the double in it. When you write this heap-number to another double-array, the value gets taken out and written into the next array. Thus the temporary heap-number is now garbage.

So if you'd do trace-opt, hopefully you wouldn't see GC anymore after the point where your copying function is optimized.

hth,
Toon


--
--
v8-users mailing list
v8-u...@googlegroups.com
http://groups.google.com/group/v8-users
---
You received this message because you are subscribed to the Google Groups "v8-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to v8-users+u...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Jakob Kummerow

unread,
Apr 21, 2013, 1:26:53 PM4/21/13
to v8-u...@googlegroups.com
On Sat, Apr 20, 2013 at 10:13 AM, Toon Verwaest <verw...@chromium.org> wrote:
Hi Andrei,

V8 has an optimization that turns arrays into unboxed double arrays. If you build a debug build of V8 you could do %DebugPrint(arr1) for example, and you'll see that the elements kind is set to FAST_DOUBLE_ELEMENTS (or FAST_HOLEY_DOUBLE_ELEMENTS).

This means that doubles are stored unboxed in the array, i.e., there's no heap-number wrapping around them. When you read such a number in optimized code, it goes straight to a double-register. When you write it back to a double array, it gets written directly inline into the array. This is a lot faster than having to allocate heap-numbers and wrap/unwrap them for computation.

There's a catch however: fullcodegen, our slow compiler used to gather typefeedback, does not support reading raw doubles. It can only work with heap numbers. This means that when you read a double from a raw-double array in fullcodegen, for example to copy it, you have to allocate a heap-number and wrap the double in it. When you write this heap-number to another double-array, the value gets taken out and written into the next array. Thus the temporary heap-number is now garbage.

So if you'd do trace-opt, hopefully you wouldn't see GC anymore after the point where your copying function is optimized.

Well, you'd be surprised then :-)

All of the cases where no excessive amounts of garbage are produced are working as expected.

The interesting case is where lots of heap numbers are allocated even though optimized code is loading from and storing into arrays with double elements. This seems to be an artifact of how the return value of top-level code is computed. Essentially, the value that's being copied may become the top level's return value, which must always be boxed in a heap number, so in every iteration a heap number is allocated and filled with the value that was just copied, just in case it needs to be returned eventually. 
I'm not sure whether this is a bug or needs to be this way; but either way since array copying will typically be implemented in a function rather than in the top level, this is probably not worth spending much time on.

Also, please note that the code in that gist (https://gist.github.com/anvaka/5423226) does not copy arrays. Instead, it's a highly inefficient mechanism to set every element in arr1 to the value of the last element in arr2. Before using this to actually copy any arrays, you'll want to remove one of the loops.

Andrei Kashcha

unread,
Apr 21, 2013, 5:53:53 PM4/21/13
to v8-u...@googlegroups.com
Jakob, Toon, thank you very much for the insightful answers! From both of you I learned something new about V8. 

The return value of top level code makes total sense to me. The GC behavior is replicated even when I replace assignment in the inner cycle to simple read expression. E.g.

for (var i = 0; i < length; ++i) {
for (var j = 0; j < length; ++j) {
arr2[j]; // You'll see tones of GCs here.
}
}

I assume GC does not happen for the integer type, because the integer return value does not need to be boxed?

And yes, Jakob, you are right. By no means the code in the gist was supposed to actually copy arrays. I used it exclusively to explore this behavior of GC.

Cheers,
Andrei

Jakob Kummerow

unread,
Apr 22, 2013, 3:26:03 AM4/22/13
to v8-u...@googlegroups.com
On Sun, Apr 21, 2013 at 11:53 PM, Andrei Kashcha <anv...@gmail.com> wrote:
Jakob, Toon, thank you very much for the insightful answers! From both of you I learned something new about V8. 

The return value of top level code makes total sense to me. The GC behavior is replicated even when I replace assignment in the inner cycle to simple read expression. E.g.


for (var i = 0; i < length; ++i) {
for (var j = 0; j < length; ++j) {
arr2[j]; // You'll see tones of GCs here.
}
}
So for the record, the lesson learned here is: don't do heavy computations in top level code (which (1) is a good idea for several reasons, and (2) is unlikely to happen in real code anyway).
 
I assume GC does not happen for the integer type, because the integer return value does not need to be boxed?

Right. "Small" integers (up to 31 bits including the sign bit) can be "Smi-tagged" (using the 32nd bit) and stored directly in objects. Since the Smi-tag conveys the information that it's a Smi, no boxing is necessary.
Reply all
Reply to author
Forward
0 new messages