Summary
A single Object.setPrototypeOf(someArray, {...}) anywhere in a program made the whole program 33× slower on the fixture where I hit it — 1.51 G → 50.0 G retired instructions — because it permanently moves every array element store onto the full write barrier.
Found incidentally while measuring something else, so treat the framing carefully: I am not claiming this is a bug. invalidate_array_index_fast_path is presumably doing something the collector needs. But the cost is a cliff rather than a slope, it is triggered by one line of ordinary JavaScript, and nothing warns. That seems worth a deliberate decision rather than an accident.
Measurement
Four fixtures differing by three lines, all compiled against the identical runtime archive (this matters — an earlier same-shaped A/B of mine was invalid precisely because the arms were built at different times):
| program |
retired instructions |
| baseline |
1.51 G |
+ Object.setPrototypeOf(anArray, { … }) |
50.0 G |
Attribution of the +48.5 G:
| symbol |
delta |
mark_dirty_old_page_uncached |
+29.6 G |
| hashbrown insert (the remembered set) |
+13.6 G |
remember_old_to_young_slot |
+3.6 G |
That is the old→young write barrier taken on every element store, where the index fast path used to serve them.
For contrast, on the same fixture family these do not trigger it: class MyErr extends Error with a live instance, and live subclass instances of Array, Map, Set and Error. Only the explicit setPrototypeOf on a non-GC_TYPE_OBJECT owner does.
Why it may matter beyond a microbenchmark
Object.setPrototypeOf on an array is legal, occasionally used by real libraries (subclass emulation, branding, Array.prototype extensions in older code), and there is no diagnostic. A program that does it once at startup pays for it on every subsequent element store, for the life of the process.
Two questions someone who owns this path should answer, and I have not:
- Is the invalidation required to be global and permanent, or could it be scoped to the array whose prototype changed, or to the shape/class that acquired a non-default prototype? The measurement says every element store pays, not only stores to that array.
- Is the full barrier the necessary consequence, or only the currently-implemented one?
Context
Measured on perrymaster, base c8cf45056, with the same instrumented-build discipline as #10362's census: node-identical output verified on every arm, and counts reconciled in-process against PERRY_GC_TRACE.
Related but distinct: #10493 is about correctness of a custom prototype on a non-object owner across a GC move (fixed in #10552). This issue is about the performance consequence of setting one at all.
Summary
A single
Object.setPrototypeOf(someArray, {...})anywhere in a program made the whole program 33× slower on the fixture where I hit it — 1.51 G → 50.0 G retired instructions — because it permanently moves every array element store onto the full write barrier.Found incidentally while measuring something else, so treat the framing carefully: I am not claiming this is a bug.
invalidate_array_index_fast_pathis presumably doing something the collector needs. But the cost is a cliff rather than a slope, it is triggered by one line of ordinary JavaScript, and nothing warns. That seems worth a deliberate decision rather than an accident.Measurement
Four fixtures differing by three lines, all compiled against the identical runtime archive (this matters — an earlier same-shaped A/B of mine was invalid precisely because the arms were built at different times):
Object.setPrototypeOf(anArray, { … })Attribution of the +48.5 G:
mark_dirty_old_page_uncachedremember_old_to_young_slotThat is the old→young write barrier taken on every element store, where the index fast path used to serve them.
For contrast, on the same fixture family these do not trigger it:
class MyErr extends Errorwith a live instance, and live subclass instances ofArray,Map,SetandError. Only the explicitsetPrototypeOfon a non-GC_TYPE_OBJECTowner does.Why it may matter beyond a microbenchmark
Object.setPrototypeOfon an array is legal, occasionally used by real libraries (subclass emulation, branding,Array.prototypeextensions in older code), and there is no diagnostic. A program that does it once at startup pays for it on every subsequent element store, for the life of the process.Two questions someone who owns this path should answer, and I have not:
Context
Measured on perrymaster, base
c8cf45056, with the same instrumented-build discipline as #10362's census: node-identical output verified on every arm, and counts reconciled in-process againstPERRY_GC_TRACE.Related but distinct: #10493 is about correctness of a custom prototype on a non-object owner across a GC move (fixed in #10552). This issue is about the performance consequence of setting one at all.