Skip to content

JIT: rewrite wasm local.set/local.get as local.tee - #131513

Merged
AndyAyersMS merged 1 commit into
dotnet:mainfrom
AndyAyersMS:wasm-local-tee
Jul 29, 2026
Merged

JIT: rewrite wasm local.set/local.get as local.tee#131513
AndyAyersMS merged 1 commit into
dotnet:mainfrom
AndyAyersMS:wasm-local-tee

Conversation

@AndyAyersMS

Copy link
Copy Markdown
Member

Emitter peephole. Shrinks the R2R corelib code section by 2.56%.

Emitter peephole. Shrinks the R2R corelib code section by 2.56%.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 29, 2026 00:27
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 29, 2026
@AndyAyersMS

Copy link
Copy Markdown
Member Author

@adamperlin PTAL
fyi @dotnet/wasm-contrib

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 6 pipeline(s).
10 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This change adds a small WASM emitter peephole optimization in CoreCLR’s JIT to reduce instruction count by folding a local.set immediately followed by a local.get of the same local into a single local.tee.

Changes:

  • Add an optimization in emitter::emitIns_I to rewrite local.set N; local.get N into local.tee N when optimizations are enabled and peepholing the last instruction is safe.

@lewing

lewing commented Jul 29, 2026

Copy link
Copy Markdown
Member

Nice win. Verified the mechanics hold up: local.get/local.set/local.tee are all IF_ULEB128 with single-byte opcodes (instrswasm.h:59-61), so mutating idIns() in place without touching idInsFmt() keeps idCodeSize() and the IF_ULEB128 output path identical — already-saved igSize/igInsCnt stay valid. And emitCanPeepholeLastIns() is a sufficient guard for wasm specifically because every structured-control-flow marker (block/loop/end/try_table/catch) is emitted as a real instruction, so it would become emitLastIns and fail the INS_local_set test.

One complementary peephole you may want to fold in while you're here: local.tee N; droplocal.set N.

WasmProduceReg already emits exactly that pair today whenever node->IsUnusedValue():

https://github.com/dotnet/runtime/blob/main/src/coreclr/jit/codegenwasm.cpp#L804-L816

The fold is unconditionally valid (tee pops/stores/pushes, drop pops — net effect is set). It'd go in the no-immediate emitter::emitIns overload rather than emitIns_I, since INS_drop comes through instGen/emitIns:

void emitter::emitIns(instruction ins)
{
    // Rewrite `local.tee N; drop` as `local.set N`.
    //
    if ((ins == INS_drop) && m_compiler->opts.OptimizationEnabled() && emitCanPeepholeLastIns() &&
        (emitLastIns->idIns() == INS_local_tee))
    {
        JITDUMP("\n -- rewriting 'local.tee' as 'local.set' since its result is immediately dropped.\n");
        emitLastIns->idIns(INS_local_set);
        return;
    }

    instrDesc* id  = emitNewInstrSmall(EA_8BYTE);

There's a larger adjacent opportunity too, though it's clearly not a drive-by: five sites emit local.get N; drop purely to materialize-and-discard operands (codegenwasm.cpp:1004, 1855, 2383, 3828-3831, and hwintrinsiccodegenwasm.cpp:187). Eliminating that pair outright saves ~3 bytes each instead of 1, but it needs emitRemoveLastInstruction() un-gated from its #ifdef TARGET_ARM64 (emit.cpp:10061), plus care around the emitLocation-invalidation caveat documented there — and the GT_KEEPALIVE drop exists specifically to hold liveness for GC info. Probably its own change, if it's worth anything.

Note

This comment was generated by GitHub Copilot.

@adamperlin adamperlin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm!

@AndyAyersMS
AndyAyersMS merged commit 9ddd59e into dotnet:main Jul 29, 2026
135 of 138 checks passed
@AndyAyersMS

Copy link
Copy Markdown
Member Author

One complementary peephole you may want to fold in while you're here

There doesn't seem to be much savings here...

local.tee N; drop
46 bytes

local.get N; drop 
312 bytes

@adamperlin

Copy link
Copy Markdown
Contributor

One complementary peephole you may want to fold in while you're here

There doesn't seem to be much savings here...

local.tee N; drop
46 bytes

local.get N; drop 
312 bytes

Yeah, I know this pattern comes up in hwintrinsic codegen for the jump tables because we drop and then rematerialize the operands in a nested block. For the hwintrinsic jump tables at least, I have #131003 which, when implemented, should reduce the size footprint a lot there.

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

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants