Not long after contact with emscripten, I was very excited. I believe that the c/c++ code is definitely better compiled than the javascript script. In fact, c/c++ is indeed faster than javascript, but webassembly is not necessarily.
//file test.cpp
#include <emscripten/bind.h>
#include <emscripten/val.h>
class Test {
private:
int count = 0;
emscripten::val callback = emscripten::val::null();
public:
Test(const emscripten::val callback) {
this->callback = callback;
}
void addCount(int val) {
this->count += val;
if (this->count >= 10000 && this->callback.typeOf().as<std::string>() == "function") {
this->callback();
}
}
};
EMSCRIPTEN_BINDINGS(TEST) {
emscripten::class_<Test>("tester")
.constructor<emscripten::val>()
.function("addCount", &Test::addCount);
}
CMD:emcc --bind -Oz -s WASM=1 -s MODULARIZE=1 test.cpp -o out/test.js
//file test.js
const wasm = require("./test");
let callback = function () {
console.timeEnd(this);
};
wasm().then(dd => {
console.time("wasm");
let Tester = new dd.tester(callback.bind("wasm"));
for(let i=0;i<10000;i++){
Tester.addCount(1);
}
});
class Tester{
constructor(callback){
this.count = 0;
this.callback = callback;
}
addCount(val){
this.count+=val;
if(this.count>=10000&& typeof this.callback ==="function"){
this.callback();
}
}
}
let JST= new Tester(callback.bind("js"));
console.time("js");
for(let i=0;i<10000;i++){
JST.addCount(1);
}
CMD:cd out&&node test.js
js: 2.352ms
wasm: 7.147ms
This code is not the advantage of wasm. Maybe mutual calls between wasm and javascript affect performance. emscripten::val is a convenient feature. It seems to be deprecated for performance reasons. Is it a matter of many operations was wasm? Advantages, I hope experts can answer my doubts or have a better way to use emscripten::val.
//file test.c
int add(int a,int b){
return a+b;
}
CMD:emcc -Oz -s WASM=1 -s MODULARIZE=1 -s EXPORTED_FUNCTIONS='["_add"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' test.c -o out/test.js
//file test.js
const wasm = require("./test");
let callback = function () {
console.timeEnd(this);
};
wasm().then(dd => {
let add= dd.cwrap("add","number",["number","number"]);
console.time("wasm1");
add(1,1);
callback.bind("wasm1").call();
console.time("wasm100");
for(let i=0;i<100;i++){
add(1,1);
}
callback.bind("wasm100").call();
console.time("wasm1000");
for(let i=0;i<1000;i++){
add(1,1);
}
callback.bind("wasm1000").call();
console.time("wasm10000");
for(let i=0;i<10000;i++){
add(1,1);
}
callback.bind("wasm10000").call();
});
let add = function(a,b){
return a+b;
};
console.time("js1");
add(1,1);
callback.bind("js1").call();
console.time("js100");
for(let i=0;i<100;i++){
add(1,1);
}
callback.bind("js100").call();
console.time("js1000");
for(let i=0;i<1000;i++){
add(1,1);
}
callback.bind("js1000").call();
console.time("js10000");
for(let i=0;i<10000;i++){
add(1,1);
}
callback.bind("js10000").call();
CMD:cd out&&node.test.js
js1: 0.118ms
js100: 0.016ms
js1000: 0.056ms
js10000: 0.366ms
wasm1: 0.009ms
wasm100: 0.027ms
wasm1000: 0.175ms
wasm10000: 3.839ms
CMD:emcc -Oz -s WASM=1 -s ONLY_MY_CODE=1 -s EXPORTED_FUNCTIONS='["_add"]' test.c -o out/test.js
//file test.js
let callback = function () {
console.timeEnd(this);
};
//buffer test.wasm
WebAssembly.compile(new Uint8Array(`
00 61 73 6d 01 00 00 00 01 07 01 60 02 7f 7f 01
7f 03 02 01 00 07 08 01 04 5f 61 64 64 00 00 0a
09 01 07 00 20 01 20 00 6a 0b`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16))
)).then(module => {
const instance = new WebAssembly.Instance(module);
const { _add} = instance.exports;
let add= _add;
console.time("wasm1");
add(1,1);
callback.bind("wasm1").call();
console.time("wasm100");
for(let i=0;i<100;i++){
add(1,1);
}
callback.bind("wasm100").call();
console.time("wasm1000");
for(let i=0;i<1000;i++){
add(1,1);
}
callback.bind("wasm1000").call();
console.time("wasm10000");
for(let i=0;i<10000;i++){
add(1,1);
}
callback.bind("wasm10000").call();
});
let add = function(a,b){
return a+b;
};
console.time("js1");
add(1,1);
callback.bind("js1").call();
console.time("js100");
for(let i=0;i<100;i++){
add(1,1);
}
callback.bind("js100").call();
console.time("js1000");
for(let i=0;i<1000;i++){
add(1,1);
}
callback.bind("js1000").call();
console.time("js10000");
for(let i=0;i<10000;i++){
add(1,1);
}
callback.bind("js10000").call();
CMD:cd out&&node.test.js
js1: 0.145ms
js100: 0.016ms
js1000: 0.057ms
js10000: 0.502ms
wasm1: 0.006ms
wasm100: 0.011ms
wasm1000: 0.047ms
wasm10000: 0.287ms
It seems that test2 is the expected result, but I don't know why, maybe it's a code problem, maybe it's improper use. I hope experts can help or suggest how to avoid these problems. How to use wasm correctly to get a better experience
Use google translation, please forgive me for any errors
$emcc -v
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.37.36
clang version 5.0.0 (emscripten 1.37.36 : 1.37.36)
Target: x86_64-apple-darwin17.5.0
Thread model: posix
InstalledDir: /usr/local/opt/emscripten/libexec/llvm/bin
INFO:root:(Emscripten: Running sanity checks)
$node -v
v9.7.1
There's a few issues:
First, all of your tests are calling across the js-wasm boundary for each turn of the for loop, which is going to add way more overhead than you want.
Second, your slowest tests are being exacerbated by additional Emscripten code on the boundary. cwrap for example does argument conversion in a much more general way, which is apparently dominating your second example. If you were calling across the js-wasm boundary less, this would be less apparent.
Modifying your code slightly:
// many_adds.c
int add(int a, int b) {
return a + b;
}
int doManyAdds(int n) {
int sum = 0;
for (int i = 0; i < n; ++i) {
sum = add(sum, i);
}
return sum;
}
compile with emcc -Oz many_adds.c -o many_adds.js -s ONLY_MY_CODE=1 -s EXPORTED_FUNCTIONS='["_doManyAdds"]' -s MODULARIZE=1
//file run_many_adds.js
const wasm = require("./many_adds.js");
wasm().then(dd => {
// this binds directly to the doManyAdds function, without needing to instantiate the module inline,
// and without having to go through cwrap
let doManyAdds = dd._doManyAdds;
console.time("wasm1");
doManyAdds(1);
console.timeEnd("wasm1");
console.time("wasm100");
doManyAdds(100);
console.timeEnd("wasm100");
console.time("wasm1000");
doManyAdds(1000);
console.timeEnd("wasm1000");
console.time("wasm10000");
doManyAdds(10000);
console.timeEnd("wasm10000");
});
let add = function(a,b){
return a+b;
};
console.time("js1");
add(1,1);
console.timeEnd("js1");
console.time("js100");
for(let i=0;i<100;i++){
add(1,1);
}
console.timeEnd("js100");
console.time("js1000");
for(let i=0;i<1000;i++){
add(1,1);
}
console.timeEnd("js1000");
console.time("js10000");
for(let i=0;i<10000;i++){
add(1,1);
}
console.timeEnd("js10000");
Timing is:
js1: 0.136ms
js100: 0.015ms
js1000: 0.055ms
js10000: 0.496ms
wasm1: 0.086ms
wasm100: 0.020ms
wasm1000: 0.010ms
wasm10000: 0.020ms
Which is to say, the wasm is faster than the timer can accurately measure.
Amusingly, adding a few zeroes:
js100000: 4.757ms
js1000000: 3.841ms
js10000000: 12.208ms
wasm100000: 0.100ms
wasm1000000: 0.581ms
wasm10000000: 6.294ms
we see some strikingly non-linear performance from JS. This is because as we run the loop for longer, the JS engine is able to tier up the JIT and generate more optimal code. For n = 10k we see wasm outperform JS by a factor of ~25x. But for n = 10M, this settles to a factor of ~2x.
This is actually another advantage of wasm, because we get top-tier code generation for any size input, as well as being faster than the highest tier of JS JIT.
Most helpful comment
There's a few issues:
First, all of your tests are calling across the js-wasm boundary for each turn of the for loop, which is going to add way more overhead than you want.
Second, your slowest tests are being exacerbated by additional Emscripten code on the boundary.
cwrapfor example does argument conversion in a much more general way, which is apparently dominating your second example. If you were calling across the js-wasm boundary less, this would be less apparent.Modifying your code slightly:
compile with
emcc -Oz many_adds.c -o many_adds.js -s ONLY_MY_CODE=1 -s EXPORTED_FUNCTIONS='["_doManyAdds"]' -s MODULARIZE=1Timing is:
Which is to say, the wasm is faster than the timer can accurately measure.
Amusingly, adding a few zeroes:
we see some strikingly non-linear performance from JS. This is because as we run the loop for longer, the JS engine is able to tier up the JIT and generate more optimal code. For n = 10k we see wasm outperform JS by a factor of ~25x. But for n = 10M, this settles to a factor of ~2x.
This is actually another advantage of wasm, because we get top-tier code generation for any size input, as well as being faster than the highest tier of JS JIT.