Skip to content

perf: one Object.setPrototypeOf on an array puts every element store on the full write barrier — 33x whole-program on a fixture #10593

Description

@proggeramlug

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions