Skip to content

perf: skip optional argument fallback for fixed-arity HybridFunctions - #1586

Open
mrousavy wants to merge 1 commit into
mainfrom
perf/required-argument-fast-path
Open

perf: skip optional argument fallback for fixed-arity HybridFunctions#1586
mrousavy wants to merge 1 commit into
mainfrom
perf/required-argument-fast-path

Conversation

@mrousavy

@mrousavy mrousavy commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

Avoid instantiating callMethod()'s static undefined fallback when a HybridFunction signature has no trailing optional arguments.

The argument count is already validated before entering callMethod(). An if constexpr now uses args[Is] directly for fixed-arity signatures, including zero-argument functions and signatures with non-trailing optionals. Signatures with trailing optional arguments retain the existing undefined fallback. A local invocation lambda shares the existing void/non-void return conversion without duplicating it.

No receiver binding, JNI, virtual dispatch, public API, codegen, argument-count validation, or JSI type-checking changes.

Why

Previously, even required-only calls declared static const jsi::Value defaultValue. Its initialization/lifetime guard survived optimized compilation despite the fallback never being needed on that path. Moving the declaration into the optional-only compile-time branch removes that guard and its initialization/exit-destructor registration code from fixed-arity instantiations. Simply making the value automatic would introduce a per-call JSI destructor instead.

Regression coverage

Add eight cases to the existing native test suite (each runs against C++ and Swift/Kotlin):

  • Required argument counts: too few / too many.
  • Explicit undefined and an incorrect type for required numbers.
  • Too many arguments to a zero-argument void method.
  • Explicit undefined for a trailing optional.
  • Missing a required argument after a non-trailing optional.
  • Omitting the only argument of an all-optional method.

Existing cases cover successful required calls, void methods, omitted/provided optionals, multiple trailing optionals, and explicit undefined in the middle of an argument list.

Validation

  • Monorepo package build (bun run build, Node 24.16.0 / Bun 1.3.14).
  • Example TypeScript check.
  • ESLint for the changed test file; C++ formatting with the repository's clang-format configuration; git diff --check.
  • Before/after Android ARM64 compile of actual HybridFunction::createHybridFunction instantiations using NDK 29.0.14206865, React Native 0.85.3 headers, C++20, -O2 -DNDEBUG: the default-value guard exists before the change for (double, double) -> double and () -> void, and is absent afterward. The (double, optional<double>) -> double fallback and guard remain.
  • Same before/after ARM64 iOS Simulator compilation with Apple Clang 21.0.0 confirms the same guard removal and optional fallback preservation.
  • Android ARM64 debug example build (:app:assembleDebug).
  • Android native harness: 554/554 tests passed, including all 16 added regression executions against C++ and Kotlin (Android 14 ARM64 emulator, debug build, minSdk 26 matching the CI harness configuration).
  • GitHub CI: default Android native harness, Android builds at minSdk 24 and 26, TypeScript compilation/lint, C++ formatting, and Nitrogen generation on Linux/Windows.
  • iOS native harness and remaining sanitizer jobs (CI still running at handoff).

The initial local minSdk 24 harness run passed 545/554 tests; the nine failures were HardwareBuffer cases whose native implementation requires a compile-time minSdk of at least 26. Rebuilding with the existing CI harness's minSdk 26 configuration passed all 554. The temporary build-configuration edit was reverted and is not part of this PR.

This PR claims removal of verified generated-code overhead, not a newly measured end-to-end percentage speedup. The earlier benchmark experiment motivated this fix but is not a timing measurement of this exact patch.

In the current Android compiler output, the required scalar path loses the four guard instructions (adrp, add, ldarb, tbz). The exact argument-count check, both checked asNumber() calls, and the virtual member invocation remain. These are assembly observations, not cycle counts or timings.

@vercel

vercel Bot commented Sep 4, 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 5, 2026 12:19pm UTC

Request Review

@mrousavy
mrousavy marked this pull request as ready for review September 4, 2026 10:41
@mrousavy
mrousavy force-pushed the perf/required-argument-fast-path branch from 98fe667 to e205c55 Compare September 5, 2026 12:19
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

Performance Report

⚠️ Advisory: Results do not fail this PR while the baseline is being calibrated.

iOS

