Skip to content

[codex] Performance experiment: bind HybridObject functions to native instances - #1648

Draft
mrousavy wants to merge 3 commits into
mainfrom
codex/bound-hybrid-performance-experiment
Draft

[codex] Performance experiment: bind HybridObject functions to native instances#1648
mrousavy wants to merge 3 commits into
mainfrom
codex/bound-hybrid-performance-experiment

Conversation

@mrousavy

@mrousavy mrousavy commented Sep 9, 2026

Copy link
Copy Markdown
Member

This is a performance experiment, enabled for every newly created HybridObject on this branch. It tests the equivalent of binding each instance's methods and accessors to its native receiver: resolve the native type once during creation, capture a typed shared_ptr in each HostFunction, and call through that pointer. Repeated calls skip the JS receiver/NativeState lookup, RTTI downcast, and temporary receiver shared_ptr copies. Native virtual methods still use normal C++ virtual dispatch.

The instance gets its own methods, getters, setters, and raw JSI methods. The shared prototype remains intact, derived registrations take precedence, and argument conversion, argument-count checks, and exception translation remain shared with the original path. The existing performance suite exercises this path without benchmark or codegen changes. Removing the bindHybridFunctions(...) call in HybridObject::toObject() restores the original dispatch path.

This is intentionally experimental and not ready for production: creation allocates functions per instance, function identity and own-property enumeration change, and bound functions retain the native object and remain callable after dispose() clears NativeState. Raw JSI methods bind the native receiver but still receive the caller's JS thisValue.

The test utilities identify HybridObjects by prototype ancestry, since bound function identity differs per instance. The 55,000-object GC tests yield between batches to keep harness heartbeats responsive. External-memory tests collect earlier garbage before measuring global-counter deltas and retain the exact one-megabyte assertions. Benchmark receivers allow 20 minutes for a case; the fixed operation counts, all warmups/samples, and timed regions remain unchanged so creation overhead stays visible.

Validation:

  • Temporary native JavaScriptCore integration harness passed in DEBUG and NDEBUG configurations with AddressSanitizer and UndefinedBehaviorSanitizer. Covered detached calls, receiver rebinding, getters/setters, getter-only and setter-only properties, inherited overrides, original prototype dispatch, optional arguments/errors, raw methods, object caching, and receiver release after runtime teardown.
  • Added example-app regression cases for bound methods/accessors and per-instance function identity.
  • Example-app TypeScript check, ESLint, C++ formatting, and git diff --check passed.
  • All 562 C++/Swift example test cases passed locally in the CI-built Release Hermes simulator app with the updated JavaScript.
  • All 159 performance-tool tests, including 1,032 assertions, and the performance-tool TypeScript check passed.
  • All CI checks passed on 18e254bfa, including Android default/ASan and iOS default/ASan/TSan harness variants.
  • Another 600 local buffer/memory-pressure checks passed with extra garbage allocations between repetitions.
  • The full Android and iOS performance suites passed on 18e254bfa: 46 cases per platform across four baseline/head pairs, with matching suite hashes. Aggregation, PR report publication, and Bencher history publication all succeeded.

Performance results from the CI iOS simulator and Android emulator, comparing 201794bf to 18e254bf (full report):

C++ benchmark iOS before → after Android before → after
simpleFunc() 65.0 → 26.2 ns (59.62% less time) 68.4 → 22.6 ns (67.00% less time)
addNumbers() 85.5 → 47.5 ns (44.51% less time) 89.9 → 41.8 ns (53.54% less time)
Number property get/set 125.1 → 66.4 ns (46.97% less time) 152.7 → 72.2 ns (52.74% less time)
create() 394.4 ns → 118.85 µs (~301× the time) 624.3 ns → 131.69 µs (~211× the time)

The experiment reduces repeated-call overhead, with a large creation cost for the benchmark HybridObjects, which expose hundreds of functions. Swift and Kotlin simpleFunc() times fell by 45.70% and 47.89%; their creation times rose to 126.80 µs and 135.30 µs respectively. These are results from this CI run, not physical-device measurements.

@vercel

vercel Bot commented Sep 9, 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 Sep 9, 2026 10:32pm UTC

Request Review

@mrousavy

mrousavy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@nitro-modules-bot please test performance

@nitro-modules-bot

This comment has been minimized.

@mrousavy

mrousavy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@nitro-modules-bot please test performance

@mrousavy

mrousavy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@nitro-modules-bot please test performance

@nitro-modules-bot

Copy link
Copy Markdown

Performance Report

iOS

