Skip to content

Commit fc310a6

Browse files
authored
Add an Intrinsics mechanism, and a call.without.effects intrinsic (#4126)
An "intrinsic" is modeled as a call to an import. We could also add new IR things for them, but that would take more work and lead to less clear errors in other tools if they try to read a binary using such a nonstandard extension. A first intrinsic is added here, call.without.effects This is basically the same as call_ref except that the optimizer is free to assume the call has no side effects. Consequently, if the result is not used then it can be optimized out (as even if it is not used then side effects could have kept it around). Likewise, the lack of side effects allows more reordering and other things. A lowering pass for intrinsics is provided. Rather than automatically lower them to normal wasm at the end of optimizations, the user must call that pass explicitly. A typical workflow might be -O --intrinsic-lowering -O That optimizes with the intrinsic present - perhaps removing calls thanks to it - then lowers it into normal wasm - it turns into a call_ref - and then optimizes further, which would turns the call_ref into a direct call, potentially inline, etc.
1 parent ea74b4f commit fc310a6

File tree

13 files changed

+540
-2
lines changed

13 files changed

+540
-2
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,46 @@ Notes when working with Binaryen IR:
141141
incorrectly.
142142
* For similar reasons, nodes should not appear in more than one functions.
143143

144+
### Intrinsics
145+
146+
Binaryen intrinsic functions look like calls to imports, e.g.,
147+
148+
```wat
149+
(import "binaryen-intrinsics" "foo" (func $foo))
150+
```
151+
152+
Implementing them that way allows them to be read and written by other tools,
153+
and it avoids confusing errors on a binary format error that could happen in
154+
those tools if we had a custom binary format extension.
155+
156+
An intrinsic method may be optimized away by the optimizer. If it is not, it
157+
must be **lowered** before shipping the wasm, as otherwise it will look like a
158+
call to an import that does not exist (and VMs will show an error on not having
159+
a proper value for that import). That final lowering is *not* done
160+
automatically. A user of intrinsics must run the pass for that explicitly,
161+
because the tools do not know when the user intends to finish optimizing, as the
162+
user may have a pipeline of multiple optimization steps, or may be doing local
163+
experimentation, or fuzzing/reducing, etc. Only the user knows when the final
164+
optimization happens before the wasm is "final" and ready to be shipped. Note
165+
that, in general, some additional optimizations may be possible after the final
166+
lowering, and so a useful pattern is to optimize once normally with intrinsics,
167+
then lower them away, then optimize after that, e.g.:
168+
169+
```
170+
wasm-opt input.wasm -o output.wasm -O --intrinsic-lowering -O
171+
```
172+
173+
Each intrinsic defines its semantics, which includes what the optimizer is
174+
allowed to do with it and what the final lowering will turn it to. See
175+
[intrinsics.h](https://github.com/WebAssembly/binaryen/blob/main/src/ir/intrinsics.h)
176+
for the detailed definitions. A quick summary appears here:
177+
178+
* `call.without.effects`: Similar to a `call_ref` in that it receives
179+
parameters, and a reference to a function to call, and calls that function
180+
with those parameters, except that the optimizer can assume the call has no
181+
side effects, and may be able to optimize it out (if it does not have a
182+
result that is used, generally).
183+
144184
## Tools
145185

146186
This repository contains code that builds the following tools in `bin/`:

src/ir/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ FILE(GLOB ir_HEADERS *.h)
22
set(ir_SOURCES
33
ExpressionAnalyzer.cpp
44
ExpressionManipulator.cpp
5+
intrinsics.cpp
56
names.cpp
67
properties.cpp
78
LocalGraph.cpp

src/ir/effects.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#ifndef wasm_ir_effects_h
1818
#define wasm_ir_effects_h
1919

20+
#include "ir/intrinsics.h"
2021
#include "pass.h"
2122
#include "wasm-traversal.h"
2223

@@ -31,7 +32,7 @@ class EffectAnalyzer {
3132
Expression* ast = nullptr)
3233
: ignoreImplicitTraps(passOptions.ignoreImplicitTraps),
3334
trapsNeverHappen(passOptions.trapsNeverHappen),
34-
debugInfo(passOptions.debugInfo), module(&module),
35+
debugInfo(passOptions.debugInfo), module(module),
3536
features(module.features) {
3637
if (ast) {
3738
walk(ast);
@@ -41,7 +42,7 @@ class EffectAnalyzer {
4142
bool ignoreImplicitTraps;
4243
bool trapsNeverHappen;
4344
bool debugInfo;
44-
Module* module;
45+
Module& module;
4546
FeatureSet features;
4647

4748
// Walk an expression and all its children.
@@ -393,6 +394,11 @@ class EffectAnalyzer {
393394
}
394395

395396
void visitCall(Call* curr) {
397+
// call.without.effects has no effects.
398+
if (Intrinsics(parent.module).isCallWithoutEffects(curr)) {
399+
return;
400+
}
401+
396402
parent.calls = true;
397403
// When EH is enabled, any call can throw.
398404
if (parent.features.hasExceptionHandling() && parent.tryDepth == 0) {

src/ir/intrinsics.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2021 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "ir/intrinsics.h"
18+
#include "wasm-builder.h"
19+
20+
namespace wasm {
21+
22+
static Name BinaryenIntrinsics("binaryen-intrinsics"),
23+
CallWithoutEffects("call.without.effects");
24+
25+
bool Intrinsics::isCallWithoutEffects(Function* func) {
26+
return func->module == BinaryenIntrinsics && func->base == CallWithoutEffects;
27+
}
28+
29+
Call* Intrinsics::isCallWithoutEffects(Expression* curr) {
30+
if (auto* call = curr->dynCast<Call>()) {
31+
// The target function may not exist if the module is still being
32+
// constructed.
33+
if (auto* func = module.getFunctionOrNull(call->target)) {
34+
if (isCallWithoutEffects(func)) {
35+
return call;
36+
}
37+
}
38+
}
39+
return nullptr;
40+
}
41+
42+
} // namespace wasm

src/ir/intrinsics.h

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2021 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef wasm_ir_intrinsics_h
18+
#define wasm_ir_intrinsics_h
19+
20+
#include "pass.h"
21+
#include "wasm-traversal.h"
22+
23+
//
24+
// See the README.md for background on intrinsic functions.
25+
//
26+
// Intrinsics can be recognized by Intrinsics::isFoo() methods, that check if a
27+
// function is a particular intrinsic, or if a call to a function is so. The
28+
// latter returns nullptr if the input is not that intrinsic, and otherwise the
29+
// intrinsic itself cast to a Call*.
30+
//
31+
32+
namespace wasm {
33+
34+
class Intrinsics {
35+
Module& module;
36+
37+
public:
38+
Intrinsics(Module& module) : module(module) {}
39+
40+
//
41+
// Check if an instruction is the call.without.effects intrinsic.
42+
//
43+
// (import "binaryen-intrinsics" "call.without.effects"
44+
// (func (..params..) (param $target funcref) (..results..)))
45+
//
46+
// call.without.effects can take any parameters, and in addition a funcref,
47+
// and return any result.
48+
//
49+
// Precise semantics:
50+
//
51+
// * The optimizer will assume this instruction has no side effects.
52+
// * Final lowering turns a call.without.effects into a call of the given
53+
// function with the given parameters. (This will either be a direct call,
54+
// or a call_ref; note that either way, the function reference that appears
55+
// here must have the proper type - if not, you will get an error.)
56+
//
57+
// call.without.effects is useful to be able to get rid of an unused result
58+
// that has side effects. For example,
59+
//
60+
// (drop (call $get-something))
61+
//
62+
// cannot be removed, as a call has side effects. But if a code generator
63+
// knows that it is fine to not make the call given that the result is
64+
// dropped (perhaps the side effects are to initialize a global cache, for
65+
// example) then instead of emitting
66+
//
67+
// (call $get-something)
68+
//
69+
// it can emit
70+
//
71+
// (call $call.without.effects (ref.func $get-something))
72+
//
73+
// which will have this behavior in the optimizer if it is dropped:
74+
//
75+
// (drop (call $call.without.effects (ref.func $get-something)))
76+
// =>
77+
// (drop (ref.func $get-something))
78+
//
79+
// Later optimizations can remove the dropped ref.func. Or, if the result is
80+
// actually used,
81+
//
82+
// (local.set $x (call $call.without.effects (ref.func $get-something)))
83+
// =>
84+
// (local.set $x (call $get-something))
85+
//
86+
// Later passes will then turn that into a direct call and further optimize
87+
// things.
88+
//
89+
bool isCallWithoutEffects(Function* func);
90+
Call* isCallWithoutEffects(Expression* curr);
91+
};
92+
93+
} // namespace wasm
94+
95+
#endif // wasm_ir_intrinsics_h

src/passes/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ set(passes_SOURCES
3535
Inlining.cpp
3636
InstrumentLocals.cpp
3737
InstrumentMemory.cpp
38+
Intrinsics.cpp
3839
LegalizeJSInterface.cpp
3940
LimitSegments.cpp
4041
LocalCSE.cpp

src/passes/Intrinsics.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2021 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "ir/intrinsics.h"
18+
#include "pass.h"
19+
#include "wasm-builder.h"
20+
#include "wasm.h"
21+
22+
namespace wasm {
23+
24+
struct IntrinsicLowering : public WalkerPass<PostWalker<IntrinsicLowering>> {
25+
bool isFunctionParallel() override { return true; }
26+
27+
Pass* create() override { return new IntrinsicLowering; }
28+
29+
void visitCall(Call* curr) {
30+
if (Intrinsics(*getModule()).isCallWithoutEffects(curr)) {
31+
// Turn into a call, by using the final operand as the function to call.
32+
auto& operands = curr->operands;
33+
auto* target = operands.back();
34+
operands.pop_back();
35+
// We could rely on later optimizations here, but at least ensure we emit
36+
// a direct call when we can, to avoid a performance cliff if the user
37+
// forgets to optimize.
38+
Builder builder(*getModule());
39+
if (auto* refFunc = target->dynCast<RefFunc>()) {
40+
replaceCurrent(builder.makeCall(refFunc->func, operands, curr->type));
41+
} else {
42+
replaceCurrent(builder.makeCallRef(target, operands, curr->type));
43+
}
44+
}
45+
}
46+
};
47+
48+
Pass* createIntrinsicLoweringPass() { return new IntrinsicLowering(); }
49+
50+
} // namespace wasm

src/passes/pass.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ void PassRegistry::registerPasses() {
164164
registerPass("inlining-optimizing",
165165
"inline functions and optimizes where we inlined",
166166
createInliningOptimizingPass);
167+
registerPass("intrinsic-lowering",
168+
"lower away binaryen intrinsics",
169+
createIntrinsicLoweringPass);
167170
registerPass("legalize-js-interface",
168171
"legalizes i64 types on the import/export boundary",
169172
createLegalizeJSInterfacePass);

src/passes/passes.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Pass* createLimitSegmentsPass();
6262
Pass* createLocalCSEPass();
6363
Pass* createLocalSubtypingPass();
6464
Pass* createLogExecutionPass();
65+
Pass* createIntrinsicLoweringPass();
6566
Pass* createInstrumentLocalsPass();
6667
Pass* createInstrumentMemoryPass();
6768
Pass* createLoopInvariantCodeMotionPass();

src/wasm/wasm-validator.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include "ir/features.h"
2323
#include "ir/global-utils.h"
24+
#include "ir/intrinsics.h"
2425
#include "ir/module-utils.h"
2526
#include "ir/stack-utils.h"
2627
#include "ir/utils.h"
@@ -2705,6 +2706,16 @@ static void validateImports(Module& module, ValidationInfo& info) {
27052706
"Imported function must not have i64 results");
27062707
}
27072708
}
2709+
2710+
if (Intrinsics(module).isCallWithoutEffects(curr)) {
2711+
auto lastParam = curr->getParams();
2712+
if (lastParam.isTuple()) {
2713+
lastParam = lastParam.getTuple().types.back();
2714+
}
2715+
info.shouldBeTrue(lastParam.isFunction(),
2716+
curr->name,
2717+
"call.if.used's last param must be a function");
2718+
}
27082719
});
27092720
ModuleUtils::iterImportedGlobals(module, [&](Global* curr) {
27102721
if (!module.features.hasMutableGlobals()) {

test/lit/help/optimization-opts.test

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@
284284
;; CHECK-NEXT: to intercept all loads and
285285
;; CHECK-NEXT: stores
286286
;; CHECK-NEXT:
287+
;; CHECK-NEXT: --intrinsic-lowering lower away binaryen intrinsics
288+
;; CHECK-NEXT:
287289
;; CHECK-NEXT: --legalize-js-interface legalizes i64 types on the
288290
;; CHECK-NEXT: import/export boundary
289291
;; CHECK-NEXT:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited.
2+
;; RUN: wasm-opt %s --intrinsic-lowering -all -S -o - | filecheck %s
3+
4+
(module
5+
;; CHECK: (type $none (func))
6+
(type $none (func))
7+
8+
;; call.without.effects with no params.
9+
;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe-v (param funcref) (result i32)))
10+
(import "binaryen-intrinsics" "call.without.effects" (func $cwe-v (param funcref) (result i32)))
11+
12+
;; call.without.effects with some params.
13+
;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe-dif (param f64 i32 funcref) (result f32)))
14+
(import "binaryen-intrinsics" "call.without.effects" (func $cwe-dif (param f64) (param i32) (param funcref) (result f32)))
15+
16+
;; call.without.effects with no result.
17+
;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe-n (param funcref)))
18+
(import "binaryen-intrinsics" "call.without.effects" (func $cwe-n (param funcref)))
19+
20+
;; CHECK: (func $test (result i32)
21+
;; CHECK-NEXT: (drop
22+
;; CHECK-NEXT: (call $test)
23+
;; CHECK-NEXT: )
24+
;; CHECK-NEXT: (drop
25+
;; CHECK-NEXT: (call $dif
26+
;; CHECK-NEXT: (f64.const 3.14159)
27+
;; CHECK-NEXT: (i32.const 42)
28+
;; CHECK-NEXT: )
29+
;; CHECK-NEXT: )
30+
;; CHECK-NEXT: (call_ref
31+
;; CHECK-NEXT: (ref.null $none)
32+
;; CHECK-NEXT: )
33+
;; CHECK-NEXT: (i32.const 1)
34+
;; CHECK-NEXT: )
35+
(func $test (result i32)
36+
;; These will be lowered into calls.
37+
(drop (call $cwe-v (ref.func $test)))
38+
(drop (call $cwe-dif (f64.const 3.14159) (i32.const 42) (ref.func $dif)))
39+
;; The last must be a call_ref, as we don't see a constant ref.func
40+
(call $cwe-n
41+
(ref.null $none)
42+
)
43+
(i32.const 1)
44+
)
45+
46+
;; CHECK: (func $dif (param $0 f64) (param $1 i32) (result f32)
47+
;; CHECK-NEXT: (unreachable)
48+
;; CHECK-NEXT: )
49+
(func $dif (param f64) (param i32) (result f32)
50+
;; Helper function for the above.
51+
(unreachable)
52+
)
53+
)

0 commit comments

Comments
 (0)