Benchmark Before After Difference
C++ large array (1,024) 64.42 µs 68.53 µs 🔴 +6.38% slower
C++ typed map (8 entries) 2.94 µs 2.32 µs 🟢 -21.04% faster
C++ addNumbers() 124.3 ns 133.0 ns 🔴 +6.99% slower
C++ simpleFunc() 103.8 ns 114.2 ns 🔴 +10.01% slower
Swift number | string variant 371.7 ns 452.7 ns 🔴 +21.81% slower
All Benchmarks
Benchmark Before After Difference
JavaScript addNumbers() 38.0 ns 36.6 ns 🟡 -3.54% faster (noisy)
C++ bounce(1 MiB) 472.3 ns 424.3 ns 🟡 -10.16% faster (noisy)
C++ bounce(4 KiB) 407.8 ns 419.3 ns 🟡 +2.81% slower (noisy)
C++ copy(1 MiB) 80.80 µs 96.17 µs 🟡 +19.03% slower (noisy)
C++ copy(4 KiB) 1.35 µs 1.67 µs 🟡 +23.41% slower (noisy)
C++ small array (16) 1.22 µs 1.28 µs 🟡 +5.34% slower (noisy)
C++ synchronousCallback() 311.4 ns 369.6 ns 🟡 +18.71% slower (noisy)
C++ create() 706.3 ns 603.4 ns 🟡 -14.57% faster (noisy)
C++ returnExisting() 314.5 ns 327.9 ns 🟡 +4.27% slower (noisy)
C++ optional trailing string 265.9 ns 256.1 ns 🟡 -3.7% faster (noisy)
C++ immediatePromise() 1.17 µs 1.53 µs ℹ️ +30.03% slower (advisory)
C++ number property get/set 216.1 ns 206.8 ns 🟡 -4.3% faster (noisy)
C++ short ASCII string 217.1 ns 220.5 ns 🟡 +1.53% slower (noisy)
C++ Unicode string 506.6 ns 481.7 ns 🟡 -4.91% faster (noisy)
C++ nested Car struct 3.76 µs 3.21 µs 🟡 -14.72% faster (noisy)
C++ number | string variant 207.5 ns 209.5 ns 🟡 +0.95% slower (noisy)
Swift bounce(1 MiB) 471.3 ns 531.8 ns 🟡 +12.84% slower (noisy)
Swift bounce(4 KiB) 422.9 ns 560.0 ns 🟡 +32.41% slower (noisy)
Swift copy(1 MiB) 97.30 µs 102.75 µs 🟡 +5.6% slower (noisy)
Swift copy(4 KiB) 1.83 µs 2.19 µs 🟡 +20.12% slower (noisy)
Swift large array (1,024) 62.80 µs 77.91 µs 🟡 +24.07% slower (noisy)
Swift small array (16) 1.30 µs 1.42 µs 🟡 +9.18% slower (noisy)
Swift synchronousCallback() 427.1 ns 521.8 ns 🟡 +22.17% slower (noisy)
Swift create() 43.85 µs 46.75 µs 🟡 +6.63% slower (noisy)
Swift returnExisting() 420.4 ns 437.6 ns 🟡 +4.1% slower (noisy)
Swift typed map (8 entries) 3.41 µs 4.39 µs 🟡 +28.65% slower (noisy)
Swift optional trailing string 287.0 ns 322.1 ns 🟡 +12.25% slower (noisy)
Swift addNumbers() 126.5 ns 129.9 ns 🟡 +2.63% slower (noisy)
Swift simpleFunc() 101.3 ns 126.0 ns 🟡 +24.45% slower (noisy)
Swift immediatePromise() 2.23 µs 2.94 µs ℹ️ +31.92% slower (advisory)
Swift number property get/set 255.4 ns 254.4 ns 🟡 -0.37% faster (noisy)
Swift short ASCII string 249.4 ns 274.8 ns 🟡 +10.21% slower (noisy)
Swift Unicode string 578.7 ns 637.3 ns 🟡 +10.12% slower (noisy)
Swift nested Car struct 3.40 µs 3.62 µs 🟡 +6.57% slower (noisy)
TurboModule addNumbers() 1.34 µs 1.44 µs 🟡 +7.24% slower (noisy)

Android

Benchmark Before After Difference
C++ short ASCII string 203.1 ns 214.5 ns 🔴 +5.61% slower
All Benchmarks
Benchmark Before After Difference
JavaScript addNumbers() 38.6 ns 36.5 ns 🟡 -5.54% faster (noisy)
C++ bounce(1 MiB) 540.4 ns 524.0 ns 🟡 -3.02% faster (noisy)
C++ bounce(4 KiB) 510.1 ns 568.4 ns 🟡 +11.43% slower (noisy)
C++ copy(1 MiB) 25.82 µs 25.71 µs ⚪ ~0.4% unchanged
C++ copy(4 KiB) 1.16 µs 1.18 µs ⚪ ~1.52% unchanged
C++ large array (1,024) 64.26 µs 64.21 µs ⚪ ~0.07% unchanged
C++ small array (16) 1.28 µs 1.27 µs ⚪ ~0.51% unchanged
C++ synchronousCallback() 433.8 ns 434.9 ns 🟡 +0.25% slower (noisy)
C++ create() 815.2 ns 792.0 ns 🟡 -2.85% faster (noisy)
C++ returnExisting() 308.9 ns 306.3 ns ⚪ ~0.84% unchanged
C++ typed map (8 entries) 2.47 µs 2.46 µs ⚪ ~0.21% unchanged
C++ optional trailing string 220.3 ns 223.7 ns ⚪ ~1.52% unchanged
C++ addNumbers() 141.1 ns 135.1 ns ⚪ ~4.24% unchanged
C++ simpleFunc() 113.7 ns 110.4 ns ⚪ ~2.87% unchanged
C++ immediatePromise() 1.36 µs 1.36 µs ℹ️ +0.14% slower (advisory)
C++ number property get/set 223.9 ns 225.3 ns ⚪ ~0.6% unchanged
C++ Unicode string 408.4 ns 410.4 ns ⚪ ~0.5% unchanged
C++ nested Car struct 3.35 µs 3.34 µs ⚪ ~0.25% unchanged
C++ number | string variant 197.6 ns 197.2 ns ⚪ ~0.22% unchanged
Kotlin bounce(1 MiB) 1.40 µs 1.43 µs ⚪ ~2.12% unchanged
Kotlin bounce(4 KiB) 1.42 µs 1.43 µs ⚪ ~0.29% unchanged
Kotlin copy(1 MiB) 392.30 µs 392.36 µs ⚪ ~0.02% unchanged
Kotlin copy(4 KiB) 6.50 µs 6.60 µs ⚪ ~1.54% unchanged
Kotlin large array (1,024) 71.64 µs 71.38 µs ⚪ ~0.37% unchanged
Kotlin small array (16) 1.71 µs 1.71 µs ⚪ ~0.23% unchanged
Kotlin synchronousCallback() 1.37 µs 1.35 µs ⚪ ~1.69% unchanged
Kotlin create() 2.56 µs 2.49 µs 🟡 -2.7% faster (noisy)
Kotlin returnExisting() 930.8 ns 930.6 ns ⚪ ~0.02% unchanged
Kotlin typed map (8 entries) 9.69 µs 9.72 µs ⚪ ~0.36% unchanged
Kotlin optional trailing string 514.2 ns 508.3 ns ⚪ ~1.14% unchanged
Kotlin addNumbers() 189.8 ns 194.8 ns ⚪ ~2.6% unchanged
Kotlin simpleFunc() 154.4 ns 160.5 ns ⚪ ~3.94% unchanged
Kotlin immediatePromise() 3.86 µs 3.80 µs ℹ️ -1.45% faster (advisory)
Kotlin number property get/set 393.7 ns 397.0 ns ⚪ ~0.83% unchanged
Kotlin short ASCII string 704.2 ns 702.2 ns ⚪ ~0.28% unchanged
Kotlin Unicode string 1.14 µs 1.17 µs ⚪ ~2.26% unchanged
Kotlin nested Car struct 7.17 µs 7.12 µs ⚪ ~0.7% unchanged
Kotlin number | string variant 637.7 ns 635.5 ns ⚪ ~0.35% unchanged
TurboModule addNumbers() 365.4 ns 374.0 ns ⚪ ~2.35% unchanged

Benchmarking Code Diff fe50d8fc...e205c555 (view raw output)

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