Benchmark Before After Difference
C++ bounce(1 MiB) 220.1 ns 180.4 ns 🟢 -18.03% faster
C++ bounce(4 KiB) 223.4 ns 179.4 ns 🟢 -19.68% faster
C++ bounce native-owned buffer (4 KiB) 345.2 ns 305.5 ns 🟢 -11.52% faster
C++ copy(1 MiB) 66.63 µs 61.87 µs 🟢 -7.15% faster
C++ copy(4 KiB) 915.6 ns 850.7 ns 🟢 -7.08% faster
C++ small array (16) 911.3 ns 844.2 ns 🟢 -7.36% faster
C++ synchronousCallback() 192.8 ns 139.5 ns 🟢 -27.65% faster
C++ create() 394.4 ns 118.85 µs 🔴 +30031.06% slower
C++ returnExisting() 193.0 ns 137.8 ns 🟢 -28.64% faster
C++ optional trailing string 141.4 ns 104.9 ns 🟢 -25.82% faster
C++ addNumbers() 85.5 ns 47.5 ns 🟢 -44.51% faster
C++ simpleFunc() 65.0 ns 26.2 ns 🟢 -59.62% faster
C++ deferred worker Promise (includes trigger call) 22.19 µs 20.90 µs 🟢 -5.84% faster
C++ immediatePromise() 723.7 ns 682.4 ns 🟢 -5.7% faster
C++ number property get/set 125.1 ns 66.4 ns 🟢 -46.97% faster
C++ short ASCII string 133.9 ns 94.1 ns 🟢 -29.71% faster
C++ Unicode string 275.8 ns 227.7 ns 🟢 -17.42% faster
C++ number | string variant 124.6 ns 88.3 ns 🟢 -29.14% faster
Swift bounce(1 MiB) 223.0 ns 197.0 ns 🟢 -11.69% faster
Swift bounce(4 KiB) 218.8 ns 178.1 ns 🟢 -18.61% faster
Swift bounce native-owned buffer (4 KiB) 359.0 ns 337.5 ns 🟢 -5.99% faster
Swift synchronousCallback() 211.0 ns 197.6 ns 🟢 -6.36% faster
Swift create() 23.12 µs 126.80 µs 🔴 +448.55% slower
Swift returnExisting() 233.3 ns 180.1 ns 🟢 -22.81% faster
Swift optional trailing string 137.3 ns 110.4 ns 🟢 -19.55% faster
Swift addNumbers() 72.3 ns 43.8 ns 🟢 -39.37% faster
Swift simpleFunc() 58.3 ns 31.7 ns 🟢 -45.7% faster
Swift number property get/set 132.3 ns 77.5 ns 🟢 -41.44% faster
Swift short ASCII string 165.0 ns 132.9 ns 🟢 -19.46% faster
Swift Unicode string 304.5 ns 279.7 ns 🟢 -8.13% faster
Swift number | string variant 171.4 ns 141.9 ns 🟢 -17.21% faster
All Benchmarks
Benchmark Before After Difference
JavaScript addNumbers() 23.0 ns 23.6 ns 🔴 +2.73% slower
C++ bounce native-owned buffer (1 MiB) 3.44 µs 3.44 µs 🔴 +0.13% slower
C++ large array (1,024) 48.02 µs 48.24 µs 🔴 +0.45% slower
C++ typed map (8 entries) 1.80 µs 1.73 µs 🟢 -3.84% faster
C++ nested Car struct 2.44 µs 2.38 µs 🟢 -2.36% faster
Swift bounce native-owned buffer (1 MiB) 3.58 µs 3.62 µs 🔴 +0.87% slower
Swift copy(1 MiB) 75.79 µs 73.88 µs 🟢 -2.51% faster
Swift copy(4 KiB) 1.06 µs 1.07 µs 🔴 +0.69% slower
Swift large array (1,024) 42.62 µs 42.77 µs 🔴 +0.37% slower
Swift small array (16) 886.0 ns 850.8 ns 🟢 -3.97% faster
Swift typed map (8 entries) 2.09 µs 2.05 µs 🟢 -1.94% faster
Swift deferred worker Promise (includes trigger call) 25.72 µs 25.05 µs 🟢 -2.61% faster
Swift immediatePromise() 1.26 µs 1.25 µs 🟢 -0.76% faster
Swift nested Car struct 2.14 µs 2.11 µs 🟢 -1.72% faster
TurboModule addNumbers() 917.8 ns 946.3 ns 🔴 +3.1% slower

Android

