Skip to content

Extend the member-call fast path to primitive string receivers#2717

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf/string-receiver-fast-call
Jul 21, 2026
Merged

Extend the member-call fast path to primitive string receivers#2717
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf/string-receiver-fast-call

Conversation

@lahma

@lahma lahma commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Part of the V8-gap performance campaign (#1775 follow-up). CPU profiles of the two string comparison rows showed the callee method for str.slice(...)-style calls being re-resolved on every invocation (StringInstance.GetOwnProperty + Engine.TryHandleStringValue ≈ 7.5% on object-string, Engine.GetValue(Reference) 8.4% on base64 — all driven from JintCallExpression).

Change

JintMemberExpression.GetCalleeForCall — which already serves object receivers from the member IC — gains a string-receiver lane: the method is resolved from String.prototype and the descriptor is cached per AST node under the same holder-identity + _propertiesVersion guard as the existing prototype-method cache (caching the descriptor, not the callable, keeps in-place String.prototype.slice = fn replacement honored — its value is read fresh per call). this is the primitive itself, matching the slow path's this-binding exactly.

Safety rails:

  • Build-time denylist for length and any name that Jint's StringInstance.GetOwnProperty would treat as an own index property — the check mirrors the runtime ToNumber coercion exactly ("0x1", "1e1", " 1", "01", "-0" all denied), so prototype-planted functions can never shadow own character properties through the cache.
  • Accessor descriptors are never cached — they fall to the Reference path so a getter runs exactly once even when its result is non-callable.
  • The receiver is never materialized during resolution (only the prototype is touched) — RavenApiUsageTests.CanInheritCustomString stays green.

Numbers (default BDN job, same-base A/B vs main)

Row main this PR delta
dromaeo-object-string-modern (Jint) 54.87 ms 43.80 ms −20.2%
dromaeo-object-string-modern (Jint_ParsedScript) 56.54 ms 43.88 ms −22.4%
dromaeo-string-base64-modern (Jint) 25.97 ms 21.47 ms −17.3%
dromaeo-string-base64-modern (Jint_ParsedScript) 25.54 ms 21.53 ms −15.7%

Gates

Jint.Tests 3658/3595 (net10/net472), PublicInterface 114/114 both TFMs, Test262 99,431 passed / 0 failed. New tests cover loop correctness, mid-loop prototype method replacement (assignment, defineProperty, delete), length semantics, and index-shaped prototype plants.

🤖 Generated with Claude Code

Calls like str.slice(...) / str.charCodeAt(i) previously always fell to
the Reference slow path, re-resolving the method through
Engine.GetValue -> TryHandleStringValue on every call (profiles show
StringInstance.GetOwnProperty 4.4% + TryHandleStringValue 3.1% on
dromaeo-object-string and Engine.GetValue 8.4% on string-base64, all
from JintCallExpression).

GetCalleeForCall now resolves the method for a JsString receiver
directly from the realm's %String.prototype%, caching the descriptor
per AST node under the same holder-identity + _propertiesVersion guard
as the existing prototype-method inline cache. In-place replacement
(String.prototype.slice = fn) flows through the cached descriptor's
live value; defineProperty/delete bump the version and re-resolve.

Safety rails:
- Build-time denylist keeps names that can be OWN properties of a boxed
  string out of the lane: `length` and any name whose ToNumber coercion
  is a non-negative int32 (StringInstance.GetOwnProperty coerces the
  name, so "0", "01", "0x1", "1e1", " 1", "-0" and "" all address
  characters). Denied names keep the unchanged Reference slow path, so
  own-property shadowing of prototype plants is preserved.
- Accessor-backed slots are never cached: they fall to the Reference
  path so getter side effects run exactly once even when the result is
  non-callable.
- The receiver is never boxed or materialized (no ToString/Length
  touch), keeping lazy CustomString implementations intact; `this` is
  the primitive itself, matching Reference.ThisValue on the slow path.
- A miss on the direct prototype (method absent or found deeper, e.g.
  Object.prototype.hasOwnProperty) falls back to the Reference path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma enabled auto-merge (squash) July 21, 2026 10:00
@lahma
lahma merged commit ca88e09 into sebastienros:main Jul 21, 2026
4 checks passed
@lahma
lahma deleted the perf/string-receiver-fast-call branch July 21, 2026 10:07
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