Skip to content

perf: pack subscription edges to remove parallel slot arrays - #3358

Open
Priyansh4444 wants to merge 1 commit into
solidjs:mainfrom
Priyansh4444:perf/packed-subscription-edges
Open

Priyansh4444 wants to merge 1 commit into
solidjs:mainfrom
Priyansh4444:perf/packed-subscription-edges

Conversation

@Priyansh4444

Copy link
Copy Markdown
Contributor

What

Solid's signal graph stores each subscription edge as two parallel arrays on both sides:

  • signal: observers[i] = computation, observerSlots[i] = that computation's index in its sources array
  • computation: sources[j] = signal/memo, sourceSlots[j] = that computation's index in the signal's observers array

This packs each edge into one array ([other, slot, other, slot, ...]), removing one array and one property per edge side.

Why

Every computation subscription currently allocates or grows two arrays (sources and sourceSlots). On the repo's own packages/solid/bench/bench.cjs workload (1e5 signals and computations), interleaved A/B pairs show:

before after
total (median of 18 pairs) 392.5 ms 326 ms
paired median delta -61 ms (-17.0%)
pairs favoring the change 17 of 18
sign test (one-sided) p = 7.2e-5

The counterbalanced subset also favors the change when it runs first, so this is not arm order. Bundle: 53,614 to 53,447 bytes.

Correctness

  • pnpm --filter solid-js test: 487 tests pass, plus 4 in the server suite.
  • Differential identity on seeded mixed signal/memo/effect workloads.
  • Structural edge harness: duplicate read of the same signal, A-B-A reads, non-last observer removal, dependency swap, memo disposal while subscribed, 20-way fan-out with interleaved disposal, nested roots, post-dispose setters. All identical.
  • No public API change; the packed fields are internal.

Notes for reviewers

  • packages/solid/bench/bench.cjs in this tree aborts on V8 14.6 because %OptimizeFunctionOnNextCall now requires %PrepareFunctionForOptimization first. Adding that call before the optimize call is enough to run the benchmark. The harness change is not part of this PR.
  • Protocol, raw runs, and the independent verification are public here: https://github.com/Priyansh4444/perf-prove-it/tree/main/study/solid-agent
  • This change was produced by an AI agent following a documented optimization protocol and was independently reproduced before submission. Happy to adjust the representation or supply more evidence in review.

@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8696fd8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
solid-js Patch
test-integration Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Priyansh4444

Copy link
Copy Markdown
Contributor Author

As mentioned before, this was created by an agent, while trying to test a performance skill I made. What the skill did was simulate what the bytecode would look like, then ACTUALLY compile what the bytecode is and see what the difference would be.

I think this is a reasonable change considering, let's say this code is hot (i.e. the runtime has hit this section of the code multiple times and therefore compiles the code JIT). It would benefit for cache locality (ofc this depends on the Cache architechture and the amount of cache in the register) since things that live together are easier to fetch together.

This is also almost purely speculative and my assumptions are based on the actual benchmark for the speed improvement!

This does sacrifice readability for the tiny bit of performance. I would love to know if this PR is useful and if it is useless feel free to close it without guilt, this is mainly a learning experience for me too!

@Priyansh4444

Copy link
Copy Markdown
Contributor Author

Once again sorry for the review burden!

@Priyansh4444

Priyansh4444 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Update: bytecode and per-scenario regression check

Adding the machine-level evidence behind this representation change.

Bytecode (node --print-bytecode, Node 26 / V8 14.6, before/after bundles). readSignal: 458 to 372 bytes, constant pool 23 to 18.

BEFORE
@158 GetNamedProperty "length"; SubSmi [1]; GetKeyedProperty       ; observers[len - 1] !== Listener
@207 CreateArrayLiteral PACKED_SMI Fix[1]; ...; SetNamedProperty "sources"
@231 CreateArrayLiteral PACKED_SMI Fix[1]; ...; SetNamedProperty "sourceSlots"
@285 GetNamedProperty "sourceSlots"; GetNamedProperty "push"; CallProperty1
@326 CreateArrayLiteral Fix[1]; GetNamedProperty "sources"; GetNamedProperty "length"; SubSmi [1]
     ...; SetNamedProperty "observerSlots"
@395 GetNamedProperty "sources"; GetNamedProperty "length"; SubSmi [1]   ; slot re-derived after push

AFTER
@162 GetNamedProperty "length"; SubSmi [2]; GetKeyedProperty       ; observers[len - 2] !== Listener
@197 ShiftRightSmi [1]                                             ; sSlot = observers.length >> 1
@212 ShiftRightSmi [1]                                             ; cSlot = sources.length >> 1 (before mutation)
@227 CreateArrayLiteral PACKED_SMI Fix[2]; StaInArrayLiteral x2; SetNamedProperty "sources"
@266 CallProperty2                                                 ; one push(this, sSlot) instead of two calls
@278 CreateArrayLiteral PACKED_SMI Fix[2]; StaInArrayLiteral x2; SetNamedProperty "observers"
@321 CallProperty2                                                 ; one push(Listener, cSlot)

The last subscriber's computation sits at len - 2 in the packed layout, so the duplicate-read test is the same check; >> 1 converts element count to pair count, which is the old array length; the update loops (writeSignal, lookUpstream, markDownstream) stride by 2 and read only computations; cleanNode's swap-remove repairs both sides with index << 1 and (s << 1) + 1. The one cold-path cost: cleanNode grew 359 to 366 bytes.

Per-scenario check (repo benchmark; pooled medians from 11 interleaved pairs, plus focused 20 to 30 pair reruns for the two scenarios that looked positive when sessions were pooled):

scenario before after delta
createComputations1to1 20.4 ms 12.4 ms -39% (26/30 pairs)
createComputations1to4 13.9 ms 10.6 ms -24% (16/20 pairs)
createComputations1to2 21 18 -14%
createComputations1to8 19 15 -21%
createComputations1to1000 21 13 -38%
createComputations2to1 24 20 -17%
createComputations4to1 26 22 -15%
createDataSignals, createComputations0to1, createComputations1000to1 5, 5, 22 5, 5, 22 0%
updateComputations1to1 38 38 0%
updateComputations2to1 27 26 -4%
updateComputations4to1 23 22 -4%
updateComputations1000to1 46 43 -7%
updateComputations1to2 29 26 -10%
updateComputations1to4 26 25 -4%
updateComputations1to1000 25 23 -8%
total 393 370 -5.9% (pooled); -17% in the best 12-pair session

No credible regression. The two positive pooled entries were bimodal noise from 5 to 20 ms scenarios; focused sampling with more pairs shows both improve.

Correctness: repo tests 487 + 4 pass; an independent 200-seed differential over conditional dependencies, duplicate reads, disposal and cleanup, batch, untrack and memo equality produced byte-identical logs (5,235 logged computation runs). All numbers on Node 26 / V8 14.6.

The evidence above was produced by an AI agent following a documented optimization protocol, and the benchmark result was independently reproduced before this draft.

@Priyansh4444
Priyansh4444 marked this pull request as ready for review September 11, 2026 04:56
@ryansolid

Copy link
Copy Markdown
Member

Stuff against Solid 1.x at this point are taken with extra scrutiny because of where it is in its lifecycle. We actually started on some improvements of this nature in a 1.10.x beta branch but weren't suffciiently testing it. I know from my 2.x work there is definitely some perf on the table, but it might take a little bit before I get back to reviewing this given the state of Solid 1.x and and the focus on the 2.0 release.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants