Skip to content

Commit c3ed0f1

Browse files
authored
wasm2js: more js optimization (#2050)
* Emit ints as signed, so -1 isn't a big unsigned number. * x - -c (where c is a constant) is larger than x + c in js (but not wasm) * +(+x) => +x * Avoid unnecessary coercions on calls, return, load, etc. - we just need coercions when entering or exiting "wasm" (not internally), and on actual operations that need them.
1 parent bc0a605 commit c3ed0f1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1568
-2465
lines changed

src/binaryen-c.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2469,7 +2469,7 @@ void BinaryenModulePrintAsmjs(BinaryenModuleRef module) {
24692469

24702470
Module* wasm = (Module*)module;
24712471
Wasm2JSBuilder::Flags flags;
2472-
Wasm2JSBuilder wasm2js(flags);
2472+
Wasm2JSBuilder wasm2js(flags, globalPassOptions);
24732473
Ref asmjs = wasm2js.processWasm(wasm);
24742474
JSPrinter jser(true, true, asmjs);
24752475
Output out("", Flags::Text, Flags::Release); // stdout

src/emscripten-optimizer/simple_ast.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,9 @@ class ValueBuilder {
14371437
static Ref makeInt(uint32_t num) {
14381438
return makeDouble(double(num));
14391439
}
1440+
static Ref makeInt(int32_t num) {
1441+
return makeDouble(double(num));
1442+
}
14401443
static Ref makeNum(double num) {
14411444
return makeDouble(num);
14421445
}

src/tools/wasm2js.cpp

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "wasm-s-parser.h"
2525
#include "wasm2js.h"
2626
#include "optimization-options.h"
27+
#include "pass.h"
2728

2829
using namespace cashew;
2930
using namespace wasm;
@@ -32,6 +33,33 @@ using namespace wasm;
3233

3334
namespace {
3435

36+
static void optimizeWasm(Module& wasm, PassOptions options) {
37+
// Perform various optimizations that will be good for JS, but would not be great
38+
// for wasm in general
39+
struct OptimizeForJS : public WalkerPass<PostWalker<OptimizeForJS>> {
40+
bool isFunctionParallel() override { return true; }
41+
42+
Pass* create() override { return new OptimizeForJS; }
43+
44+
void visitBinary(Binary* curr) {
45+
// x - -c (where c is a constant) is larger than x + c, in js (but not
46+
// necessarily in wasm, where LEBs prefer negatives).
47+
if (curr->op == SubInt32) {
48+
if (auto* c = curr->right->dynCast<Const>()) {
49+
if (c->value.geti32() < 0) {
50+
curr->op = AddInt32;
51+
c->value = c->value.neg();
52+
}
53+
}
54+
}
55+
}
56+
};
57+
58+
PassRunner runner(&wasm, options);
59+
runner.add<OptimizeForJS>();
60+
runner.run();
61+
}
62+
3563
template<typename T>
3664
static void printJS(Ref ast, T& output) {
3765
JSPrinter jser(true, true, ast);
@@ -41,10 +69,15 @@ static void printJS(Ref ast, T& output) {
4169

4270
static void optimizeJS(Ref ast) {
4371
// helpers
72+
4473
auto isOrZero = [](Ref node) {
4574
return node->isArray() && node->size() > 0 && node[0] == BINARY && node[1] == OR && node[3]->isNumber() && node[3]->getNumber() == 0;
4675
};
4776

77+
auto isPlus = [](Ref node) {
78+
return node->isArray() && node->size() > 0 && node[0] == UNARY_PREFIX && node[1] == PLUS;
79+
};
80+
4881
auto isBitwise = [](Ref node) {
4982
if (node->isArray() && node->size() > 0 && node[0] == BINARY) {
5083
auto op = node[1];
@@ -84,13 +117,22 @@ static void optimizeJS(Ref ast) {
84117
node[3] = node[3][2];
85118
}
86119
}
120+
// +(+x) => +x
121+
else if (isPlus(node)) {
122+
while (isPlus(node[2])) {
123+
node[2] = node[2][2];
124+
}
125+
}
87126
});
88127
}
89128

90-
static void emitWasm(Module& wasm, Output& output, Wasm2JSBuilder::Flags flags, Name name, bool optimize=false) {
91-
Wasm2JSBuilder wasm2js(flags);
129+
static void emitWasm(Module& wasm, Output& output, Wasm2JSBuilder::Flags flags, PassOptions options, Name name) {
130+
if (options.optimizeLevel > 0) {
131+
optimizeWasm(wasm, options);
132+
}
133+
Wasm2JSBuilder wasm2js(flags, options);
92134
auto js = wasm2js.processWasm(&wasm, name);
93-
if (optimize) {
135+
if (options.optimizeLevel >= 2) {
94136
optimizeJS(js);
95137
}
96138
Wasm2JSGlue glue(wasm, output, flags, name);
@@ -104,7 +146,8 @@ class AssertionEmitter {
104146
AssertionEmitter(Element& root,
105147
SExpressionWasmBuilder& sexpBuilder,
106148
Output& out,
107-
Wasm2JSBuilder::Flags flags) : root(root), sexpBuilder(sexpBuilder), out(out), flags(flags) {}
149+
Wasm2JSBuilder::Flags flags,
150+
PassOptions options) : root(root), sexpBuilder(sexpBuilder), out(out), flags(flags), options(options) {}
108151

109152
void emit();
110153

@@ -113,6 +156,7 @@ class AssertionEmitter {
113156
SExpressionWasmBuilder& sexpBuilder;
114157
Output& out;
115158
Wasm2JSBuilder::Flags flags;
159+
PassOptions options;
116160
Module tempAllocationModule;
117161

118162
Ref emitAssertReturnFunc(Builder& wasmBuilder,
@@ -131,7 +175,7 @@ class AssertionEmitter {
131175
void fixCalls(Ref asmjs, Name asmModule);
132176

133177
Ref processFunction(Function* func) {
134-
Wasm2JSBuilder sub(flags);
178+
Wasm2JSBuilder sub(flags, options);
135179
return sub.processStandaloneFunction(&tempAllocationModule, func);
136180
}
137181

@@ -370,7 +414,7 @@ void AssertionEmitter::emit() {
370414
asmModule = Name(moduleNameS.str().c_str());
371415
Module wasm;
372416
SExpressionWasmBuilder builder(wasm, e);
373-
emitWasm(wasm, out, flags, funcName);
417+
emitWasm(wasm, out, flags, options, funcName);
374418
continue;
375419
}
376420
if (!isAssertHandled(e)) {
@@ -492,9 +536,9 @@ int main(int argc, const char *argv[]) {
492536
if (options.debug) std::cerr << "j-printing..." << std::endl;
493537
Output output(options.extra["output"], Flags::Text, options.debug ? Flags::Debug : Flags::Release);
494538
if (!binaryInput && options.extra["asserts"] == "1") {
495-
AssertionEmitter(*root, *sexprBuilder, output, flags).emit();
539+
AssertionEmitter(*root, *sexprBuilder, output, flags, options.passOptions).emit();
496540
} else {
497-
emitWasm(wasm, output, flags, "asmFunc", options.passOptions.optimizeLevel > 0);
541+
emitWasm(wasm, output, flags, options.passOptions, "asmFunc");
498542
}
499543

500544
if (options.debug) std::cerr << "done." << std::endl;

src/wasm2js.h

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class Wasm2JSBuilder {
126126
bool emscripten = false;
127127
};
128128

129-
Wasm2JSBuilder(Flags f) : flags(f) {}
129+
Wasm2JSBuilder(Flags f, PassOptions options) : flags(f), options(options) {}
130130

131131
Ref processWasm(Module* wasm, Name funcName = ASM_FUNC);
132132
Ref processFunction(Module* wasm, Function* func, bool standalone=false);
@@ -136,7 +136,7 @@ class Wasm2JSBuilder {
136136

137137
// The second pass on an expression: process it fully, generating
138138
// JS
139-
Ref processFunctionBody(Module* m, Function* func);
139+
Ref processFunctionBody(Module* m, Function* func, bool standalone);
140140

141141
// Get a temp var.
142142
IString getTemp(Type type, Function* func) {
@@ -225,6 +225,7 @@ class Wasm2JSBuilder {
225225

226226
private:
227227
Flags flags;
228+
PassOptions options;
228229

229230
// How many temp vars we need
230231
std::vector<size_t> temps; // type => num temps
@@ -238,6 +239,11 @@ class Wasm2JSBuilder {
238239

239240
size_t tableSize;
240241

242+
// If a function is callable from outside, we'll need to cast the inputs
243+
// and our return value. Otherwise, internally, casts are only needed
244+
// on operations.
245+
std::unordered_set<Name> functionsCallableFromOutside;
246+
241247
void addBasics(Ref ast);
242248
void addFunctionImport(Ref ast, Function* import);
243249
void addGlobalImport(Ref ast, Global* import);
@@ -252,6 +258,18 @@ class Wasm2JSBuilder {
252258
};
253259

254260
Ref Wasm2JSBuilder::processWasm(Module* wasm, Name funcName) {
261+
// Scan the wasm for important things.
262+
for (auto& exp : wasm->exports) {
263+
if (exp->kind == ExternalKind::Function) {
264+
functionsCallableFromOutside.insert(exp->value);
265+
}
266+
}
267+
for (auto& segment : wasm->table.segments) {
268+
for (auto name : segment.data) {
269+
functionsCallableFromOutside.insert(name);
270+
}
271+
}
272+
255273
// Ensure the scratch memory helpers.
256274
// If later on they aren't needed, we'll clean them up.
257275
ABI::wasm2js::ensureScratchMemoryHelpers(wasm);
@@ -645,26 +663,29 @@ Ref Wasm2JSBuilder::processFunction(Module* m, Function* func, bool standaloneFu
645663
temps.resize(std::max(i32, std::max(f32, f64)) + 1);
646664
temps[i32] = temps[f32] = temps[f64] = 0;
647665
// arguments
666+
bool needCoercions = options.optimizeLevel == 0 || standaloneFunction || functionsCallableFromOutside.count(func->name);
648667
for (Index i = 0; i < func->getNumParams(); i++) {
649668
IString name = fromName(func->getLocalNameOrGeneric(i), NameScope::Local);
650669
ValueBuilder::appendArgumentToFunction(ret, name);
651-
ret[3]->push_back(
652-
ValueBuilder::makeStatement(
653-
ValueBuilder::makeBinary(
654-
ValueBuilder::makeName(name), SET,
655-
makeAsmCoercion(
656-
ValueBuilder::makeName(name),
657-
wasmToAsmType(func->getLocalType(i))
670+
if (needCoercions) {
671+
ret[3]->push_back(
672+
ValueBuilder::makeStatement(
673+
ValueBuilder::makeBinary(
674+
ValueBuilder::makeName(name), SET,
675+
makeAsmCoercion(
676+
ValueBuilder::makeName(name),
677+
wasmToAsmType(func->getLocalType(i))
678+
)
658679
)
659680
)
660-
)
661-
);
681+
);
682+
}
662683
}
663684
Ref theVar = ValueBuilder::makeVar();
664685
size_t theVarIndex = ret[3]->size();
665686
ret[3]->push_back(theVar);
666687
// body
667-
flattenAppend(ret, processFunctionBody(m, func));
688+
flattenAppend(ret, processFunctionBody(m, func, standaloneFunction));
668689
// vars, including new temp vars
669690
for (Index i = func->getVarIndexBase(); i < func->getNumLocals(); i++) {
670691
ValueBuilder::appendToVar(
@@ -683,15 +704,17 @@ Ref Wasm2JSBuilder::processFunction(Module* m, Function* func, bool standaloneFu
683704
return ret;
684705
}
685706

686-
Ref Wasm2JSBuilder::processFunctionBody(Module* m, Function* func) {
707+
Ref Wasm2JSBuilder::processFunctionBody(Module* m, Function* func, bool standaloneFunction) {
687708
struct ExpressionProcessor : public Visitor<ExpressionProcessor, Ref> {
688709
Wasm2JSBuilder* parent;
689710
IString result; // TODO: remove
690711
Function* func;
691712
Module* module;
713+
bool standaloneFunction;
692714
MixedArena allocator;
693-
ExpressionProcessor(Wasm2JSBuilder* parent, Module* m, Function* func)
694-
: parent(parent), func(func), module(m) {}
715+
716+
ExpressionProcessor(Wasm2JSBuilder* parent, Module* m, Function* func, bool standaloneFunction)
717+
: parent(parent), func(func), module(m), standaloneFunction(standaloneFunction) {}
695718

696719
// A scoped temporary variable.
697720
struct ScopedTemp {
@@ -847,12 +870,19 @@ Ref Wasm2JSBuilder::processFunctionBody(Module* m, Function* func) {
847870

848871
Ref visitCall(Call* curr) {
849872
Ref theCall = ValueBuilder::makeCall(fromName(curr->target, NameScope::Top));
873+
// For wasm => wasm calls, we don't need coercions. TODO: even imports might be safe?
874+
bool needCoercions = parent->options.optimizeLevel == 0 || standaloneFunction || module->getFunction(curr->target)->imported();
850875
for (auto operand : curr->operands) {
851-
theCall[2]->push_back(
852-
makeAsmCoercion(visit(operand, EXPRESSION_RESULT),
853-
wasmToAsmType(operand->type)));
876+
auto value = visit(operand, EXPRESSION_RESULT);
877+
if (needCoercions) {
878+
value = makeAsmCoercion(value, wasmToAsmType(operand->type));
879+
}
880+
theCall[2]->push_back(value);
881+
}
882+
if (needCoercions) {
883+
theCall = makeAsmCoercion(theCall, wasmToAsmType(curr->type));
854884
}
855-
return makeAsmCoercion(theCall, wasmToAsmType(curr->type));
885+
return theCall;
856886
}
857887

858888
Ref visitCallIndirect(CallIndirect* curr) {
@@ -992,7 +1022,14 @@ Ref Wasm2JSBuilder::processFunctionBody(Module* m, Function* func) {
9921022
abort();
9931023
}
9941024
}
995-
return makeAsmCoercion(ret, wasmToAsmType(curr->type));
1025+
// Coercions are not actually needed, as if the user reads beyond valid memory, it's
1026+
// undefined behavior anyhow, and so we don't care much about slowness of undefined
1027+
// values etc.
1028+
bool needCoercions = parent->options.optimizeLevel == 0 || standaloneFunction;
1029+
if (needCoercions) {
1030+
ret = makeAsmCoercion(ret, wasmToAsmType(curr->type));
1031+
}
1032+
return ret;
9961033
}
9971034

9981035
Ref visitStore(Store* curr) {
@@ -1521,10 +1558,11 @@ Ref Wasm2JSBuilder::processFunctionBody(Module* m, Function* func) {
15211558
if (!curr->value) {
15221559
return ValueBuilder::makeReturn(Ref());
15231560
}
1524-
Ref val = makeAsmCoercion(
1525-
visit(curr->value, EXPRESSION_RESULT),
1526-
wasmToAsmType(curr->value->type)
1527-
);
1561+
Ref val = visit(curr->value, EXPRESSION_RESULT);
1562+
bool needCoercion = parent->options.optimizeLevel == 0 || standaloneFunction || parent->functionsCallableFromOutside.count(func->name);
1563+
if (needCoercion) {
1564+
val = makeAsmCoercion(val, wasmToAsmType(curr->value->type));
1565+
}
15281566
return ValueBuilder::makeReturn(val);
15291567
}
15301568

@@ -1564,7 +1602,7 @@ Ref Wasm2JSBuilder::processFunctionBody(Module* m, Function* func) {
15641602
}
15651603
};
15661604

1567-
return ExpressionProcessor(this, m, func).visit(func->body, NO_RESULT);
1605+
return ExpressionProcessor(this, m, func, standaloneFunction).visit(func->body, NO_RESULT);
15681606
}
15691607

15701608
void Wasm2JSBuilder::addMemoryGrowthFuncs(Ref ast, Module* wasm) {

test/wasm2js.traps.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function check3() {
105105
if (!check3()) throw 'assertion failed: ( assert_trap ( call div_s ( i32.const 0 ) ( i32.const 0 ) ) integer divide by zero )';
106106
function check4() {
107107
function f() {
108-
retasmFunc0.div_s(2147483648 | 0, 4294967295 | 0);
108+
retasmFunc0.div_s(-2147483648 | 0, -1 | 0);
109109
}
110110

111111
try {

test/wasm2js/address.2asm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function asmFunc(global, env, buffer) {
4343

4444
function $1(i) {
4545
i = i | 0;
46-
HEAP32[i + 4294967295 >> 2] | 0;
46+
HEAP32[i + 4294967295 >> 2];
4747
}
4848

4949
var FUNCTION_TABLE = [];

test/wasm2js/block.2asm.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function asmFunc(global, env, buffer) {
5757

5858
function $6() {
5959
dummy();
60-
return __wasm_ctz_i32(13 | 0) | 0;
60+
return __wasm_ctz_i32(13) | 0;
6161
}
6262

6363
function $7() {
@@ -159,15 +159,14 @@ function asmFunc(global, env, buffer) {
159159
$0 = Math_imul($0, 7);
160160
break block;
161161
}
162-
return ($0 | 0) == (4294967282 | 0) | 0;
162+
return ($0 | 0) == (-14 | 0) | 0;
163163
}
164164

165165
function __wasm_ctz_i32(var$0) {
166-
var$0 = var$0 | 0;
167166
if (var$0) {
168-
return 31 - Math_clz32(var$0 + 4294967295 ^ var$0) | 0
167+
return 31 - Math_clz32(var$0 + -1 ^ var$0) | 0
169168
}
170-
return 32 | 0;
169+
return 32;
171170
}
172171

173172
var FUNCTION_TABLE = [];

0 commit comments

Comments
 (0)