8 ms·
Speed Without Wizardry
- austincheney 9y agoThe simple rule I have found for achieving superior performance in high level languages, particularly JavaScript is to simply do less. It isn't that simple though. Doing less really means less code totally at the current compilation target, essentially feeding fewer total instructions to the compiler. This means no frameworks and minimal abstractions. It means having a clear appreciation for the APIs you are writing to. It means minimizing use of nested loops, which exponentially increase statement count. Sometimes caching groups of instructions in functions can allow for cleaner code with a positive performance impact. V8 cannot compile arithmetic assignment operators, which it calls left-side expressions, so you can see a rapid speed boost in V8 when you replace something like a += 1 with a = a + 1. The side benefit of less code is generally clearer and cleaner code to read. There isn't any wizardry or black magic. No tricks or super weapon utilities. As an example I wrote a new diff algorithm last year that I thought was really fast. https://news.ycombinator.com/item?id=13983085 https://news.ycombinator.com/item?id=13983085 This algorithm is only fast because it does substantially less than other algorithms. I only wrote it because I could not wrap my head around the more famous Myers' O(ND) algorithm. A side benefit, in this case, of doing less is an algorithm that produces substantially more accurate results.
- Narishma 9y ago> V8 cannot compile arithmetic assignment operators, which it calls left-side expressions, so you can see a rapid speed boost in V8 when you replace something like a += 1 with a = a + 1. Is there a reason it can't? I'm not familiar with Javascript, but aren't the two expressions equivalent?
- austincheney 9y agoThe two expressions are equivalent. V8 cannot compile that logic into optimized bytecode due to a violation in its code engine that conflicts with other optimization logic. So instead of fast compiled code the code in the local scope of that expression is slow string interpreted code. https://github.com/vhf/v8-bailout-reasons https://github.com/vhf/v8-bailout-reasons
- chillee 9y agoDo you have a source that cites the += example? I can't seem to find it on the page you linked.
- austincheney 9y agoI remember seeing the cause of this specific case mentioned in a slide deck by one of the V8 engineers. I don't remember where online it is. I was to validate this performance limitation more than a year ago through self-testing in my personal code. As titzer mentioned this issue may no longer exist. I would have to run additional tests to independently make an assessment with the current V8.
- titzer 9y agoThat list is for CrankShaft which has been replaced by TurboFan > 6 months ago. If you continue to experience slowdowns please file a bug and it can be investigated.
- maaark 9y agoThere's an automotive engineer somewhere reading this, irrationally upset at the idea of replacing a crankshaft with a turbofan.
- whatyoucantsay 9y ago> "It means minimizing use of nested loops, which exponentially increase statement count." Nesting two loops has an n^2 cost and nesting 3 levels deep costs n^3. At no point does it ever cost 2^n or any x^n. It's polynomial, not exponential.
- austincheney 9y agoBefore I begin just let me say I am not a mathematician. I program so that I don't have to do complex math. I know in reality the frequency of iterations varies considerably but for simplicity of discussion let's remove variability. Say we have a loop with 1000 iterations. That is at minimum 1000 statements in the loop body plus expression overhead from the loop itself. If this loop is nested once with a same sized loop there are now 1,000,000 iterations plus some expression overhead per iteration. If it is nested twice deep there are now 1 billion iterations. That example is exponential of 1000. Given that there is overhead associated with operation of a loop it is actually greater than exponential. It may not be quite so dramatic as a logarithmic growth curve though. I completely concede that in reality loops vary in iteration count and so nested loops aren't likely perfectly exponential unless their iteration counts are identical. The increase of iterations from nesting loops does increase more dramatically than a simple multiplicative operation as the depth of loop nesting increases, such that the growth of total iterations is a curve on a graph. A polynomial growth operation when graphed should present a straight diagonal line without the presence of a third variable.
- mlevental 9y ago>that example is exponential of 1000. no it's not. you simply have the definitions mixed up. exponential slow down or speed up means a^x where x=1000. you are describing polynomial growth, i.e. x^a (where x=1000 and a=3)
- yorwba 9y agoIt is exponential in the number of nested loops, which is what's important for the realization that adding more nested loops is bad.
- oldandtired 9y agoOne would have expected that the arithmetic assignments operators would have been faster as a += 1 would only compute the address of "a" only once and then duplicate it, whereas a = a + 1 would compute the address of "a" twice. For more complicated examples the first should see an even faster speedup. So, from my perspective, there is a serious problem here in the optimiser.
- IncRnd 9y ago> This is a factor of 4 improvement! This is a common mistake. It should read, "This is a factor of 3 improvement!" x+x+x+x is an improvement over x of 3x not of 4x. The improvement factor is 3.
- recursive 9y agoIt's 3x faster than, but 4x as fast as.
- deleted 9y ago[deleted]
- IncRnd 9y agoYes
- mraleph 9y agoI am not a native speaker, so I was always under impression that "factor of K larger" means something was X and became K * X (e.g. the multiplicative factor was 1 and became X). Can I read somewhere about the correct usage?
- IncRnd 9y agowww.wikihow.com/Calculate-Percentage-Increase The general equation for an increased quantity from A to B is increase = B - A So a percentage increase is increase * 100 not (increase + A) * 100
- mraleph 9y agobut I am not talking about the percentage increase I am talking about increase "by a factor of X" that is X = B/A. See: https://ell.stackexchange.com/a/52747 https://ell.stackexchange.com/a/52747 Maybe my problem is that I am missing "by".
- 9y ago
- DannyBee 9y agoI really don't understand this article, and the claims really rub me the wrong way. The main point it makes is, again "He perfectly demonstrates one of the points my “Oxidizing” article was making: with Rust and WebAssembly we have reliable performance without the wizard-level shenanigans that are required to get the same performance in JavaScript." This doesn't make a lot of sense as a claim. Why? Because underneath all that rust .... is an optimizing compiler, and it happens the author has decided to stay on the happy path of that. There is also an unhappy path there. Is that happy path wider? Maybe. It's a significantly longer and more complex optimization pipeline just to wasm output, let alone the interpretation of that output. I have doubts it's as "reliable" as the author claims (among other things, WebAssembly is still an experimental target for LLVM). Adding the adjective "reliable" repeatedly does not make it so. Let's ignore this though, because there are easier claims to pick a bone with. It also tries to differentiate optimizations between the two in ways that don't make sense to me: "In some cases, JITs can optimize away such allocations, but (once again) that depends on unreliable heuristics, and JIT engines vary in their effectiveness at removing the allocations." I don't see a guarantee in the rust language spec that these allocations will be optimized away. Maybe i missed it. Pointers welcome. Instead, i have watched plenty of patches to LLVM go by to try to improve it's heuristics (oh god, there's that evil word they used above!) for removing allocations for rust. They are all heuristic based, they deliberately do not guarantee attempting to remove every allocation (for a variety of reasons). In general, it can be proven this is a statically undecidable problem for a language like rust (and most languages), so i doubt rustc has it down either (though i'm sure it does a great job in general!) The author also writes the following: "WebAssembly is designed to perform well without relying on heuristic-based optimizations, avoiding the performance cliffs that come if code doesn’t meet those heuristics. It is expected that the compiler emitting the WebAssembly (in this case rustc and LLVM) already has sophisticated optimization infrastructure," These two sentences literally do not make sense together. The "sophisticated optimization infrastructure" is also using heuristics to avoid expensive compilation times, pretty much all over the place. LLVM included. Even in basic analysis, where it still depends on quadratic algorithms in basic things. If you have a block with 99 stores, and ask LLVM's memory dependence analysis about the dependency between the first and the last, you will get a real answer. If you have 100 stores, it will tell you it has no idea. What happened to reliable? Why does this matter? For example: Every time rust emits a memcpy (which is not infrequent), if there are more than 99 instructions in between them in the same block, it will not eliminate it, even if it could. Whoops. Thats' a random example. These things are endless. Because compilers make tradeoffs (and because LLVM has some infrastructure that badly needs rewriting/reworking). These "sophisticated optimization infrastructures" are not different than JITs in their use of heuristics. They often use the same algorithms. The only difference is the time budget allocated to them and how expensive the heuristics let things get. There may be good reasons to want to write code in rust and good reasons to believe it will perform better, but they certainly are not the things mentioned above. Maybe what the author really wants to say is "we expect the ahead of time compiler we use is better and more mature than most JITs and can spend more time optimizing". But they don't. Maybe it would also surprise the author to learn that their are JITs that beat the pants off LLVM AOT for dynamic languages like javascript (they just don't happen to be integrated into web browsers). But instead, they make ridiculous claims about heuristics and JITs. Pretending the compiler they use doesn't also depend, all over the place, on heuristics and other things is just flat out wrong. At least to me (and i don't really give a crap about what programming language people use), it makes it come off as rampant fanboism. (Which is sad, because i suspect, had it been written less so, it might be actually convincing)
- Felz 9y agoFunny, I was using the source-map library under Nashorn. The performance was poor enough that I had to switch to embedding V8; I'm not sure whether that was a consequence of Nashorn itself being too slow, or the Javascript optimizations intended for V8/Firefox just completely missing their mark. Not that the WASM version of the library would've helped, since Nashorn doesn't do WASM at all. But maybe the performance would've been decent if it had.
- nickm12 9y agoI've got to admire the graciousness in this response. It's making the point that mraleph's “Maybe you don’t need Rust and WASM to speed up your JS” article completely neglected code maintainability as a factor, but it does so without turning the whole thing into a pissing match. It's all been a fascinating to read.
- wwwigham 9y agoIt's also probably not worth overtly begging the question of weather maintaining multiple languages within one project is worth the burden (seriously the full build for the `source-map` package is complex in comparison to the usual JS state of affairs if you want to experiment with the now-Rust bits), especially considering that at least part of the reason node became popular was to have one language for all needs, since focusing on that would invite that charged discussion. As a polyglot developer, I welcome all the mixing of the languages that wasm is bringing (certainly, it makes my flexible skillset more valuable); but honestly I do think we're leaving _something_ of the past homogeneity behind in doing so. (Was FFI quality all that was stopping us from mixing tons of languages in our non-browser projects before?) Circling back; both authors have their biases - it's best to read both articles skeptically, and IMO, take away the sage bits of advice they both echo and not anything about a specific technology: You should make the right choices for your project and your design, performance, and maintenance needs (including making your own evaluations), rather than jumping on some bandwagon without being properly informed. Also that profile-guided algorithmic improvements are usually the easiest place to make sweet, sweet perf gains (in any language) before you have to get into hairy maintainability trade-off decisions.
- AstralStorm 9y agoUnfortunately, JS does not lend itself to ease of maintenance - it is bad that it became language for the web browsers. You get dynamic weak typing making reuse more complex, unpredictable (nonlinear in performance with coffee changes) GC and JIT, weird type system. No error handling facilities in the language either. Heck, compared to JS even modern Java (which shares the GC and JIT unpredictability) or C++ (incl. arcane syntax and less safety if you like living dangerously) seem easy to achieve predictable results with. Rust takes more work you front - but not that much more.
- hobofan 9y ago> But a distinction between JavaScript and Rust+WebAssembly emerges when we consider the effort required to attain inlining and monomorphization, or to avoid allocations. I'm not sure that is true. Having worked/interacted with a lot of people working with Rust on different experience levels, most of them (that includes me) don't have a deep knowledge of what Rust concept maps to a specific concept with which performance implications. And if they do it's often only partial. I'd say that right now, only very few people that don't work on the Rust compiler have a broad knowledge in that area. Sure, it's much better to have to Result of the optimization expressed in code itself, but I'd say that the amount of knowledge and effort required to get to such a level of optimization is similar to optimizing Javascript. I also found the hint to `#[inline]` suggestions, a bit disingenuous. In the end they are just _suggestions_, and your are just as much at the mercy of the Rust/LLVM optimizer to accept them, as you are with a Javascript JIT. I'm a big fan of Rust, and I'm a big fan of Rust+Webassembly (working with it is the most fun I had programming in a long time!). Generally I think that Rust has one of the better performance optimzation stories, I just don't a gree with some of the sentiments in the post. There are also enough other reasons to love Rust+WebAssembly than just the peformance!
- lambda 9y ago> I'd say that the amount of knowledge and effort required to get to such a level of optimization is similar to optimizing Javascript. Really? After reading all of the articles in this series (the original about porting to Rust, the rebuttal about optimizing JavaScript, and this one)? I'm more familiar with Rust than JavaScript, but I found that other than the algorithmic optimization, the rest of the JavaScript optimizations were very non-obvious in order to achieve an effect that is entirely natural in Rust. There's no need to be careful to avoid particular types of function calls, since there are no dynamic variadic function calls in Rust. There's no need to resort to using the equivalent of `eval` for monomorphization, it's a natural feature of the language. There's no need to do manual memory management by encoding values into typed arrays of integers; you can simply allocate vectors of structs, borrow references, and so on. These are all things that had significant cost in JS, and needed to be worked around via some intensive profiling and knowledge of how JIT engines work, but just doing things naturally in Rust leads to a pretty much zero-cost solution. It's true that if you want to really get into the nitty-gritty of micro-optimization in Rust, you need to learn some more and do things like profiling and inspecting the generated code to see what optimizations the compiler was able to apply or not. But the Rust rewrite of source maps did none of that; they just rewrote it in fairly idiomatic Rust, and achieved a similar speedup to what a JIT engineer armed with a profiler, a deep knowledge of what affects how well a JIT works, and a willingness to do manual memory management in typed arrays was able to do. > Having worked/interacted with a lot of people working with Rust on different experience levels, most of them (that includes me) don't have a deep knowledge of what Rust concept maps to a specific concept with which performance implications What parts of Rust do you feel like you don't understand the performance implications of? I feel like the mental model for performance is relatively similar to C or C++, with relatively few things that would surprise you if you're familiar with modern C++ (in fact, a lot fewer performance surprises than modern C++ offers, in addition to the extra safety).