Live data from Hacker News

A first look at WebAssembly performance

stefankrause.net

101–110 of 130 posts

Re: A first look at WebAssembly performance

#101
post #66

The way I understand it, WebAssembly is all about the size of the binary and parsing overhead. Or, at a higher level, about enabling a level playing field between more languages than just JavaScript. Speed improvements from a common runtime and bytecode are certainly welcome, but if they are possible with WebAssembly, they are also be possible with plain JavaScript, and therefore shouldn't be visible in a comparison…

> WebAssembly is all about the size of the binary and parsing overhead. I don't think this is a good assessment. WebAssembly needs to pass through a complete compiler backend before it can be executed. WASM needs to be parsed, an in-memory representation is produced (probably LLVM IR), data flow and control flow graphs generated, a whole bunch of optimizations take place after which normal instruction selection, sche…

> I don't think this is a good assessment.

It's not so much an assessment as one of the main stated goals of the project. The initial point of WebAssembly is to do what asm.js does but with a smaller on-the-wire size and much less processing overhead to get from the on-the-wire format to executable code. And more cross-vendor buy-in, so the results are more reliable.

> an in-memory representation is produced (probably LLVM IR),

It's not LLVM IR in either Firefox or Chrome, fwiw. In the case of Firefox, it's the IR that Ion (the "fastest" tier of the JIT) uses. I expect this to be a pretty common approach across browsers, actually; it lets you leverage your existing code generation infrastructure.

Safari actually tried using using LLVM for this sort of thing. They stopped. See https://webkit.org/blog/5852/introducing-the-b3-jit-compiler... for details.

> This task is comparable in complexity to a JavaScript interpreter with a JIT compiler.

Not really. Once you've got the IR you just use your existing codepaths for it; the IR generation from webasm is a much simpler task than IR generation from JS, complete with runtime instrumentation and whatnot.

Re: A first look at WebAssembly performance

#102

Earlier quoted context omitted.

> Speed improvements from a common runtime and bytecode are certainly welcome, but if they are possible with WebAssembly, they are also be possible with plain JavaScript Nope, a static language will execute faster than a dynamic language, because the runtime knows precisely what type everything is and how much space to allocate. Additionally no faffing around with dictionaries for dynamic types. Currently JS engines…

That's not really true, as JS engines will already compile things with the assumption that the types won't change and "bail out" if they do. So if you can "hint" to the JIT that a variable is an integer, and it will stay an integer, then it will not only "unbox" it and treat it as an integer, it will compile the code very similar to how a static language would. In asm.js, this is done by using little "tricks" of JS t…

Integers are hardly the worry. Any custom types the user makes are the problem.

Re: A first look at WebAssembly performance

#103
post #80

Earlier quoted context omitted.

Exactly... Now, that alone could make it tricky to get a factor 60 difference.

Write a simplistic O(N^3) algortihm When N is 100, all fixed costs in the inner loop are multiplied by 1,000,000. So a nanosecond difference is now a millisecond. It's pretty easy to get worse than that by boneheaded data layout (row-major vs column-major is pretty common)

No...

If you use a "bonehead" data structure each memory reference will force a cache-miss, i.e. taking q CPU cycles instead of 1 CPU cycles.

If the algorithms has a running time of O(N^3), it means it is using some number of instructions Now, if each operation takes q CPU cycles instead of 1 cycles due to cache-miss on every operation, the running time of your algorithm is still bounded by q * (c * N^3) operations.

For the example with N=100 -> The fast version takes c * 1,000,000 operations to complete, for some value c. For the slow version, as each operation is now q times slower, it will take q * c * 1,000,000 operations to complete for values q and c.

I.e. it takes q times as long, not q * q * q.

What you were saying is that, if you run a O(N) algorithm on a 100 MHZ CPU, it will take 10 times as long as on a 1000 MHZ CPU, but an O(N^3) algorithm would take 1000 times as long. And that is obviously, not true. Both will take 10 times longer.

Re: A first look at WebAssembly performance

#104

Earlier quoted context omitted.

That's a bold claim; Sure it is theoretically possible, to have two versions of the same C program take either 500ms or 8ms purely due to memory layout. But I would like to challenge you to actually do it! I.e. same number of calculations, on same amount of data and a factor of 60 run time difference, with only the memory layout as actual difference between the two implementations. Up for it?

