Skip to content

Commit 3563dd1

Browse files
committed
[JSC] Fast JS-to-Wasm call from FTL
https://bugs.webkit.org/show_bug.cgi?id=250545 rdar://104214223 Reviewed by Keith Miller. This patch supports direct Wasm call from FTL. FTL can know type speculations. We check this in DFG strength reduction phase, and generate appropriate stack and register assignment for Wasm call. This is further more efficient than Wasm IC since, 1. Based on type speculation, we can skip many type checks for arguments. 2. Because FTL can control registers and stacks, we can appropriately configure values in the right argument registers and stack location in FTL side and directly call Wasm function from FTL. By using patchpoint, B3 can assign right registers / stack location for them. 3. This removes Wasm IC trampoline between JS and Wasm function. Wasm function is now directly called from JS. To make this work, we require 259139@main. That change allows us to remove a hack in unwinding for wasm (wasm function call can modify global state (vm.wasmContext.instance), and unwinding needed to restore them appropriately. The above patch removed this necessity). As a result, we can directly call wasm function from FTL without doing a hack in unwinding. And we can also remove save / restore of vm.wasmContext.instance. We also need to encourage CallWasm in DFG ByteCodeParser. CallWasm needs constant-folded callee currently, but it needs to be materialized well from DFG ByteCodeParser by inserting appropriate checks from CallVariant. Note that we are reporting wasm pinned registers' clobbering from FTL patchpoint. This teaches FTL to save and resume these callee-save registers as FTL's callee-save registers. Thus, OSR exit / exception unwinding just works well: FTL cares these registers and correctly restore them when OSR exit happens. This is also the reason why we cannot apply this optimization to TailCall right now: wasm function clobbers callee-save registers and tail-call needs an adaptor to restore them correctly when returning to the caller's caller. In the future, we should align wasm pinned registers with JS JIT default callee-save registers so that we can easily restore then when OSR exit happens from DFG too. This is necessary if we would like to introduce this direct call from DFG side. This improves JetStream2/richards-wasm Runtime from 13.021 to 16.129, 23% improvement. * Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h: (JSC::DFG::AbstractInterpreter<AbstractStateType>::executeEffects): * Source/JavaScriptCore/dfg/DFGClobberize.h: (JSC::DFG::clobberize): * Source/JavaScriptCore/dfg/DFGDoesGC.cpp: (JSC::DFG::doesGC): * Source/JavaScriptCore/dfg/DFGFixupPhase.cpp: (JSC::DFG::FixupPhase::fixupNode): * Source/JavaScriptCore/dfg/DFGMayExit.cpp: * Source/JavaScriptCore/dfg/DFGNode.cpp: (JSC::DFG::Node::convertToCallWasm): * Source/JavaScriptCore/dfg/DFGNode.h: (JSC::DFG::Node::hasHeapPrediction): (JSC::DFG::Node::hasCellOperand): * Source/JavaScriptCore/dfg/DFGNodeType.h: * Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp: * Source/JavaScriptCore/dfg/DFGSafeToExecute.h: (JSC::DFG::safeToExecute): * Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h: * Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp: (JSC::DFG::SpeculativeJIT::compile): * Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp: (JSC::DFG::SpeculativeJIT::compileCallWasm): (JSC::DFG::SpeculativeJIT::compile): * Source/JavaScriptCore/dfg/DFGStrengthReductionPhase.cpp: (JSC::DFG::StrengthReductionPhase::handleNode): * Source/JavaScriptCore/ftl/FTLCapabilities.cpp: (JSC::FTL::canCompile): * Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp: (JSC::FTL::DFG::LowerDFGToB3::compileNode): (JSC::FTL::DFG::LowerDFGToB3::compileCompareStrictEq): Canonical link: https://commits.webkit.org/259250@main
1 parent 6b4b5fc commit 3563dd1

27 files changed

+653
-36
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as assert from '../assert.js';
2+
import Builder from '../Builder.js';
3+
4+
function jsToWasm(func, arg0, arg1, arg2)
5+
{
6+
try {
7+
var result = func(arg0, arg1, arg2);
8+
throw new Error("throwing");
9+
} catch (error) {
10+
return error;
11+
}
12+
}
13+
noInline(jsToWasm);
14+
15+
async function test() {
16+
let builder = (new Builder())
17+
.Type().End()
18+
.Import().Function("context", "callback", { params: [], ret: "void" }).End()
19+
.Function().End()
20+
.Export()
21+
.Function("foo")
22+
.End()
23+
.Code()
24+
.Function("foo", { params: ["i32", "i32", "i32"], ret: "i32" })
25+
.Call(0)
26+
.I32Const(42)
27+
.GetLocal(0)
28+
.GetLocal(1)
29+
.GetLocal(2)
30+
.I32Add()
31+
.I32Add()
32+
.I32Add()
33+
.Return()
34+
.End()
35+
.End();
36+
37+
let flag = false;
38+
function callback() {
39+
if (flag)
40+
throw new Error("flagged");
41+
}
42+
43+
const bin = builder.WebAssembly().get();
44+
const {instance} = await WebAssembly.instantiate(bin, { context: { callback } });
45+
46+
for (let i = 0; i < 100000; i++)
47+
assert.instanceof(jsToWasm(instance.exports.foo, 1, 2, 3), Error);
48+
flag = true;
49+
assert.instanceof(jsToWasm(instance.exports.foo, 1, 2, 3), Error);
50+
for (let i = 0; i < 100000; i++)
51+
assert.instanceof(jsToWasm(instance.exports.foo, 1, 2, 3), Error);
52+
}
53+
54+
assert.asyncTest(test());
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as assert from '../assert.js';
2+
import Builder from '../Builder.js';
3+
4+
async function test() {
5+
let params = [];
6+
for (let i = 0; i < 50; ++i)
7+
params.push('f64');
8+
9+
let cont = (new Builder())
10+
.Type().End()
11+
.Function().End()
12+
.Export()
13+
.Function("foo")
14+
.End()
15+
.Code()
16+
.Function("foo", { params: params, ret: "f64" });
17+
for (let i = 0; i < 50; ++i)
18+
cont = cont.GetLocal(i);
19+
for (let i = 0; i < 49; ++i)
20+
cont = cont.F64Add();
21+
let builder = cont.Return()
22+
.End()
23+
.End();
24+
25+
const bin = builder.WebAssembly().get();
26+
const {instance} = await WebAssembly.instantiate(bin, {});
27+
28+
for (let i = 0; i < 1000000; i++) {
29+
assert.eq(instance.exports.foo(
30+
0,1,2,3,4,5,6,7,8,9,
31+
10,11,12,13,14,15,16,17,18,19,
32+
20,21,22,23,24,25,26,27,28,29,
33+
30,31,32,33,34,35,36,37,38,39,
34+
40,41,42,43,44,45,46,47,48,49,
35+
), 1225);
36+
}
37+
}
38+
39+
assert.asyncTest(test());
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as assert from '../assert.js';
2+
import Builder from '../Builder.js';
3+
4+
async function test() {
5+
let params = [];
6+
for (let i = 0; i < 50; ++i)
7+
params.push('externref');
8+
9+
let cont = (new Builder())
10+
.Type().End()
11+
.Function().End()
12+
.Export()
13+
.Function("foo")
14+
.End()
15+
.Code()
16+
.Function("foo", { params: params, ret: "externref" });
17+
for (let i = 0; i < 50; ++i)
18+
cont = cont.GetLocal(i);
19+
for (let i = 0; i < 49; ++i)
20+
cont = cont.Drop();
21+
let builder = cont.Return()
22+
.End()
23+
.End();
24+
25+
const bin = builder.WebAssembly().get();
26+
const {instance} = await WebAssembly.instantiate(bin, {});
27+
28+
for (let i = 0; i < 1000000; i++) {
29+
assert.eq(instance.exports.foo(
30+
0,1,2,3,4,5,6,7,8,9,
31+
10,11,12,13,14,15,16,17,18,19,
32+
20,21,22,23,24,25,26,27,28,29,
33+
30,31,32,33,34,35,36,37,38,39,
34+
40,41,42,43,44,45,46,47,48,49,
35+
), 0);
36+
}
37+
}
38+
39+
assert.asyncTest(test());
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as assert from '../assert.js';
2+
import Builder from '../Builder.js';
3+
4+
async function test() {
5+
let params = [];
6+
for (let i = 0; i < 50; ++i)
7+
params.push('i32');
8+
9+
let cont = (new Builder())
10+
.Type().End()
11+
.Function().End()
12+
.Export()
13+
.Function("foo")
14+
.End()
15+
.Code()
16+
.Function("foo", { params: params, ret: "i32" });
17+
for (let i = 0; i < 50; ++i)
18+
cont = cont.GetLocal(i);
19+
for (let i = 0; i < 49; ++i)
20+
cont = cont.I32Add();
21+
let builder = cont.Return()
22+
.End()
23+
.End();
24+
25+
const bin = builder.WebAssembly().get();
26+
const {instance} = await WebAssembly.instantiate(bin, {});
27+
28+
for (let i = 0; i < 1000000; i++) {
29+
assert.eq(instance.exports.foo(
30+
0,1,2,3,4,5,6,7,8,9,
31+
10,11,12,13,14,15,16,17,18,19,
32+
20,21,22,23,24,25,26,27,28,29,
33+
30,31,32,33,34,35,36,37,38,39,
34+
40,41,42,43,44,45,46,47,48,49,
35+
), 1225);
36+
}
37+
}
38+
39+
assert.asyncTest(test());

Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
#include "StringObject.h"
5656
#include "StructureCache.h"
5757
#include "StructureRareDataInlines.h"
58+
#include "WasmTypeDefinitionInlines.h"
5859
#include "WebAssemblyModuleRecord.h"
5960
#include <wtf/BooleanLattice.h>
6061
#include <wtf/CheckedArithmetic.h>
@@ -4642,6 +4643,45 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
46424643
makeHeapTopForNode(node);
46434644
break;
46444645

4646+
case CallWasm: {
4647+
#if ENABLE(WEBASSEMBLY)
4648+
clobberWorld();
4649+
4650+
WebAssemblyFunction* wasmFunction = node->castOperand<WebAssemblyFunction*>();
4651+
const auto& typeDefinition = Wasm::TypeInformation::get(wasmFunction->typeIndex()).expand();
4652+
const auto& signature = *typeDefinition.as<Wasm::FunctionSignature>();
4653+
if (signature.returnsVoid()) {
4654+
setConstant(node, jsUndefined());
4655+
break;
4656+
}
4657+
4658+
ASSERT(signature.returnCount() == 1);
4659+
auto type = signature.returnType(0);
4660+
switch (type.kind) {
4661+
case Wasm::TypeKind::I32: {
4662+
setNonCellTypeForNode(node, SpecInt32Only);
4663+
break;
4664+
}
4665+
case Wasm::TypeKind::Ref:
4666+
case Wasm::TypeKind::RefNull:
4667+
case Wasm::TypeKind::Funcref:
4668+
case Wasm::TypeKind::Externref: {
4669+
makeHeapTopForNode(node);
4670+
break;
4671+
}
4672+
case Wasm::TypeKind::F32:
4673+
case Wasm::TypeKind::F64: {
4674+
setNonCellTypeForNode(node, SpecBytecodeDouble);
4675+
break;
4676+
}
4677+
default: {
4678+
break;
4679+
}
4680+
}
4681+
#endif
4682+
break;
4683+
}
4684+
46454685
case ForceOSRExit:
46464686
case CheckBadValue:
46474687
m_state.setIsValid(false);

Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,13 @@ class ByteCodeParser {
195195
bool handleVarargsInlining(Node* callTargetNode, Operand result, const CallLinkStatus&, int registerOffset, VirtualRegister thisArgument, VirtualRegister argumentsArgument, unsigned argumentsOffset, NodeType callOp, InlineCallFrame::Kind);
196196
unsigned getInliningBalance(const CallLinkStatus&, CodeSpecializationKind);
197197
enum class CallOptimizationResult { OptimizedToJump, Inlined, DidNothing };
198-
CallOptimizationResult handleCallVariant(Node* callTargetNode, Operand result, CallVariant, int registerOffset, VirtualRegister thisArgument, int argumentCountIncludingThis, BytecodeIndex osrExitIndex, InlineCallFrame::Kind, SpeculatedType prediction, unsigned& inliningBalance, BasicBlock* continuationBlock, bool needsToCheckCallee);
198+
CallOptimizationResult handleCallVariant(Node* callTargetNode, Operand result, CallVariant, int registerOffset, VirtualRegister thisArgument, int argumentCountIncludingThis, BytecodeIndex osrExitIndex, NodeType callOp, InlineCallFrame::Kind, SpeculatedType prediction, unsigned& inliningBalance, BasicBlock* continuationBlock, bool needsToCheckCallee);
199199
CallOptimizationResult handleInlining(Node* callTargetNode, Operand result, const CallLinkStatus&, int registerOffset, VirtualRegister thisArgument, int argumentCountIncludingThis, BytecodeIndex osrExitIndex, NodeType callOp, InlineCallFrame::Kind, SpeculatedType prediction, ECMAMode);
200200
template<typename ChecksFunctor>
201201
void inlineCall(Node* callTargetNode, Operand result, CallVariant, int registerOffset, int argumentCountIncludingThis, InlineCallFrame::Kind, BasicBlock* continuationBlock, const ChecksFunctor& insertChecks);
202202
// Handle intrinsic functions. Return true if it succeeded, false if we need to plant a call.
203203
template<typename ChecksFunctor>
204-
bool handleIntrinsicCall(Node* callee, Operand result, CallVariant, Intrinsic, int registerOffset, int argumentCountIncludingThis, SpeculatedType prediction, const ChecksFunctor& insertChecks);
204+
bool handleIntrinsicCall(Node* callee, Operand result, CallVariant, Intrinsic, int registerOffset, int argumentCountIncludingThis, NodeType callOp, SpeculatedType prediction, const ChecksFunctor& insertChecks);
205205
template<typename ChecksFunctor>
206206
bool handleDOMJITCall(Node* callee, Operand result, const DOMJIT::Signature*, int registerOffset, int argumentCountIncludingThis, SpeculatedType prediction, const ChecksFunctor& insertChecks);
207207
template<typename ChecksFunctor>
@@ -1889,7 +1889,7 @@ void ByteCodeParser::inlineCall(Node* callTargetNode, Operand result, CallVarian
18891889
m_currentInstruction = savedCurrentInstruction;
18901890
}
18911891

1892-
ByteCodeParser::CallOptimizationResult ByteCodeParser::handleCallVariant(Node* callTargetNode, Operand result, CallVariant callee, int registerOffset, VirtualRegister thisArgument, int argumentCountIncludingThis, BytecodeIndex osrExitIndex, InlineCallFrame::Kind kind, SpeculatedType prediction, unsigned& inliningBalance, BasicBlock* continuationBlock, bool needsToCheckCallee)
1892+
ByteCodeParser::CallOptimizationResult ByteCodeParser::handleCallVariant(Node* callTargetNode, Operand result, CallVariant callee, int registerOffset, VirtualRegister thisArgument, int argumentCountIncludingThis, BytecodeIndex osrExitIndex, NodeType callOp, InlineCallFrame::Kind kind, SpeculatedType prediction, unsigned& inliningBalance, BasicBlock* continuationBlock, bool needsToCheckCallee)
18931893
{
18941894
VERBOSE_LOG(" Considering callee ", callee, "\n");
18951895

@@ -1935,7 +1935,7 @@ ByteCodeParser::CallOptimizationResult ByteCodeParser::handleCallVariant(Node* c
19351935

19361936
Intrinsic intrinsic = callee.intrinsicFor(specializationKind);
19371937
if (intrinsic != NoIntrinsic) {
1938-
if (handleIntrinsicCall(callTargetNode, result, callee, intrinsic, registerOffset, argumentCountIncludingThis, prediction, insertChecksWithAccounting)) {
1938+
if (handleIntrinsicCall(callTargetNode, result, callee, intrinsic, registerOffset, argumentCountIncludingThis, callOp, prediction, insertChecksWithAccounting)) {
19391939
endSpecialCase();
19401940
return CallOptimizationResult::Inlined;
19411941
}
@@ -2138,7 +2138,7 @@ ByteCodeParser::CallOptimizationResult ByteCodeParser::handleInlining(
21382138
if (!callLinkStatus.couldTakeSlowPath() && callLinkStatus.size() == 1) {
21392139
return handleCallVariant(
21402140
callTargetNode, result, callLinkStatus[0], registerOffset, thisArgument,
2141-
argumentCountIncludingThis, osrExitIndex, kind, prediction, inliningBalance, nullptr, true);
2141+
argumentCountIncludingThis, osrExitIndex, callOp, kind, prediction, inliningBalance, nullptr, true);
21422142
}
21432143

21442144
// We need to create some kind of switch over callee. For now we only do this if we believe that
@@ -2228,7 +2228,7 @@ ByteCodeParser::CallOptimizationResult ByteCodeParser::handleInlining(
22282228

22292229
auto inliningResult = handleCallVariant(
22302230
myCallTargetNode, result, callLinkStatus[i], registerOffset,
2231-
thisArgument, argumentCountIncludingThis, osrExitIndex, kind, prediction,
2231+
thisArgument, argumentCountIncludingThis, osrExitIndex, callOp, kind, prediction,
22322232
inliningBalance, continuationBlock, false);
22332233

22342234
if (inliningResult == CallOptimizationResult::DidNothing) {
@@ -2327,9 +2327,10 @@ void ByteCodeParser::handleMinMax(Operand result, NodeType op, int registerOffse
23272327
}
23282328

23292329
template<typename ChecksFunctor>
2330-
bool ByteCodeParser::handleIntrinsicCall(Node* callee, Operand result, CallVariant variant, Intrinsic intrinsic, int registerOffset, int argumentCountIncludingThis, SpeculatedType prediction, const ChecksFunctor& insertChecks)
2330+
bool ByteCodeParser::handleIntrinsicCall(Node* callee, Operand result, CallVariant variant, Intrinsic intrinsic, int registerOffset, int argumentCountIncludingThis, NodeType callOp, SpeculatedType prediction, const ChecksFunctor& insertChecks)
23312331
{
23322332
VERBOSE_LOG(" The intrinsic is ", intrinsic, "\n");
2333+
UNUSED_PARAM(callOp);
23332334

23342335
if (!isOpcodeShape<OpCallShape>(m_currentInstruction)) {
23352336
VERBOSE_LOG(" Failing because instruction is not OpCallShape.\n");
@@ -3810,6 +3811,33 @@ bool ByteCodeParser::handleIntrinsicCall(Node* callee, Operand result, CallVaria
38103811
return true;
38113812
}
38123813

3814+
#if ENABLE(WEBASSEMBLY)
3815+
case WasmFunctionIntrinsic: {
3816+
if (callOp != Call)
3817+
return false;
3818+
if (m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadType))
3819+
return false;
3820+
if (m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadConstantValue))
3821+
return false;
3822+
if (!m_graph.m_plan.isFTL())
3823+
return false;
3824+
3825+
// We encourage CallWasm conversion by checking callee constant here.
3826+
// This allows strength reduction to fold this Call to CallWasm.
3827+
auto* function = variant.function();
3828+
if (!function)
3829+
return false;
3830+
3831+
insertChecks();
3832+
auto* frozenFunction = m_graph.freeze(function);
3833+
addToGraph(CheckIsConstant, OpInfo(frozenFunction), Edge(callee, CellUse));
3834+
RELEASE_ASSERT(!didSetResult);
3835+
addCall(result, callOp, OpInfo(), jsConstant(frozenFunction), argumentCountIncludingThis, registerOffset, prediction);
3836+
didSetResult = true;
3837+
return true;
3838+
}
3839+
#endif
3840+
38133841
default:
38143842
return false;
38153843
}

Source/JavaScriptCore/dfg/DFGClobberize.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
744744
case ConstructVarargs:
745745
case ConstructForwardVarargs:
746746
case CallDirectEval:
747+
case CallWasm:
747748
case ToPrimitive:
748749
case ToPropertyKey:
749750
case InByVal:

Source/JavaScriptCore/dfg/DFGDoesGC.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ bool doesGC(Graph& graph, Node* node)
290290
case DirectConstruct:
291291
case DirectTailCall:
292292
case DirectTailCallInlinedCaller:
293+
case CallWasm:
293294
case ForceOSRExit:
294295
case FunctionToString:
295296
case GetById:

0 commit comments

Comments
 (0)