Skip to content

perf: emplace buffered JSI array values - #2037

Merged
mrousavy merged 1 commit into
v1from
mrousavy/jsi-array-emplace
Nov 2, 2026
Merged

mrousavy merged 1 commit into
v1from
mrousavy/jsi-array-emplace

Conversation

@mrousavy

@mrousavy mrousavy commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Construct heap-buffered JSI values with reserve() and emplace_back() instead of default-initializing and then assigning every slot. Keep the stack path for up to eight elements and one bulk push into Hermes.

The shared helper covers C++ vectors, Swift arrays, and JNI primitive/object arrays. Tuples already construct their fixed staging values directly; their direct-insertion alternative is measured below.

Measured heap-backed numeric arrays take about 19–23% less time. Direct indexed insertion is slower for multi-element arrays and tuples, so bulk insertion stays here.

Payload Elements Current resize/assign + bulk push Reserve/emplace + bulk push Preallocated array + indexed stores
Numbers 0 11.7 11.6 (0.989×; 0.984–0.994) 10.5 (0.895×; 0.890–0.901)
Numbers 8 75.2 72.7 (0.965×; 0.958–0.971) 249.0 (3.309×; 3.287–3.320)
Numbers 9 117.8 91.5 (0.774×; 0.767–0.793) 278.8 (2.371×; 2.306–2.391)
Numbers 64 506.0 409.9 (0.811×; 0.803–0.820) 1,908.0 (3.769×; 3.737–3.792)
Numbers 16,384 111,860 89,698 (0.803×; 0.797–0.809) 484,276 (4.325×; 4.312–4.345)
Object references 64 665.6 572.1 (0.859×; 0.844–0.865) 2,049.9 (3.077×; 3.016–3.104)
Fresh UTF-8 strings 64 1,370.9 1,293.4 (0.943×; 0.938–0.949) 2,836.1 (2.065×; 2.056–2.074)
Tuple Bulk push (current and reserve PR) Direct indexed stores
Empty 20.1 11.1 (0.551×; 0.548–0.555)
Four numbers 37.9 128.3 (3.416×; 3.368–3.443)
Number, boolean, string, number 53.6 143.6 (2.679×; 2.649–2.709)

Reproducible benchmark source, raw samples and build commands: https://gist.github.com/mrousavy/039d0c2ee0624790396f84deda84dd80

Benchmark method and environment

Measurements are ns per produced array/tuple. Candidate cells show median time, median paired ratio to the current implementation, and the paired ratio's P10–P90 range; lower is faster. Each case has 35 samples across five fresh processes, seven rounds per process. All three methods used the same calibrated iteration count per case; option order and case order were shuffled. Each option warmed up before timing; a full Hermes GC ran before each batch, outside the timer. Every produced array escaped through a compiler barrier; result lengths, values and reference identity were checked outside timers. Allocation, conversion, stores, push and result destruction are inside the measured loop.

Apple M2 Pro (arm64), macOS 27.2 (26B123), Apple clang 21.0.0 (clang-2100.4.1.1), benchmark -std=c++20 -O3 -DNDEBUG. Hermes pinned commit 26fbb82fad39d9d06901adb1d6d630dacc84c877, rebuilt from its clean checkout using the repository's cached Release configuration: -O3 -DNDEBUG, Hades GC, HEAP_HV_64, Unicode-lite, debugger/Intl disabled. The benchmark is C++ → JSI → Hermes, with no JNI, Swift or React Native bridge. Object/string-reference cases clone preexisting rooted JS values; fresh strings use String::createFromUtf8 on 22–26-byte ASCII inputs. One process ran at a time during an exclusive no-build window.