I'll take that challenge. Here's how to do it, with a little bit of sneaky interpretation of 'memory layout': have an algorithm which takes a large struct S and looks at a subset Sf to determine what to do with S, for some value of Sf use all of S, otherwise skip it. (e.g. when distance between 2 particles Now have a very low pass-rate for the filter so that total time ~= time taken to read all of Sf. Make Sf a singl…

Sorry, I don't quite understand the explanation...

Do you have some simple pseudo-code?

And are you using the same number of instructions in both cases?

Note also: If you are moving data structures of different sizes, you are not using the same number of instructions(calculations), as the challenge required.

Re: A first look at WebAssembly performance

#105

Am I the only one that was expecting web assembly to be like 10x JS speeds?

Well remember, WebAssembly just a different representation format for the same instruction set, executed by the underlying virtual machine (v8, spidermonkey, etc). So you should expect to see much of a performance difference. The main benefit of WebAssembly is so that we can write code using our favorite languages (not just javascript!) and compile to a common binary format the browser understands. The objectives of…

WebAssembly is tied to asm.js, which is about performance.

Re: A first look at WebAssembly performance

#106
post #80

Earlier quoted context omitted.

Write a simplistic O(N^3) algortihm When N is 100, all fixed costs in the inner loop are multiplied by 1,000,000. So a nanosecond difference is now a millisecond. It's pretty easy to get worse than that by boneheaded data layout (row-major vs column-major is pretty common)

No... If you use a "bonehead" data structure each memory reference will force a cache-miss, i.e. taking q CPU cycles instead of 1 CPU cycles. If the algorithms has a running time of O(N^3), it means it is using some number of instructions Now, if each operation takes q CPU cycles instead of 1 cycles due to cache-miss on every operation, the running time of your algorithm is still bounded by q * (c * N^3) operations.…

No

I'm saying that a poor choice of data structure will have an amplified impact on an O(N^3) algorithm.

Giving it as an example of where you could have the different implementations of the same algorithm have massively different runtimes on the same processor.

Re: A first look at WebAssembly performance

#107

Earlier quoted context omitted.

That's not really true, as JS engines will already compile things with the assumption that the types won't change and "bail out" if they do. So if you can "hint" to the JIT that a variable is an integer, and it will stay an integer, then it will not only "unbox" it and treat it as an integer, it will compile the code very similar to how a static language would. In asm.js, this is done by using little "tricks" of JS t…

Integers are hardly the worry. Any custom types the user makes are the problem.

Integers are the worry.

the JIT being able to say "This here is an int, which means that this function takes an in and returns an int always, i'm going to compile it that way" means that now that function is "C-Speed".

If your program were filled with functions similar to that where the compiler is easily able to optimize the shit out of it, then it will run suprisingly close to "C-Speed".

Custom types (which I took to mean different data storage formats) aren't really something that the compiler can help with (any compiler). If you use a hashmap, you've got a hashmap, if you use a doubly-linked-list you've got a doubly-linked-list. C or JS iterating over an entire hashmap is going to be slower than iterating over memory addresses.

But luckily JS has stuff like TypedArrays which are basically C arrays with bounds checking.

And if you loop over it in a way that the compiler can determine you won't go out of bounds (say, with a simple for loop that runs while x So you "can" (technically, on paper) make a JS program that would run at C speeds in most cases, but at the end of the day some part of your stack is going to begin using the dynamic parts of the language, and then you get the big speed roadblocks again. (by the way, the above is basically what ASM.js is, a well curated subset of JS which doesn't allow anything dynamic so that the JIT can compile it to run at "C speeds")

The benefits of a static language aren't in what they allow, but what they don't. Tying your hands and saying "No, you can't make an array of half strings and half integers" lets the compiler be simpler and not have to worry about those "edge cases", which means that it can focus on the "fast path" more. (in practice this almost always shows up as a faster language because JITs aren't perfect, so they can get confused by code that could technically be compiled more optimally but won't because the JIT can't identify it)

It's also why adding optional static typing to a language won't make it faster on it's own (after all, the JIT already knows what types variables are at calling time, telling it that again won't solve anything...)

Re: A first look at WebAssembly performance

#108
post #106

Earlier quoted context omitted.

No... If you use a "bonehead" data structure each memory reference will force a cache-miss, i.e. taking q CPU cycles instead of 1 CPU cycles. If the algorithms has a running time of O(N^3), it means it is using some number of instructions Now, if each operation takes q CPU cycles instead of 1 cycles due to cache-miss on every operation, the running time of your algorithm is still bounded by q * (c * N^3) operations.…

