Things get more complicated if your VM has a JIT, but there are interesting results even there, see pypy.js.
This approach lets you have arbitrary semantics, even ones that differ hugely from JavaScript, with decent performance.
I've suggested this in the past on HN - I think that approach could work for Dart as well. Would be happy to help investigate it.
You are comparing different ratios: munificent was comparing against JS and your "2 times" prediction is against native performance of the compiled VM.
> there are interesting results even there, see pypy.js.
Yes, the most interesting result is that you have to warm up pypy.js with a blow torch, otherwise performance is abysmal.
> I think that approach could work for Dart as well.
Have you seen people complaining about size of dart2js output? Can you imagine how big emscripten output would be?
I have been arguing that dart2js could have been using hand-written JIT compiler on the client side, but the startup performance compared to AOT would really be a big deal. I think a combination of AOT and JIT would be the best, but sadly it is also true that JavaScript lacks right level of abstraction. It is either too low-level (typed arrays, hand rolled allocations etc) or too high-level.
That sounds to me like it would just kill your startup perf. Users would have to download an entire Ruby VM every time they hit your site, wouldn't they?
Maybe I'm just a luddite, but spending network resources downloading a garbage collector written in JS only so that I can run it in JS... which natively supports GC just seems really gross to me.
Don't get me wrong, I think Emscripten is very very cool. It just feels like a strange fit for applications written in a language whose semantics aren't that far from JS.
Yes, it seems ironic to download a GC written in JS, when JS can do GC. But JS can only do SOME types of GC. For example, it lacks destructor callbacks, which things like Lua require. Some other language might need weakrefs which JS also lacks. So it is not quite that unreasonable to download a GC, as you can get the right semantics you want.
But I do agree it gets less clear when the semantics are very close to JS. I'm not sure if Ruby is close enough, though (CoffeeScript certainly is).
Also, a disclaimer in case anyone tries this themselves: there is a bunch of plaftorm/library code in the Dart output that's not used by such a trivial program and could be eliminated. The dart2js team is focusing of reducing the size of large programs, which do usually use this code, so eliminating it in small programs isn't much of a real-world win right now.
Note that the Opal code also has this.
I'd suggest that, while correct, your observation about types is a strawman--the generated Javascript (while quite slow!) follows the semantics of the Ruby language correctly; the addition operator is just a function, and with different context could've done something entirely differently. We could've, say, briefly overwritten the + operator on Fixnum or done silly alias_method tricks.
Exactly, the compiled code follows the Ruby language correctly. It's also unusably slow.
It's possible to generated compiled code that also follows the Ruby language semantics by using knowledge of which portions of those semantics the actual program in question relies on. That program can be a lot faster.
In other words, if you haven't overloaded arithmetic operators, your program compiled to JS shouldn't pay the runtime penalty as if you had.
That's a compiler's job: preserve semantics but make the resulting code run as fast as possible by whatever means necessary.
If you've got a way to compile Ruby to JS that has tolerable arithmetic perf and doesn't do type analysis or generate a huge volume of code, I would definitely like to know more about it.
Yeah, that's kinda the point, you can do that and preserve Ruby semantics?
Let me note here that is a strawman indeed, Dart has frozen classes, Ruby is a bit different. In order to do something like that you should inline a ton of ternary operator to check if the method has been redefined on that particular object.
Something like:
(a._isNumber && !a._plusRedefined) ? a + 1 : a['$+'](1)
plus listening on new method definitions to set `_plusRedefined`.It's definitively a trade off, readability will suffer.
Opal had optimized operators in the past (that assumed native operations on numbers), they could even be back in the near future. But what I see here is a comparison of two different language semantics. I agree that “compiling to JS is hard”, but I don't think that this demonstrates anything. What can be argued instead is that JS VMs could and should do that for us.
(My hunch is that your inlined check would actually be slower than just calling the method, actually. If you just do a method call, the inline caching will optimize it away in many cases.)
> I agree that “compiling to JS is hard”, but I don't think that this demonstrates anything.
All I was trying to demonstrate was just that. If you want a new language that runs in a browser, you can have:
1. Different (presumably better) semantics from JS.
2. A simple compiler that generates readable JS.
3. Performance competitive with raw JS.
But you only get to pick two. CoffeeScript picks 2 and 3 (though it does subset out some of the bad JS behavior). Dart picks 1 and 3. Opal (from the very little that I know about it) seems to be picking 1 and 2.By belief is that for a client-side language (i.e. a language that runs on the end user's hardware, which you don't control), that you don't have the luxury of sacrificing perf. You can in a server-side language since you can just throw more hardware at it. That's why languages like C++ still dominate client-side apps and games.
Given that, I don't think a language that targets the browser can really discard #3 and expect to get a decent number of users. Given that I love different languages, I'd be happy to be proven wrong here.
I want to stress, though, that I'm not singling Opal out here. I think Opal is a really cool project, and I hope it works well for lots of people. It's just a good example language since it's on HN right now.
Let's compile this Ruby code to JS with Opal:
Opal gives us: We'll compare it to some vanilla JS: I don't care at all that the generated code is a little funny looking. That's fine. Opal's code is actually pretty readable to me. What is a problem for some (many?) users is the performance.You'll note that Opal did not compile "a + 1" to "a + 1". Instead it generated "a['$+'](1)". That's because Ruby's arithmetic semantics are different from JavaScript's. To implement those semantics correctly, it needs to use a method call instead of using the built-in arithmetic.
We can profile the two using this fiddle: http://jsfiddle.net/3UtNf/1/
On my laptop, the Opal code is 264 times slower than the raw JS code. In other words, it runs at 0.3% of the speed of the JS code. Now imagine sacrificing that much perf on a mobile device. That's enough to make the language unsuitable for many real-world use cases.
This isn't intractable, though. You just need to compile math down to real JS arithmetic operators when JS's semantics line up with your language's (which typically means, when you're suring you've got numbers and not some other type with a user-defined operator).
Determining where you can do that is the hard part. It requires type analysis. Doing that well in a language that doesn't have a sound static type system requires whole-program analysis. It's extremely complex, monolithic, and leads to very strange output code.
This is why, for example, Dart's dart2js compiler is so complex and heavyweight. It does do this kind of analysis. It compiles this Dart program:
to this JS: That has the same performance as the JS code. The reason it does this is because it knows "a" is a number. If we change the Dart code to: Then generated JS changes completely: Interestingly, here you can see the compiler understood that the "+" operator on Foo always returns the same object and was able to inline the call to it and then hoist it out of the loop completely.This is the kind of stuff you need to do if you want to have a language that compiles to JS and (unlike, say CoffeeScript and TypeScript) has semantics that aren't very very similar to JS.