Full benchmark cases
Payload Elements Current resize/assign + bulk push Reserve/emplace + bulk push Preallocated array + indexed stores
Numbers 0 11.7 11.6 (0.989×; 0.984–0.994) 10.5 (0.895×; 0.890–0.901)
Numbers 1 38.7 38.5 (0.992×; 0.985–1.003) 39.0 (1.008×; 1.001–1.026)
Numbers 8 75.2 72.7 (0.965×; 0.958–0.971) 249.0 (3.309×; 3.287–3.320)
Numbers 9 117.8 91.5 (0.774×; 0.767–0.793) 278.8 (2.371×; 2.306–2.391)
Numbers 16 166.5 135.8 (0.815×; 0.800–0.827) 486.8 (2.932×; 2.871–2.965)
Numbers 64 506.0 409.9 (0.811×; 0.803–0.820) 1,908.0 (3.769×; 3.737–3.792)
Numbers 256 1,813.0 1,468.8 (0.810×; 0.805–0.815) 7,667.0 (4.224×; 4.171–4.248)
Numbers 1,024 7,040.0 5,708.6 (0.810×; 0.805–0.825) 30,371 (4.319×; 4.277–4.336)
Numbers 16,384 111,860 89,698 (0.803×; 0.797–0.809) 484,276 (4.325×; 4.312–4.345)
Object references 0 12.0 11.6 (0.969×; 0.954–1.008) 10.8 (0.898×; 0.883–0.916)
Object references 1 40.2 39.4 (0.980×; 0.975–0.987) 40.0 (0.996×; 0.990–1.004)
Object references 8 103.8 97.8 (0.941×; 0.933–0.952) 267.6 (2.583×; 2.560–2.606)
Object references 9 143.3 121.9 (0.848×; 0.841–0.864) 299.2 (2.085×; 2.053–2.098)
Object references 16 208.6 178.5 (0.853×; 0.846–0.864) 521.4 (2.499×; 2.472–2.518)
Object references 64 665.6 572.1 (0.859×; 0.844–0.865) 2,049.9 (3.077×; 3.016–3.104)
Object references 256 2,499.8 2,159.5 (0.863×; 0.855–0.870) 8,225.5 (3.292×; 3.257–3.298)
Object references 1,024 9,718.7 8,384.0 (0.863×; 0.857–0.871) 32,679 (3.367×; 3.318–3.384)
Object references 16,384 160,729 136,143 (0.849×; 0.842–0.854) 519,944 (3.241×; 3.210–3.256)
String references 0 11.8 11.4 (0.964×; 0.953–0.974) 10.6 (0.896×; 0.888–0.906)
String references 1 40.4 39.6 (0.980×; 0.975–0.990) 39.7 (0.985×; 0.979–0.991)
String references 8 105.7 99.1 (0.936×; 0.927–0.942) 269.1 (2.546×; 2.452–2.561)
String references 9 146.0 124.1 (0.852×; 0.845–0.862) 300.8 (2.059×; 2.024–2.076)
String references 16 211.6 180.8 (0.856×; 0.850–0.864) 525.8 (2.484×; 2.450–2.502)
String references 64 670.7 581.3 (0.867×; 0.862–0.874) 2,065.6 (3.083×; 3.057–3.097)
String references 256 2,507.9 2,166.8 (0.865×; 0.858–0.871) 8,287.6 (3.302×; 3.281–3.324)
String references 1,024 9,694.9 8,365.8 (0.862×; 0.859–0.867) 32,962 (3.400×; 3.374–3.412)
String references 16,384 162,515 138,390 (0.854×; 0.843–0.876) 525,235 (3.239×; 3.176–3.264)
Fresh UTF-8 strings 0 11.9 11.6 (0.984×; 0.969–0.991) 11.7 (0.979×; 0.965–0.985)
Fresh UTF-8 strings 1 54.3 53.5 (0.985×; 0.976–0.992) 55.4 (1.020×; 1.010–1.030)
Fresh UTF-8 strings 8 192.7 185.3 (0.963×; 0.953–0.972) 359.7 (1.866×; 1.853–1.883)
Fresh UTF-8 strings 9 245.1 226.2 (0.924×; 0.917–0.931) 401.3 (1.635×; 1.621–1.649)
Fresh UTF-8 strings 16 386.0 359.4 (0.932×; 0.928–0.939) 732.6 (1.899×; 1.888–1.914)
Fresh UTF-8 strings 64 1,370.9 1,293.4 (0.943×; 0.938–0.949) 2,836.1 (2.065×; 2.056–2.074)
Fresh UTF-8 strings 256 4,908.5 4,634.2 (0.944×; 0.940–0.948) 10,951 (2.231×; 2.220–2.241)
Fresh UTF-8 strings 1,024 18,862 17,823 (0.944×; 0.941–0.950) 42,875 (2.275×; 2.265–2.282)
Fresh UTF-8 strings 16,384 358,936 341,608 (0.954×; 0.936–0.965) 718,175 (2.008×; 1.981–2.024)

Validation: the caller and template implementations pass ASan+UBSan checks for empty/small/large arrays, heterogeneous values, reference identity, conversion order, conversion exceptions and forced GC. Hermes itself is an uninstrumented Release build. All 127 generated Android C++ translation units pass NDK syntax checks. Prototype-setter behavior matches the current implementation. Benchmark fixtures remain outside the repository.

Draft direct-insertion follow-up: #2038.

Small-array follow-up: sizes 1–8

Fresh five-process run on the same Release Hermes/M2 Pro setup, with seven paired randomized rounds per case (35 samples per option). Times are ns per numeric array; ratio ranges are paired P10–P90. Baseline means the original resize/assignment implementation, reserve/emplace is #2037, and direct insertion is #2038.

Elements Baseline ns Reserve/emplace ns Reserve/baseline Direct index ns Direct/baseline
1 39.8 39.0 0.990× (0.972–1.011) 40.3 1.010× (0.983–1.035)
2 46.5 45.5 0.976× (0.938–1.004) 71.1 1.523× (1.481–1.572)
3 50.8 49.6 0.980× (0.921–1.005) 104.7 2.058× (1.957–2.098)
4 56.5 54.7 0.967× (0.944–1.038) 134.9 2.390× (2.274–2.469)
5 61.3 60.9 0.978× (0.941–1.011) 164.8 2.688× (2.620–2.749)
6 69.1 66.4 0.971× (0.937–0.996) 200.2 2.952× (2.848–3.040)
7 72.3 69.5 0.969× (0.921–0.984) 224.9 3.131× (3.021–3.298)
8 77.4 75.2 0.971× (0.945–1.005) 257.6 3.322× (3.245–3.412)

Direct insertion is effectively tied at one element and becomes slower at two. All four measured payload kinds show a regression from two elements. The small bulk paths use stack storage, so this range has no temporary heap-buffer allocation to eliminate.

Source, raw samples, build commands and full payload table: https://gist.github.com/mrousavy/baf71995556d27d5cf3f61921f8db4d3

@vercel

vercel Bot commented Nov 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
nitro-docs Skipped Skipped Nov 2, 2026 1:03pm UTC

Request Review

@mrousavy
mrousavy added this pull request to stack #2039 November 2, 2026 13:03
@mrousavy
mrousavy merged commit 4ee6537 into v1 Nov 2, 2026
16 checks passed

This branch was previously deployed

1 inactive deployment
Preview — f8114879 Deployed Nov 2, 2026 by vercel[bot]
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.

1 participant