Benchmark Before After Difference
C++ bounce(1 MiB) 435.7 ns 383.8 ns 🟢 -11.91% faster
C++ bounce(4 KiB) 443.3 ns 401.3 ns 🟢 -9.47% faster
C++ copy(4 KiB) 760.7 ns 703.9 ns 🟢 -7.46% faster
C++ synchronousCallback() 318.8 ns 258.2 ns 🟢 -19.03% faster
C++ create() 624.3 ns 131.69 µs 🔴 +20995.72% slower
C++ returnExisting() 219.8 ns 132.3 ns 🟢 -39.82% faster
C++ optional trailing string 145.6 ns 92.4 ns 🟢 -36.54% faster
C++ addNumbers() 89.9 ns 41.8 ns 🟢 -53.54% faster
C++ simpleFunc() 68.4 ns 22.6 ns 🟢 -67% faster
C++ deferred worker Promise (includes trigger call) 22.18 µs 23.93 µs 🔴 +7.87% slower
C++ number property get/set 152.7 ns 72.2 ns 🟢 -52.74% faster
C++ short ASCII string 144.3 ns 85.2 ns 🟢 -40.98% faster
C++ Unicode string 253.1 ns 192.3 ns 🟢 -24.03% faster
C++ nested Car struct 2.10 µs 1.99 µs 🟢 -5.41% faster
C++ number | string variant 132.4 ns 85.6 ns 🟢 -35.33% faster
Kotlin bounce(1 MiB) 1.05 µs 937.2 ns 🟢 -10.51% faster
Kotlin bounce(4 KiB) 1.09 µs 1.02 µs 🟢 -6.89% faster
Kotlin copy(4 KiB) 4.91 µs 4.64 µs 🟢 -5.45% faster
Kotlin small array (16) 990.3 ns 939.2 ns 🟢 -5.16% faster
Kotlin synchronousCallback() 1.01 µs 919.4 ns 🟢 -8.71% faster
Kotlin create() 1.59 µs 135.30 µs 🔴 +8402.52% slower
Kotlin returnExisting() 594.8 ns 510.6 ns 🟢 -14.16% faster
Kotlin optional trailing string 315.0 ns 270.2 ns 🟢 -14.22% faster
Kotlin addNumbers() 119.8 ns 78.6 ns 🟢 -34.35% faster
Kotlin simpleFunc() 97.1 ns 50.6 ns 🟢 -47.89% faster
Kotlin immediatePromise() 2.28 µs 2.15 µs 🟢 -5.81% faster
Kotlin number property get/set 224.0 ns 137.7 ns 🟢 -38.52% faster
Kotlin short ASCII string 432.5 ns 393.7 ns 🟢 -8.97% faster
Kotlin Unicode string 704.3 ns 619.1 ns 🟢 -12.1% faster
Kotlin number | string variant 389.3 ns 324.0 ns 🟢 -16.77% faster
All Benchmarks
Benchmark Before After Difference
JavaScript addNumbers() 21.0 ns 21.1 ns 🔴 +0.58% slower
C++ bounce native-owned buffer (1 MiB) 3.21 µs 3.18 µs 🟢 -0.96% faster
C++ bounce native-owned buffer (4 KiB) 427.1 ns 406.8 ns 🟢 -4.75% faster
C++ copy(1 MiB) 30.46 µs 29.47 µs 🟢 -3.27% faster
C++ large array (1,024) 38.13 µs 38.96 µs 🔴 +2.16% slower
C++ small array (16) 773.0 ns 735.8 ns 🟢 -4.81% faster
C++ typed map (8 entries) 1.71 µs 1.71 µs 🟢 -0.13% faster
C++ immediatePromise() 846.4 ns 808.8 ns 🟢 -4.44% faster
Kotlin bounce native-owned buffer (1 MiB) 3.85 µs 3.95 µs 🔴 +2.68% slower
Kotlin bounce native-owned buffer (4 KiB) 1.10 µs 1.07 µs 🟢 -2.56% faster
Kotlin copy(1 MiB) 312.66 µs 298.93 µs 🟢 -4.39% faster
Kotlin large array (1,024) 39.16 µs 38.85 µs 🟢 -0.78% faster
Kotlin typed map (8 entries) 6.37 µs 6.09 µs 🟢 -4.36% faster
Kotlin deferred worker Promise (includes trigger call) 31.42 µs 31.32 µs 🟢 -0.32% faster
Kotlin nested Car struct 4.18 µs 4.19 µs 🔴 +0.18% slower
TurboModule addNumbers() 214.8 ns 215.7 ns 🔴 +0.4% slower

Benchmarking Code Diff 201794bf...18e254bf (view raw output)

Raw measurements and artifacts

Raw measurements: performance-report-1 (JSON artifact). Run 34414142495, attempt 1. Download requires GitHub access.

Android: measurements, attempt 1, apps, attempt 1.
iOS: measurements, attempt 1, apps, attempt 1.

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