8 ms·
How I Made My Ruby Project 10x Faster
- deleted 13y ago[deleted]
- scosman 13y agoAll of the performance improvements combined were about 0.00002s per call. They came at the cost of simplicity.
- grandalf 13y agoGreat point. Simplicity is one of the main reasons to use Ruby, not performance.
- vidarh 13y agoThat may be so, but sometimes, once your design has settled, you'll have hotspots you want to speed up. Of course I hope nobody starts jumping through all kinds of hoops before they know that it matters...
- catnaroek 13y agoEase of use. Which is not the same thing as simplicity.
- grandalf 13y agoWell, code can be organized in a simple way where the constructs of the language do not get in the way of conceptual clarity. This may or may not translate to ease of use depending on the user.
- catnaroek 13y agoMy understanding of simplicity is that it can be nailed down to a few universal principles: 1. Facilitating local reasoning: I do not want to read the whole program to understand what a single line of code may or may not do. 2. Compartmentalizing concepts: There should be a single, easily identifiable feature for each of: defining classes of data, separating concerns between different code units, enabling code reuse. 3. Keeping abstraction leaks to a minimum: Abstraction leaks force corner cases to be handled outside of the abstraction (that is, low-level programming). So abstractions should ideally not be leaky, and leaks should only be tolerated if 1. there is no nonleaky (or less leaky) alternative, 2. the benefits of the abstraction outweigh the cost of handling the leak. Simplicity leads to conceptual clarity. On the other hand, ease of use is more about the pragmatic desire to invest the least amount of effort possible in making a working program. At its most short-sighted extreme, the easiest thing to do is simply following the path of least resistance. A more refined version is following design patterns that experience tells work okay. Simplicity and ease of use are most certainly positively correlated, but it is perfectly possible for a horribly contrived construct to be easy to use, if its complex internal details happen to match nicely the problem domain one is working on. My heuristic take (admittedly not justified on anything beyond aesthetical preference) is that simplicity is the more fundamental concept, and ease of use is just a corollary. And my impression of Ruby is that it seeks to maximize ease of use, resorting to non-simple means if necessary.
- jamesbritt 13y agoIndeed, it would be simpler if there was only "if" and no "unless", or multiple names for what are the same method (Array#size and Array#length), but (often) they're there to make things easier to read and write.
- catnaroek 13y agoA more serious offender is the .to_s method: Why on earth is nil.to_is == "", but [nil].to_s == "[nil]"?
- danenania 13y agoWhat would you expect [nil].to_s to be? Seems like a reasonable representation to me. I certainly wouldn't want "['']", and I also wouldn't want nil.to_s to give me "nil". In practice having nil evaluate to an empty string is quite useful.
- catnaroek 13y agoI think nil.to_s == "nil" is pretty reasonable. At least, it gives .to_s an elegant definition that does not need to case-analyze the nil-ness of the elements of an array. In any case, this just reinforces my point that Ruby emphasizes ease of use, not simplicity.
- egonschiele 13y agoIn most cases I agree with you. In this case though, the speed penalty could have been an argument not to use contracts.ruby...and I wanted to avoid that :)
- catnaroek 13y agoRuntime tests always cause a performance penalty. (And they do not prevent you from writing buggy programs! They just make the runtime environment yell at you if a bug is found.) If you want to guarantee that a property holds for all the elements of a set (every possible running instance of a program), forget about the elements and work with the set's definition (the program itself).
- egonschiele 13y agoDo you mean something like laser?: https://github.com/michaeledgar/laser https://github.com/michaeledgar/laser
- catnaroek 13y agoLASER's homepage ( http://carboni.ca/projects/p/laser http://carboni.ca/projects/p/laser ) for some reason does not load in my browser. http://downforeveryoneorjustme.com/http://carboni.ca/projects/p/laser http://downforeveryoneorjustme.com/http://carboni.ca/project... Judging the project from its feature list in the README.md, it does not seem like LASER is capable of handling arbitrary invariants.
- ufo 13y agoStatic analysis is really hard in general though, and in complex and highly dynamic languages like Ruby its even harder. Some things you really are better off using runtime checks for... And in the end, smaller overhead on the contract checker means that the programmer has more room to add extra tests and less reason to turn off the checks in production.
- catnaroek 13y ago
- vidarh 13y agoArguably, a lot of them came through improvements that did nothing to harm simplicity. Removing the unnecessary callback, for example. And ".times" is certainly more idiomatic than ".zip" for his case. It's a toss up between .each_with_index and .times, but really both will be readable enough. And from experience, you'll find Ruby devs that are unfamiliar with "zip", but not with "each_with_index" or "times". "alias_method" is also far more idiomatic than grabbing a reference to the method - I don't think I've ever actually seem method() used in production code (not that I've explicitly looked)
- cortesoft 13y agoThis was my thought as well. This is one of the main fallacies of code optimization; unless these functions are called a LOT, you very well might be optimizing something that is not your bottleneck. You can see the author is missing this point because he talks about going from "20 seconds to 1.5 seconds." He is using those numbers as if they are real use case times, when in fact they are generated from an arbitrary amount of calls to a function. So many times I will see people make code optimizations of this type to shave fractions of a millisecond from a function call at the cost of complexity, memory usage, etc. Meanwhile, most of the time their program is actually waiting on IO or something of the like. Blind optimization is counter productive.
- egonschiele 13y agoWhy do you think this is blind optimization? When do you think an optimization like this would be appropriate? Can you give an example of a performance optimization you have made recently?
- jasonlotito 13y ago> Why do you think this is blind optimization? Because it is. It's a clear case of premature optimization. Especially since nothing was offered up to to suggest otherwise. > When do you think an optimization like this would be appropriate? When it's being reported back that it's one of the top bottlenecks. Before that, it's mostly a educational trip. It's great to learn how to write optimized code, but, frankly, removing empty function calls isn't an optimization. > Can you give an example of a performance optimization you have made recently? Eliminating the need for excessive memcache calls by not only changing the objects being called, but also by eliminating the need to make the calls in the first place by intelligently using existing locally cached… cache. If that makes sense. Anyways, that improved performance by ~20ms. It wasn't the slowest call running, but it was the slowest call running that could be optimized at this point.
- nonchalance 13y agoYou don't know what project he was using that gem for. It's very well possible that he did some tests and found that this contracts library was the bottleneck. If that were the case, then the approach makes sense. Although, given the nature of the library, I highly doubt that was the bottleneck
- orclev 13y agoIs it wrong that I was secretly hoping the answer was "I rewrote it in something that isn't ruby"?
- rubiquity 13y agoWrong? Sometimes. Trendy? All of the time.
- joemaller1 13y agoAt this point on HN, I was expecting a page that just said "I rewrote it in Go"
- unknownian 13y agoOr Erlang/Elixir.
- SkyMarshal 13y agoDamn, busted. Elixir was the first thing that popped into my head on seeing this title. It looks almost like a Ruby port to the Erlang VM. That said, good writeup, even for a non-Rubyist, enjoyed seeing the process. One thing that stuck out is how control flow via if/then/else is faster than via case (for more than 2 options) in Ruby. I always assumed the opposite, for any language.
- ics 13y agoThere's an older (but still interesting) post about case in Ruby here[1] and another here[2] with some extra insight into the MRI implementation. Having learned Ruby before most of the other stuff I play with these days, I always second-guess myself when using a switch/case construct in my code. [1] http://www.skorks.com/2009/08/how-a-ruby-case-statement-works-and-what-you-can-do-with-it/ http://www.skorks.com/2009/08/how-a-ruby-case-statement-work... [2] http://www.daniellesucher.com/2013/07/ruby-case-versus-if/ http://www.daniellesucher.com/2013/07/ruby-case-versus-if/
- nfm 13y agoDoes anyone know what's going on behind the scenes with `alias_method`? I'm curious to know how it's implemented such that it was actually faster!
- jfarmer 13y agoalias_method is faster than what he did before because he's replicating a bunch of internal logic at run-time, e.g., binding "self" to the correct thing. Somewhere deep in the bowels of the Ruby interpreter it's going to be doing the same thing, except it has the opportunity to create the binding statically at define-time. Likewise, he's calling a method on a Method object to invoke the original method vs. invoking the original method directly. This might speed things up a little more, too: alias_method :"original_#{name}", name class_eval %{ def #{name}(*args) self.original_#{name}(*args) end } "send" is going to be using extra machinery in the same way, although I think "send" also circumvents certain protections like public/private, so I'm not 100% sure where in the Ruby method invocation process it fits in. "send" might be closer to the metal, but a pre-defined method name would have the opportunity to cache certain things in the invocation process.
- blakeshall 13y agoI really enjoy write-ups like these. Anyone have a resource that is primarily blog posts/articles like this?
- karlmageddon 13y agoThe rest of his block is pretty great -- http://www.adit.io/index.html http://www.adit.io/index.html Some of them have pretty cool original illustrations, too!
- Pxtl 13y agotl;dr: bolting on a dynamic-typed imitation of static-typing is slow.
- Locke1689 13y agoContracts are not types and do not really behave like types in many ways.
- vidarh 13y agoThey behave enough like types that the library in question is pretty much as far from idiomatic Ruby you get if you use it like in his examples and tutorial. Constraining inputs to a specific class, for example is a huge, giant no-no. If you're going to make Ruby statically typed, then you're better off picking a language that's easier to make fast. This is a pet peeve of mine as I've more than once had to work around arbitrary, stupid checks against specific classes in cases where meeting the "actual" contract of the method called would be down to implementing a simple method or two. The tutorial hints at some possibly Ruby-ish possibilities, like contracts based on whether an object responds to a method, but all the focus seems to be on class comparisons. I do like the idea of specifying contracts, fwiw, as long as they're focused on behaviour and not on matching specific classes. I don't, however like the idea of specifying them in the class files. Specifying them in separate files, that could be loaded when wanted, and that could also be used by a testing app to do some degree of automated testing of limits etc., or by other apps to do limited static analysis would be interesting.
- catnaroek 13y agoAnd their core, types and contracts are the same thing: logical propositions asserting properties of a program that have to be enforced somehow. The difference is the method of enforcement: types are enforced via static analysis and contracts are enforced via runtime checks. Oh, and if what you meant was "types cannot check pre/postconditions", then you should have a look at dependently typed languages. Some dependently typed systems can express any proposition expressive in first-order predicate logic. (The downside is that satisfying the type checker becomes bloody hard.)
- deleted 13y ago[deleted]
- Demiurge 13y agoDid you rewrite it in Go? I guess I should read the article. OK, now that I've read it, profiling ftw :-)
- mosselman 13y agoWould have been more useful if there were more before-after examples.
- aneth4 13y agoSort of takes away from the "power" and "beauty" of the language when you need to dissect your code and understand the runtime to get reasonable performance.
- rubydoobydoo 13y agoI can't believe this is the top comment at the moment. First, let's get something straight. This project wasn't necessary, because you shouldn't be type checking in Ruby if it is written correctly. That's basically what contracts are, at least in this case. To address your concern, Ruby's performance is plenty fast- like Java entering the early 2000's fast. (You probably don't get my reference, but in the mid-1990's, people complained Java was slow.) No one at work has complained about the performance of our Rails app running on Unicorn, and we've not had to do one performance tweaking iteration ever.
- annnnd 13y agoI can't believe this is not a top comment anymore. I don't have enough experience with Ruby to comment about it, but as far as Java is concerned, people (myself included) are still complaining it is slooooooooo... <let me pause a bit to collect garbage>... ooow. Don't get me wrong, Java is an excellent language which has plenty of other benefits (as I guess Ruby does too), but raw performance is not one of them.
- aneth4 13y agoI do get your reference. I was a java developer since 1996. 2000 was 13 years ago and ruby is far older than Java was then, so hardly a valid comparison. I am a ruby developer and ruby is dog slow.
- rubydoobydoo 13y agoAlmost the same # years experience here in Java, then in Ruby for the last several. Ruby is NOT dog slow compared in duration of time per activity on the (virtual) hardware we have now than Java was in the early 2000s on the hardware in those days. Java is slower than writing machine code, but that doesn't mean I'm going to start coding in machine code. If Ruby is dog slow for you, you are probably trying to run JRuby in development (vs. on server) which is dog slow (though on server after startup and compilation it is pretty darn fast), or you're starting Rails up everytime you need to do anything (vs. the many ways around that). If you've read JRuby is faster- they are talking about the server. They didn't mean locally, recompiling all of the time. It's true Ruby is older, but Ruby is usable today. I'm sorry that you had a bad experience but please don't spread FUD.