Skip to content

[wasm][coreclr][R2R] Closed generic MethodSpec imports cannot call specialized bodies without an instantiating adapter #134565

Description

@lewing

Description

On browser-WASM CoreCLR ReadyToRun, a cross-module call to a closed generic value-type method can remain bound to an interpreter thunk even when the exact specialized R2R body exists in the target assembly.

The concrete reproduction is System.MemoryExtensions.Contains<int>(ReadOnlySpan<int>, int). R2R callers repeatedly enter the interpreter and execute the complete scan there instead of calling the compiled specialization and its existing SIMD SpanHelpers leaf.

This is not merely a missing profile/root. Explicitly rooting Contains<int> successfully emits its R2R body, but the caller still cannot bind to it because the callable import ABI has one more hidden generic-context parameter than the specialized body.

Configuration

The original signal came from browser-WASM CoreCLR microbenchmarks in build 20260923.2, runtime SHA 806095e5, performance SHA 99531dce, V8 15.3.76, Ubuntu 22.04 x64 Viper workers.

Controlled reproduction used the same runtime cohort in a minimal browser app. A supported deterministic MIBC was generated with dotnet-pgo create-mibc-from-method-list and consumed through @(PublishReadyToRunPgoFiles).

Regression?

This is best treated as an incomplete WASM R2R generic-call mechanism rather than a temporal regression from a known-good implementation.

Data

ADX build 20260923.2, same-build CoreCLR interpreter/R2R:

Benchmark Interpreter R2R Ratio
ContainsFalse<Int32>.Span(Size: 512) 17.2867 ms 125.5179 ms 7.26x
ContainsFalse<Int32>.Array(Size: 512) 17.4128 ms 122.3899 ms 7.03x
ContainsTrue<Int32>.Span(Size: 512) 8.5965 ms 63.9192 ms 7.44x

Controlled minimal app, 512 Contains calls per operation:

Case Interpreter R2R
False Span, length 512 17.6 ms 170.4 ms
False Array, length 512 17.6 ms 168.9 ms
True Span, length 512 9.0 ms 86.2 ms

The slowdown scales with elements examined: length 0/1 is approximately equal or faster under R2R, while length 8 and 512 diverge sharply. Fixed transition overhead is therefore not the main cost; the full scan runs interpreted.

Rooting MemoryExtensions.Contains<int> does not improve the result. With the rooted MIBC, representative results remained about 6-6.5x slower under R2R, and the compiled SIMD leaf still was not reached.

Analysis

The linker rewrites both the Array and Span benchmarks to the same System.MemoryExtensions.Contains<T> MethodSpec, so all three cases share the hot path.

Without the explicit profile, the CoreLib R2R image contains no MemoryExtensions.Contains<int> body. It does contain the optimized leaf SpanHelpers.NonPackedIndexOfValueType<int,DontNegate<int>>, whose WASM disassembly has the expected SIMD loop.

Live NESM/Chrome CDP debugging of the R2R caller shows:

  1. The first hot call enters the delay-load helper.
  2. The patched subsequent call enters CoreLib WasmR2RToInterpreterThunk(iiiip).
  3. This happens once per lookup: 512 R2R→interpreter transitions per benchmark operation.
  4. Breakpoints on the compiled specialized Contains<int> body and compiled SIMD SpanHelpers leaf do not fire.

A supported root experiment proves this is deeper than missing rooting:

  • Crossgen2 accepts System.MemoryExtensions.Contains<int>(ReadOnlySpan<int>, int).
  • The published CoreLib gains the exact managed R2R body, independently identified by both its name section and R2R MethodDef index.
  • The exact compiled specialization has WASM type:
(i32, i32, i32, i32) -> i32
  • The closed generic MethodSpec import and its working interpreter thunk have WASM type:
(i32, i32, i32, i32, i32) -> i32

The extra argument is the hidden generic-instantiation/MethodDesc context expected by the generic callable ABI. The exact value-type specialization does not need this context. Both shapes already contain the normal WASM managed-call plumbing; this is an additional generic-context parameter.

Because a WebAssembly table entry must have the exact expected function type, the four-argument specialized body cannot be installed in the five-argument import slot. No R2R-callable instantiating adapter is emitted or selected, so the runtime retains the correctly typed interpreter thunk despite the R2R body existing.

The likely fix is to emit/select an R2R instantiating adapter that:

  1. Accepts the generic MethodSpec callable ABI, including the hidden context.
  2. Drops or translates the context for an exact specialization that does not require it.
  3. Tail-calls the specialized R2R body using its actual ABI.
  4. Publishes that adapter in the portable entrypoint/import cell instead of the interpreter thunk.

Alternatively, the specialized body could be emitted with the externally callable generic-context ABI, if that is compatible with existing ReadyToRun method identity and sharing rules.

A regression test should:

  • Explicitly root MemoryExtensions.Contains<int>.
  • Verify the specialized body and SIMD leaf exist.
  • Verify the app MethodSpec import patches to the specialized body or an instantiating adapter, not WasmR2RToInterpreterThunk.
  • Verify the SIMD leaf executes.
  • Cover Span and linker-rewritten Array forms with true/false and length 0/8/512 cases.

Related issues/PRs:

Note

This issue was prepared with GitHub Copilot assistance.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions