Skip to content

Commit ab081a2

Browse files
committed
[JSC] Allocate registers for locals in BBQ
https://bugs.webkit.org/show_bug.cgi?id=326017 rdar://188956724 Reviewed by Dan Hecht. getLocal() copied a local into a fresh temp and setLocal() stored one back, so a local was loaded at every read and stored at every write. Let an expression stack entry reference a local rather than copy it, with evacuateLocal() replacing every reference to a given local in the stack with its current value. For the function's busiest locals, ranked by IPInt, assign a callee-save register for the whole body, so a call preserves them by ABI. Assignments also store to the slot, since unwinding and frame reconstruction read locals there. Tests: JSTests/wasm/stress/bbq-argument-canonical-slots.js JSTests/wasm/stress/get-local-repeated-on-stack.js * JSTests/wasm/stress/bbq-argument-canonical-slots.js: Added. * JSTests/wasm/stress/get-local-repeated-on-stack.js: Added. (async test): * Source/JavaScriptCore/jit/RegisterAtOffsetList.cpp: (JSC::RegisterAtOffsetList::bbqCalleeSaveRegisters): Deleted. * Source/JavaScriptCore/jit/RegisterAtOffsetList.h: * Source/JavaScriptCore/wasm/WasmBBQJIT.cpp: (JSC::Wasm::BBQJITImpl::BBQJIT::BBQJIT): (JSC::Wasm::BBQJITImpl::BBQJIT::getLocal): (JSC::Wasm::BBQJITImpl::BBQJIT::assignToLocal): (JSC::Wasm::BBQJITImpl::BBQJIT::setLocal): (JSC::Wasm::BBQJITImpl::BBQJIT::teeLocal): (JSC::Wasm::BBQJITImpl::BBQJIT::addTopLevel): (JSC::Wasm::BBQJITImpl::BBQJIT::addLoopOSREntrypoint): (JSC::Wasm::BBQJITImpl::BBQJIT::makeStackMap): (JSC::Wasm::BBQJITImpl::BBQJIT::addReturn): (JSC::Wasm::BBQJITImpl::BBQJIT::flushValue): (JSC::Wasm::BBQJITImpl::BBQJIT::flushRegistersForException): (JSC::Wasm::BBQJITImpl::BBQJIT::emitTailCall): (JSC::Wasm::BBQJITImpl::BBQJIT::emitIndirectTailCall): (JSC::Wasm::BBQJITImpl::BBQJIT::addCallIndirect): (JSC::Wasm::BBQJITImpl::BBQJIT::willParseOpcode): (JSC::Wasm::BBQJITImpl::BBQJIT::didFinishParsingLocals): (JSC::Wasm::BBQJITImpl::BBQJIT::emitShuffle): (JSC::Wasm::BBQJITImpl::BBQJIT::allocateWithHint): (JSC::Wasm::BBQJITImpl::BBQJIT::loadIfNecessary): (JSC::Wasm::BBQJITImpl::BBQJIT::bind): (JSC::Wasm::BBQJITImpl::BBQJIT::unbind): (JSC::Wasm::BBQJITImpl::BBQJIT::flushAllLocals): (JSC::Wasm::BBQJITImpl::BBQJIT::assignPinnedLocals): (JSC::Wasm::BBQJITImpl::BBQJIT::loadPinnedLocals): (JSC::Wasm::BBQJITImpl::BBQJIT::assertPinnedLocalsAreResident): (JSC::Wasm::BBQJITImpl::BBQJIT::unbindAllLocals): (JSC::Wasm::BBQJITImpl::BBQJIT::tailCallArgumentSource): (JSC::Wasm::BBQJITImpl::BBQJIT::evacuateLocal): (JSC::Wasm::parseAndCompileBBQ): (JSC::Wasm::BBQJIT::emitPushCalleeSaves): (JSC::Wasm::BBQJIT::emitRestoreCalleeSaves): (JSC::Wasm::BBQJITImpl::ControlData::touch): Deleted. * Source/JavaScriptCore/wasm/WasmBBQJIT.h: (JSC::Wasm::BBQJITImpl::BBQJIT::ControlData::flushAtBlockBoundary): (JSC::Wasm::BBQJITImpl::BBQJIT::maxPinnedLocals): (JSC::Wasm::BBQJITImpl::BBQJIT::isPinnedLocalRegister const): (JSC::Wasm::BBQJITImpl::BBQJIT::assertPinnedLocalsAreResident): (JSC::Wasm::BBQJITImpl::BBQJIT::ownsRegister const): (JSC::Wasm::BBQJITImpl::BBQJIT::clobber): * Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp: (JSC::Wasm::BBQJITImpl::BBQJIT::getGlobal): (JSC::Wasm::BBQJITImpl::BBQJIT::emitAtomicCompareExchange): (JSC::Wasm::BBQJITImpl::BBQJIT::addRefI31): (JSC::Wasm::BBQJITImpl::BBQJIT::marshallToInt): (JSC::Wasm::BBQJITImpl::BBQJIT::addArrayNew): (JSC::Wasm::BBQJITImpl::BBQJIT::addArrayNewDefault): (JSC::Wasm::BBQJITImpl::BBQJIT::addArrayGet): (JSC::Wasm::BBQJITImpl::BBQJIT::emitArraySetUnchecked): (JSC::Wasm::BBQJITImpl::BBQJIT::addArrayFill): (JSC::Wasm::BBQJITImpl::BBQJIT::emitCatchPrologue): (JSC::Wasm::BBQJITImpl::BBQJIT::materializeToGPR): (JSC::Wasm::BBQJITImpl::BBQJIT::marshallToI64): Deleted. * Source/JavaScriptCore/wasm/WasmCallee.cpp: (JSC::Wasm::IPIntCallee::IPIntCallee): (JSC::Wasm::BBQCallee::calleeSaveRegistersImpl): Deleted. * Source/JavaScriptCore/wasm/WasmCallee.h: * Source/JavaScriptCore/wasm/WasmFunctionIPIntMetadataGenerator.cpp: (JSC::Wasm::FunctionIPIntMetadataGenerator::hotLocals const): * Source/JavaScriptCore/wasm/WasmFunctionIPIntMetadataGenerator.h: (JSC::Wasm::FunctionIPIntMetadataGenerator::recordLocalRead): (JSC::Wasm::FunctionIPIntMetadataGenerator::recordLocalWrite): (JSC::Wasm::FunctionIPIntMetadataGenerator::enterLoop): (JSC::Wasm::FunctionIPIntMetadataGenerator::exitLoop): (JSC::Wasm::FunctionIPIntMetadataGenerator::loopDepth const): (JSC::Wasm::FunctionIPIntMetadataGenerator::adjustLocalScore): * Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp: (JSC::Wasm::IPIntGenerator::getLocal): (JSC::Wasm::IPIntGenerator::setLocal): (JSC::Wasm::IPIntGenerator::teeLocal): (JSC::Wasm::IPIntGenerator::addLoop): (JSC::Wasm::IPIntGenerator::addEndToUnreachable): (JSC::Wasm::IPIntGenerator::finalize): Canonical link: https://commits.webkit.org/323294@main
1 parent b7bbb82 commit ab081a2

