12 ms·
Performance Showdown: Rust vs Javascript (2020)
- chovybizzass 6y agoso which is it?
- OneAlias1 6y agoRust, no surprises here.
- johannes1234321 6y agoThat isn't the point of the article though. More like: If you tune your code Rust (and any native language) is faster, with naive code however there is a good chance that JavaScript can be faster.
- OneAlias1 6y ago'After spending some weeks playing with Rust,'... he is new to Rust and keeps using String, accepting `&String` as args etc this is not tuning. This would be caught by linters like clippy telling you to double check it. I can show you naive Rust vs Naive Node.js and like his follow up article x10 performance over Node.js is not abnormal.
- josephg 6y agoYep. 8x is the right ballpark I usually see comparing decent C/Rust to javascript. I saw a 10x slowdown porting the Chipmunk2d physics engine to javascript. (Which dropped to 5x or something after a few weeks of JS optimizations). Porting operational transform heuristics from JS to C got me a ~40x speedup. The code in question used heterogeneous data types which V8 hates (causing both heap thrashing and code deopt). In C I handed it with a tagged union and the code needed 0 allocations in the hot path. Generally the rule is, if your code has complex data types, heterogeneous data types or a lot objects being created and destroyed it be much slower in javascript compared to C/Rust. A lot of that benefit goes away if the native code does sloppy allocations everywhere. And on the other hand, if your code is Math heavy, operating on simple data types, V8 will do great work.
- rattray 6y agoThere area lot of JS developers who are considering moving code to Rust for performance, but will start out as naive.
- laurent123456 6y agoOr like: if you spend several hours optimising your Rust code, and not optimising the JS one, then Rust will be faster.
- josephg 6y agoSure but there’s an upper bound on how fast you can make javascript code run. The same amount of optimization work (if you know what you’re doing) will make a far greater difference to the performance of rust code than it will to the performance of javascript. Carefully written javascript is still pretty slow because you can’t choose to put objects on the stack in JS, or allocate nested objects together in contiguous memory. And that makes a massive difference because you end up thrashing main memory when you could be spending those cycles getting work done.
- johannes1234321 6y ago> that makes a massive difference because you end up thrashing main memory when you could be spending those cycles getting work done. That is true, however there is a huge domain of problems where that isn't much of a concern, while development time spent on it is a concern. Then sacrificing a bit of performance over development speed can be quite worthwhile. Of course not a high frequency trading platform and not an AAA+ Game, but a small Webservice just taking data from a ("slow") database, doing little transformation, and sending over network .... less of a problem
- watt 6y agoI find it unsettling how one-sided the article is, if it's a "showdown" I expect the same effort to be put in JS side of things to maintain some pretense of fairness.
- alpaca128 6y agoOne issue I see is that the Rust code seems to use String objects for single characters instead of e.g. a chars iterator or a single String that you then slice up. With a couple changes you could probably eliminate all allocations after the initial one.
- OneAlias1 6y agoHe figures that out in his 2nd article, where he now finds Rust is 1/8th of the time of Node.js.
- lionkor 6y agoSounds like a good read, do you happen to have a link? EDIT: https://cesarvr.io/post/rust-vs-javascript/ https://cesarvr.io/post/rust-vs-javascript/
- gregod 6y agoI think the following minor optimizations can further decrease the runtime: * There is no use in first pop()ing a candidate from the "reacts" list and then potentially re-pushing them later. Getting a pointer to the last element, and removing it if it is not needed, is more efficient. * Reordering the character equality condition could help a tiny bit since the inequality check should be faster than the ignore case check.
- creata 6y ago> Reordering the character equality condition could help a tiny bit since the inequality check should be faster than the ignore case check. I think the given ordering is better: in a conjuction, you typically put the less likely condition first, not the cheaper one.
- gregod 6y agoGood point. Here it is premature optimization anyway and too dependent on the input. In general, I think a balance between likelihood and cost should be found. Out of curiosity, I just did a small benchmark (criterion) using the input.txt provided, and reordering results in a 10% difference: Old Order time:[307.28 us 309.12 us 311.17 us] Reversed time:[270.82 us 271.18 us 271.54 us] This feels weird since the input.txt contains few pairs of equal characters, so there is likely something else going on. The inequality is very inexpensive but the ignore case compare should not be that expensive either.
- 01acheru 6y agoWhile I totally agree that Rust is a great language the point I got from this article is more about how fast NodeJS has become over the years, the work done on V8 is astounding!
- deleted 6y ago[deleted]
- kevincox 6y agoThe performance of JavaScript VMs is without a doubt in my mind one of our most incredible technological achievements. The JVM may have more hours of work spent (largely due to academic attention) but it seems that JS and Java have roughly the same performance which is outstanding when you consider how much more dynamic JS is.
- kaba0 6y agoJust as another data point, trufflejs (that is GraalVM’s implementation of JavaScript on top of a JVM written in Java running on top a traditional JVM) has comparable peak performance to V8.
- gher-shyu3i 6y agoJava is still much faster: https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/javascript.html https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
- kohlerm 6y agoThis looks kind of a another flawed benchmark, because of the use of time, which includes the startup cost for the process. Rust is compiled to native code with an AOT compiler, which should have significantly lower overhead compared to the NodeJS Jit.
- OneAlias1 6y agoPerfectly valid to include it because that is what you will experience when using it.
- adam_arthur 6y agoExcept that startup cost is a one-time hit (of course), and 99% of the time performance sensitive code will be running in a longer lived context.
- OneAlias1 6y agoIn a longer live context(more data crunching) Rust will win by even a greater margin, as performance improvements will compound. I know from benchmarking various Rust vs Go vs Node.js scenarios, and as the volume of data increases Node.js's performance gets worse.
- nicopappl 6y agoThe point on the unicode to_lowercase is quite interesting. I wonder if the javascript version is faster because it's less safe or because the JIT compiler identifies properly that unicode-awareness is useless and specializes the code. The author has a follow up article[1] on their blog about it. They rewrite the rust code to eliminate the extra allocations they identified at the end of this post. It then performs 8 times faster than the javascript version. However, I think they could have simply iterated on the code from the first post: There are a lot of allocations because two strings are created every time they are pushed to the queue (.to_string()): queue.push(candidate.to_string()); queue.push(token.to_string()); This is unnecessary, because candidate and token are both a single character. Rust has a char type that is useful to store single characters. Especially, unlike String which is heap-allocated, char is a primitive value that resides on the stack and CPU registers. Since we are comparing single characters, it's much better to use char. So if the queue is defined as a Vec<char> instead of a Vec<String>, and the comparison function took as argument char rather than &String, it will spare a lot of allocations and this will speed up the final application. let mut queue: Vec<char> // ... if !compare(candidate, token) { queue.push(candidate); queue.push(token); // ... fn compare(token1: char, token2: char) -> bool { This has also the advantage of removing the references, making the code cleaner. Using Vec::with_capacity(n) instead of Vec::new() will also help lowering the allocation load by preemptively allocating a large enough memory slice. With Vec::new(), the vector will reallocate a few times. [1] https://cesarvr.io/post/rust-vs-javascript/ https://cesarvr.io/post/rust-vs-javascript/
- mathw 6y agoYes I read the Rust code and just knew it would likely not be dramatically faster than the JS - certainly not as fast as it could be. This is why all languages reward study and depth of knowledge, as you just avoid some of the patterns which kill your performance almost instinctively after a while. The whole "I wrote it in JS and then ported it" introduction meant I was expecting exactly that kind of problem.
- lionkor 6y agoThis is why no language is "just faster" than another. The languages that are commonly associated with being "fast" can be way slower than any "slow" language if written wrong. Yes Rust, C, C++ can be fast. Really fast. Impossible-to-beat fast - if you know what you're doing. If you don't, it's going to be worse than any trivial python, lua or js that does the same thing.
- lifthrasiir 6y agoPossibly due to the lack of LTO, Link-Time Optimization. Rust standard library and user program get separately compiled and needs a special procedure to inline them out. I couldn't find Cargo.toml used in this benchmark but it ought to contain `lto = true` somewhere in the profile section to get the optimal performance especially for this kind of tight loops. (It makes compile slower, unfortunately, and no profile currently turns that on by default.)
- IshKebab 6y agoMinor nit, but if you find yourself writing `return true` `return false`, it's a sign your code can be simplified. In this case: fn react(token1: &String, token2: &String) -> bool { token1.to_lowercase() == token2.to_lowercase() && token1 != token2 } Anyway as soon as I saw the problem I thought "this is either completely trivial or insanely difficult depending on whether you want proper unicode handling or not". I haven't really thought about it but I doubt his solution is fully correct according to unicode. E.g. What happens with `aßSSb`? Who knows what should even happen in that case. As for the malloc performance, MacOS has a significantly slower system memory allocator than Linux does. If you use jemalloc (https://crates.io/crates/jemallocator https://crates.io/crates/jemallocator) instead I think you will see better performance than Javascript. Good article anyway.
- mywittyname 6y agoReplacing the function call with this (and flipping the checks around) will probably improve performance as well. I'd also be interesting in the performance improvements from replacing those to_lowercase() calls with a lookup from a static lookup array. Something like: if ( explosion_map[token1] == token2 ) There would need to be duplicates, explosion_map['a'] = 'A' and explosion_map['A'] = 'a'. But that's a small cost to pay for eliminating three function calls and two comparisons. As an added bonus, branch prediction will work really well on this strategy (since the majority of comparisons will have the same result). And doing that is a more of an actual language performance comparison, instead of a the standard library performance comparison this is.
- edoloughlin 6y agoIs this the developer version of motivated reasoning? Throw up two solutions, then spend hours optimising the one you assume should be faster and don't revisit the other one...
- creata 6y agoHere are some more obvious modifications[1] to the author's follow-up post.[0] It runs a further 1.4x faster on my machine. [0]: https://cesarvr.io/post/rust-vs-javascript/ https://cesarvr.io/post/rust-vs-javascript/ [1]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8fbb9870c0cb4a382f7aca372dd60341 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- gchamonlive 6y agoI can't help but feel the way people are supposed to optimize code written in languages like Rust doesn't scale too well. It relies heavily on exploratory work and the inner workings of the compiler and target platform. It can grow very slow very quickly with a large codebase. This in itself is not the problem. The problem is that you shouldn't be doing this at code level, but at compiler level. The optimizer is supposed to do these manipulations. At code level, you should be able to describe your problem in lots of details, use identities to simplify your logic, but leave the optimizations to the compiler. From the little I read about rust, I believe its type system is powerful enough to enable the compiler to do this. I believe it is a matter of time until the compiler matures to the point of doing the kind of optimization it is required to do.
- creata 6y agoRoughly how would you write code to solve this particular problem in your ideal language?
- gchamonlive 6y agoJust like I described, detailing the problem in the type system, simplifying the abstractions and proving correctness of code.
- creata 6y agoThat's a lovely ideal, but could you write some pseudocode for this particular problem in such a language?
- gchamonlive 6y agoThat is beside the point. I could produce something in Haskell, but the "magic" would all done in the compiler, so in the context of my original comment I see no reason why I should.
- 6y ago
- ricardobeat 6y agoThe main flaw in this benchmark is that it's too little work, so the javascript numbers are very close to node's startup time alone. Running 100 iterations gives you a much better idea of actual performance (JS is over 10x slower) but then you need to think about what compiler optimizations are being applied.
- michaelscott 6y agoThis might be a silly observation, but I do notice in the perf section the author is using "cargo build" without the release flag. This will definitely slow Rust down considerably, although I'm sure the author is aware of this and ran the final benchmarks with Rust in release. Would just be good to note in the post.
- nyanpasu64 6y agoThe "Attaching Perf" section shows that "./target/release/day-5" was being launched, which is the release binary.
- michaelscott 6y agoI didn't catch that, thanks. Shows how thoroughly I'm reading
- brundolf 6y agoI remember when I first discovered this. I was implementing a raytracer and test runs were taking forever. Then I read about enabling compiler optimizations, and with the flip of a switch my performance increased 16x...
- pdpi 6y agoThe HotSpot JVM, V8, LuaJIT and probably a few other such projects have become wickedly fast over the years, and using native languages because they're "faster" is folly. What Rust, C, C++ and the like actually give you is control. It is up to you, as a developer, to leverage that control to get better performance. Control also gives you nearly limitless rope to hang yourself with.
- deleted 6y ago[deleted]
- yodelshady 6y agoThe Rust code provided runs in 0.1 s (debug) / 0.04 s (release) for me (generic ultrabook + linux). That's quite a lot faster. Don't think I'm the first to mention - what would profile.release do if you're not calling cargo build --release? Maybe the author is, but a slight suspicion... they're calling older code. (Also, what's the deal with read_file() and wanting a Vec<String>? Rust has a read_to_string() method as part of std::fs.)
- m1el 6y agoAfter reading this blog post, I had a question: why doesn't it take less than millisecond in Rust code? And the answer is that the author allocates a string for each character, and does more allocation by calling to_uppercase/to_lowercase. There's a hundredfold improvement in speed by using char or u8 to iterate over a string.
- flohofwoe 6y agoApart from the UNICODE to_lowercase(), what's worrying is the high memory allocation cost, even though it's not very obvious in the source code where those allocations are exactly happening (even if you "know" that String and Vector are allocating under the hood it's not obvious when and where exactly memory is allocated). This is basically the same problem as in C++: convenient and "idiomatic" standard library features may end up doing tons of expensive hidden allocations. Isn't this a pretty big problem for a "systems programming language"?
- nyanpasu64 6y agoIt should be possible to have perf launch the program itself, avoiding the race condition or pgrep ambiguity, and having to reconfigure the kernel to allow debugging non-child processes: perf record -F 99 ./target/release/day-5
- bg117 6y agoGlad that Rust won. We can sleep peacefully now...
- yesbabyyes 6y agoI did some of the Advent of Code 2020 challenges in both js and Rust. For one of them, the js solution took minutes to finish, while the same algorithm in rust finished in <200 ms. I could probably have optimized the js solution to remove a bunch of allocations- it must have been the gc-but it was really surprising with the speed-up.
- peterkelly 6y agoThis really doesn't tell you much at all, because: - Only one program is tested, exercising a very small portion of each language (and its built-in APIs). - The runtime is too small; less than a second in both cases. I'd really need to see examples where the benchmark has been run in a loop multiple times, preferably with a warmup run beforehand, and an execution time measured in tens of seconds. At least the author made the effort to figure out the reasons behind the differences (based on the hypothesis that Rust would be faster), so credit is due there.
- hedora 6y agoPopping from the source is likely a big source of allocator / copy overhead. It’s probably better to just iterate over an input array. The algorithm in the article would be quadratic in perl 5; not sure about rust or js, but the point still stands: It’s likely to see asymptotic performance differences in different languages. (Edit: it’s not quadratic in perl. If it used shift to pull from the front of the array it would be.)
- Measter 6y agoPopping doesn't touch the allocator, as it doesn't resize the backing storage. The only thing being copied is the String struct (24 bytes), which while unnecessarily large here, wouldn't be the bottleneck.
- la_fayette 6y agoSo let ne be naive, is performance important? Would a user matter? In my opinion what matters is how much energy is consumed when executed on a smartphone or IoT device. Because there the main question is, when do I have to recharge the next time. For IoT applications this is highly critical. In this regard fast is not automatically energy efficient, if you consider e.g. big.LITTLE cpu architectures...
- imiric 6y agoI'd argue that what matters is human time. If writing this in JS took an hour to deliver acceptable performance, and writing it in Rust took 6 hours of reading documentation and googling to produce a similar or faster by a few milliseconds result, then JS is the clear winner here.