No I'm saying that a poor choice of data structure will have an amplified impact on an O(N^3) algorithm. Giving it as an example of where you could have the different implementations of the same algorithm have massively different runtimes on the same processor.

If the algorithm has a running time of O(N^3), that implies that it takes O(N^3) If we disregard c, and set N = 100, that is 1,000,000 operations.

We have two situations. 1) Either we use a good layout where each operation is fast because each memory access is from cache. 2) Or we use a bad layout where each operation is slow because we have a cache-miss and have to access the RAM. These access times are independent of the specific algorithm, and only depends on CPU architecture.

Let say that the fast operations each take 1 second. And the slow operations each take f seconds.

In the fast case, with good memory layout, the algorithm completes in 1,000,000 seconds. In the slow case, with bad memory layout, the algorithm completes in f * 1,000,000 seconds.

The time of each operation we perform does not depend on the size of N. However, in practice, you'll typically have a lot of cache hits even with a bad memory layout, so the difference may be less than a factor of f for smaller values of N (where you'll have more cache hits in a cubic algorithm). However, it will be upper bounded by f. I.e. there is an upper bound on how much slower you can make an algorithm by changing the memory layout.

Re: A first look at WebAssembly performance

#109

Earlier quoted context omitted.

I'll take that challenge. Here's how to do it, with a little bit of sneaky interpretation of 'memory layout': have an algorithm which takes a large struct S and looks at a subset Sf to determine what to do with S, for some value of Sf use all of S, otherwise skip it. (e.g. when distance between 2 particles Now have a very low pass-rate for the filter so that total time ~= time taken to read all of Sf. Make Sf a singl…

Sorry, I don't quite understand the explanation... Do you have some simple pseudo-code? And are you using the same number of instructions in both cases? Note also: If you are moving data structures of different sizes, you are not using the same number of instructions(calculations), as the challenge required.

>Sorry, I don't quite understand the explanation...

define a

  struct S{char flag, data[63];}
  
then do

  foreach s in std::vector(big number){
    if s.flag == 0{ //set this variable to be very rarely 0
     do_something_with(s.data);
    }
this will load the entire struct into memory each iteration because loads from memory happen on cacheline granularity (i.e. 64 bytes at a time)

then the optimized version is

>And are you using the same number of instructions in both cases?

  //define S as struct of arrays now
  struct S{char* flags, (*data)[63];}
  s = initialize_S(big number);
  for(i = 0; i 
This one will load 64 flags into the L1 cache at a time. because the L1 cache is much much faster than main memory access time will be ~64x faster for the checking of the flags. Because it is rare for the loop to enter the if, this means the overal speed will be ~64x faster. As proven by the graph I posted where it is about 16x faster (because I use an int instead of a byte as flag).

Syntax is slightly different between SoA and AoS, but functionally they are the same except for how multiple S's are laid out in memory.

>And are you using the same number of instructions in both cases? >Note also: If you are moving data structures of different sizes, you are not using the same number of instructions(calculations), as the challenge required.

The same number of instructions is an arbitrary and impossible task. I'm not going to handwrite assembly for some internet points, I only do that for fun ;). Do they perform the same amount of cpu work though, yes. They are functionally the same and use the same types of instructions. That's about as good as anyone can get it. In any case, the program is completely limited by memory throughput not computation. I could make one of the inner loops a sqrt and the other a nop and the difference would be the exact same.

If you really want to see it in assembly to prove it uses the same amount of instructions, then it's just a mov rax, [rcx], cmp rax, r8, je INNER_FUNCTION, inc rcx {sizeof(S),1} depending on the method. Add 0x90 where appropriate to match instruction counts. The rest is left as an exercise to the reader.

Re: A first look at WebAssembly performance

#110

Earlier quoted context omitted.

> Speed improvements from a common runtime and bytecode are certainly welcome, but if they are possible with WebAssembly, they are also be possible with plain JavaScript Nope, a static language will execute faster than a dynamic language, because the runtime knows precisely what type everything is and how much space to allocate. Additionally no faffing around with dictionaries for dynamic types. Currently JS engines…

That's not really true, as JS engines will already compile things with the assumption that the types won't change and "bail out" if they do. So if you can "hint" to the JIT that a variable is an integer, and it will stay an integer, then it will not only "unbox" it and treat it as an integer, it will compile the code very similar to how a static language would. In asm.js, this is done by using little "tricks" of JS t…

> That's not really true, as JS engines will already compile things with the assumption that the types won't change and "bail out" if they do.

Which makes for some great benchmarks but poor real world results.

Post reply on HN