13 files changed

Lines changed: 716 additions & 172 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Builder from '../Builder.js'
2+
import * as assert from '../assert.js'
3+
4+
const argumentCount = 8;
5+
const counter = argumentCount;
6+
7+
let f = new Builder()
8+
.Type().End()
9+
.Function().End()
10+
.Export().Function("readArgumentsLate").End()
11+
.Code()
12+
.Function("readArgumentsLate", { params: new Array(argumentCount).fill("i32"), ret: "i32" }, ["i32"])
13+
.I32Const(0)
14+
.SetLocal(counter)
15+
.Loop("void")
16+
.Block("void", b => b
17+
.GetLocal(counter).I32Const(64).I32Eq().BrIf(0)
18+
.GetLocal(counter).I32Const(1).I32Add().SetLocal(counter)
19+
.Br(1))
20+
.End();
21+
22+
f = f.GetLocal(0);
23+
for (let i = 1; i < argumentCount; ++i)
24+
f = f.GetLocal(i).I32Const(i + 1).I32Mul().I32Add();
25+
26+
const bin = f.Return().End().End().WebAssembly().get();
27+
const instance = new WebAssembly.Instance(new WebAssembly.Module(bin));
28+
29+
const args = [11, 22, 33, 44, 55, 66, 77, 88];
30+
let expected = args[0];
31+
for (let i = 1; i < argumentCount; ++i)
32+
expected += args[i] * (i + 1);
33+
34+
for (let i = 0; i < wasmTestLoopCount; ++i)
35+
assert.eq(expected, instance.exports.readArgumentsLate(...args));
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//@ requireOptions("--useWasmTailCalls=true")
2+
import { instantiate } from "../wabt-wrapper.js"
3+
import * as assert from "../assert.js"
4+
5+
// A cross-instance return_call_indirect whose arguments are locals held in registers. Every
6+
// allocatable GPR holds a local, so making the restore frame needs a scratch that evicts one of
7+
// them. Locals are set in register allocation order (ARM64: x0-x7, x9-x15). The high-index locals
8+
// land in the registers freed before the frame is copied, so only high-index slots bound the copy;
9+
// local 0 lands in the first register the scratch can take, and its slot is far above that bound.
10+
const argumentCount = 15;
11+
const setOrder = [14, 13, 11, 10, 9, 8, 7, 6, 12, 0, 1, 2, 3, 4, 5];
12+
const params = Array(argumentCount).fill("i32").join(" ");
13+
14+
function weightedSum(values)
15+
{
16+
let sum = 0;
17+
for (let i = 0; i < values.length; ++i)
18+
sum = (sum + Math.imul(values[i], i + 1)) | 0;
19+
return sum;
20+
}
21+
22+
const calleeBody = [];
23+
for (let i = 0; i < argumentCount; ++i)
24+
calleeBody.push(`(local.get ${i}) (i32.const ${i + 1}) (i32.mul)`, i ? "(i32.add)" : "");
25+
26+
const watCallee = `
27+
(module
28+
(type $sig (func (param ${params}) (result i32)))
29+
(func (export "weightedSum") (type $sig)
30+
${calleeBody.join("\n ")}
31+
)
32+
)`;
33+
34+
const locals = [];
35+
const sets = [];
36+
const gets = [];
37+
for (let i = 0; i < argumentCount; ++i) {
38+
locals.push(`(local $l${i} i32)`);
39+
gets.push(`(local.get $l${i})`);
40+
}
41+
for (let i of setOrder)
42+
sets.push(`(local.set $l${i} (i32.add (global.get $base) (i32.const ${(i + 1) * 7})))`);
43+
44+
const watCaller = `
45+
(module
46+
(type $sig (func (param ${params}) (result i32)))
47+
(import "m" "weightedSum" (func $weightedSum (type $sig)))
48+
(import "m" "base" (global $base i32))
49+
(table 1 funcref)
50+
(elem (i32.const 0) $weightedSum)
51+
(func (export "test") (result i32)
52+
${locals.join(" ")}
53+
${sets.join("\n ")}
54+
${gets.join(" ")}
55+
(i32.const 0)
56+
(return_call_indirect (type $sig))
57+
)
58+
)`;
59+
60+
async function test()
61+
{
62+
const base = 1000;
63+
const callee = await instantiate(watCallee, { }, { tail_call: true });
64+
const caller = await instantiate(watCaller, {
65+
m: { weightedSum: callee.exports.weightedSum, base: new WebAssembly.Global({ value: "i32" }, base) },
66+
}, { tail_call: true });
67+
68+
const expected = weightedSum(Array.from({ length: argumentCount }, (_, i) => base + (i + 1) * 7));
69+
for (let i = 0; i < wasmTestLoopCount; ++i)
70+
assert.eq(caller.exports.test(), expected);
71+
}
72+
73+
await assert.asyncTest(test());
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import * as assert from '../assert.js'
2+
import { instantiate } from '../wabt-wrapper.js';
3+
4+
async function test() {
5+
// The same local reaching a call twice, so the parallel move has to fan one source out to two
6+
// parameter registers.
7+
{
8+
const instance = await instantiate(`
9+
(func $sub (param i32) (param i32) (result i32)
10+
(i32.sub (local.get 0) (local.get 1)))
11+
12+
(func (export "foo") (param i32) (result i32)
13+
(call $sub (local.get 0) (local.get 0)))
14+
`);
15+
assert.eq(instance.exports.foo(7), 0);
16+
}
17+
18+
// Fan-out and a swap in the same move: the third argument shares a source with the first, while
19+
// the first two are passed in the opposite order to the one they arrive in.
20+
{
21+
const instance = await instantiate(`
22+
(func $mix (param i32) (param i32) (param i32) (result i32)
23+
(i32.add (i32.mul (local.get 0) (i32.const 100))
24+
(i32.add (i32.mul (local.get 1) (i32.const 10)) (local.get 2))))
25+
26+
(func (export "foo") (param i32) (param i32) (result i32)
27+
(call $mix (local.get 1) (local.get 0) (local.get 1)))
28+
`);
29+
assert.eq(instance.exports.foo(2, 3), 323);
30+
}
31+
32+
// One local filling every parameter, to exercise a source that is already-moved by the time the
33+
// later copies are emitted.
34+
{
35+
const instance = await instantiate(`
36+
(func $sum4 (param i32) (param i32) (param i32) (param i32) (result i32)
37+
(i32.add (i32.add (local.get 0) (local.get 1))
38+
(i32.add (local.get 2) (local.get 3))))
39+
40+
(func (export "foo") (param i32) (result i32)
41+
(call $sum4 (local.get 0) (local.get 0) (local.get 0) (local.get 0)))
42+
`);
43+
assert.eq(instance.exports.foo(5), 20);
44+
}
45+
46+
// The same local as two return values.
47+
{
48+
const instance = await instantiate(`
49+
(func (export "foo") (param i32) (result i32 i32)
50+
(local.get 0) (local.get 0))
51+
`);
52+
assert.eq(instance.exports.foo(11), [11, 11]);
53+
}
54+
55+
// Floats, so the fan-out runs through the FPR side of the move.
56+
{
57+
const instance = await instantiate(`
58+
(func $fsub (param f64) (param f64) (result f64)
59+
(f64.sub (local.get 0) (local.get 1)))
60+
61+
(func (export "foo") (param f64) (result f64 f64)
62+
(call $fsub (local.get 0) (local.get 0))
63+
(local.get 0))
64+
`);
65+
assert.eq(instance.exports.foo(1.5), [0, 1.5]);
66+
}
67+
68+
// Several entries naming one local when a write to it lands: each needs the old value, and the
69+
// reads afterwards need the new one.
70+
{
71+
const instance = await instantiate(`
72+
(func (export "foo") (param i32) (result i32 i32 i32 i32)
73+
(local.get 0)
74+
(local.get 0)
75+
(local.get 0)
76+
(local.set 0 (i32.const 42))
77+
(local.get 0))
78+
`);
79+
assert.eq(instance.exports.foo(3), [3, 3, 3, 42]);
80+
}
81+
82+
// A write inside a nested block, with the readers sitting on the enclosing stack.
83+
{
84+
const instance = await instantiate(`
85+
(func (export "foo") (param i32) (result i32 i32 i32)
86+
(local.get 0)
87+
(local.get 0)
88+
(block (local.set 0 (i32.const 0xbbadbeef)))
89+
(local.get 0))
90+
`);
91+
assert.eq(instance.exports.foo(3), [3, 3, 0xbbadbeef | 0]);
92+
}
93+
94+
// Duplicate namers crossing a block boundary as that block's results.
95+
{
96+
const instance = await instantiate(`
97+
(func (export "foo") (param i32) (result i32 i32)
98+
(block (result i32 i32)
99+
(local.get 0)
100+
(local.get 0)))
101+
`);
102+
assert.eq(instance.exports.foo(9), [9, 9]);
103+
}
104+
105+
// Duplicate namers live across a loop back edge.
106+
{
107+
const instance = await instantiate(`
108+
(func (export "foo") (param i32) (result i32)
109+
(local i32)
110+
(local.set 1 (i32.const 0))
111+
(loop $l
112+
(local.set 1 (i32.add (local.get 1) (i32.add (local.get 0) (local.get 0))))
113+
(local.set 0 (i32.sub (local.get 0) (i32.const 1)))
114+
(br_if $l (local.get 0)))
115+
(local.get 1))
116+
`);
117+
assert.eq(instance.exports.foo(4), 20);
118+
}
119+
}
120+
121+
await assert.asyncTest(test());

