I started using Julia a couple of months ago, deciding to give it a try after weeks of hearing people praise various features of it. The more I learned about it, the more I liked its style, merging ease of expressing concepts in high-level language with a focus on speed and usability. I implemented a model that I'd also written in C++ and R in Julia, and found the Julia version ran much faster than the R version, but still slightly slower than C++. Even so the code was more legible in Julia than it was in both other languages. This is worth a lot, and in particular as I've generalized the model, the amount of work to do to broaden the scope of the Julia code was far less than the comparable amount of worked threatened to be in the other languages.
Lately, I have been focused on getting my Julia code to run faster, because I need to run this model trillions of times. In doing so I've been guided by @code_warntype
, @time
, @profile
and ProfileView
and the track-allocation
flag. Great. This toolkit isn't as nice as some other languages' profiling tooling, but it still pointed out a lot of bottlenecks.
What I find is that I have in my code precisely the high-level expressivity that I like in Julia, and when I rewrite that expressivity to avoid unnecessary allocations, I lose that very expressivity. As a trivial example, I recently changed a line of code that read
sum([x*y for x in some_list, y in similar_list])
to a loop that iterates over the lists and adds to a state variable. Not rocket science, and I understand why it's a little faster not to have to allocate the array. Actually it's a lot faster though. So I've done similar things with avoiding using Dicts or the composite types that "feel" right for the problem, when I can instead just manually keep track of indices in temporary parallel arrays, a style of coding I abhor but which evidently runs a lot faster when I'm repeating the particular operation of creating and briefly using a small data structure lots and lots of times.
In general, this is largely fine because I've taken to heart the instructions to write short methods, so the higher-level methods that compose behavior of my own shorter methods don't need to "worry" about how the shorter methods work; the shorter ones can be clunky to read without making the core of my program clunky to read.
But this has me wondering if I'm "missing something." If the whole point of the language (for me as a not-theory-concerned end user) is in part to combine speed with ease of development/thought/reading/maintenance etc., i.e. to be a writable and usable technical computing language, then doesn't that mean that I shouldn't ever be spending time thinking about the fastest way to add up a bunch of numbers, shouldn't be rewriting "easy to read" or elegant code that takes advantage of mappings, filters, and high-level function concepts into "clunky" code that reinvents the wheel to express those things in terms of low-level keeping track of array indices everywhere? (Some part of me expects a language with so much intelligence behind its design to be smart enough to "figure out" that it doesn't actually need to allocate a new array when I write sum([x*y]), and that I'm just too obtuse to figure out the right words to tell the language that except by literally "telling it" the whole loop business manually. At one point I even thought about writing @macros
to "convert" some quickly expressed code into the long but faster loopy expressions, but I figured that I must be thinking about the problem wrong if I'm essentially trying to extend the compiler's functions just to solve fairly straightforward problems, which is what led me to write this question.)
Perhaps the answer is "if you want really performant code, you pay the price no matter what." Or put differently, fast code with annoyingly unpleasant to read loops, arrays, keeping track of indices, etc. is on the efficient frontier of the speed-legibility tradeoff space. If so, that's perfectly valid and I would not say therefore that I think any less of Julia. I'm just wondering if this sort of programming style really is on the frontier, or if my experience is what it is because I'm just not programming "well" in the language. (By analogy, see the question What is your most productive shortcut with Vim? , where the accepted and excellent answer is essentially that the OP just didn't "get" it.) I'm suspicious that even though I've succeeded in getting the language to do much of what I want, that I just don't "get" something, but I don't have a good sense of what to ask for as the thing I fear I don't get is an unknown unknown to me.
TL;DR: Is it expected for best-practice in Julia that I spend a lot of time "decomposing" my high-level function calls into their primitive implementations in terms of loops and arrays in order to get faster performance, or is that indicative of me not thinking about programming in / using the language correctly?