‎Source/JavaScriptCore/jit/RegisterAtOffsetList.cpp‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,6 @@ const RegisterAtOffsetList& RegisterAtOffsetList::ipintCalleeSaveRegisters()
120120
return result.get();
121121
}
122122

123-
const RegisterAtOffsetList& RegisterAtOffsetList::bbqCalleeSaveRegisters()
124-
{
125-
static std::once_flag onceKey;
126-
static LazyNeverDestroyed<RegisterAtOffsetList> result;
127-
std::call_once(onceKey, [] {
128-
result.construct(RegisterSet::bbqCalleeSaveRegisters());
129-
});
130-
return result.get();
131-
}
132123
#endif
133124

134125
} // namespace JSC

‎Source/JavaScriptCore/jit/RegisterAtOffsetList.h‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ class RegisterAtOffsetList {
7373
#if ENABLE(WEBASSEMBLY)
7474
static const RegisterAtOffsetList& wasmPinnedRegisters();
7575
static const RegisterAtOffsetList& ipintCalleeSaveRegisters(); // Registers and Offsets saved and used by IPInt.
76-
static const RegisterAtOffsetList& bbqCalleeSaveRegisters(); // Registers and Offsets saved and used by BBQ.
7776
#endif
7877

7978
private:

0 commit comments

Comments
 (0)