From: Alex Martelli Date: 2003-09-17T02:31:54+09:00 Subject: Re: performance and style advice requested Ben Giddings wrote: ... > Alex Martelli wrote: >> Heh -- matter of taste, I guess; what _I_ personally find ugly is ... > Sorry, I wasn't completely clear. It's the leading underscores that I > object to. I don't mind underscores_in_names at all. The reason I > dislike leading underscores is that they tend to be holdovers to > languages that don't have proper access control. (C mostly) Or Python -- no "access control" whatsoever, just *advisory* indications of what's meant to be "published" and what's meant to be for internal use only. Just the time saved not having to decide what's private, public, or protected (Stroustrup regrets having introduced the complication of that third intermediate classifier -- see his book "Design and Evolution of the C++ programming language") is by itself a huge performance boost. > In any situation where someone is tempted to use them, I find that they > normally should be using something else (like using instance variables > which are by their nature private). Given that in Ruby one can always, anywhere, reopen a class and add getters and setters for all attributes of interest in a few keystrokes, I guess the "by their nature private" issue doesn't cost you much (except some wasted performance, perhaps?) but doesn't buy you much either (no more than in C++ with its typical "#define private public" trick, say). Java, at least in theory, _does_ enforce privacy strictly (but I've seen too many security issues with JVM's to trust it, personally;-), so there are "real" costs and benefits involved; the same might be said of a very few implementations of C++ (I'm thinking of SOM, but it's SO many years since I used it that I may have the details wrong -- clearly the strict enforcement didn't do much for the market success of those implementations of C++ -- against MS, Borland, and gcc, say;-). But as long as the "protection" is fundamentally just advisory, I don't think there's much in it, either as a cost or as a benefit. I'd rather have a simpler language relying on a simple naming convention than a more complicated language with elaborate advisory mechanisms, personally. > As for the ugliness of '$', it has been said that it is supposed to be > ugly. Global variables in general are ugly code, and so people tend to > avoid using '$', making their programs better. I have nothing against this line of reasoning. I'm still allowed to make the code a BIT less ugly (in my eyes) by using $_foo instead of base $foo, though. By the same token, literals in your code are ugly (they should all be constants, right?) -- so do you object to Ruby letting me write a prettier (and more readable, IMHO) 1_000_000 rather than an uglier (harder to read, must count 0's) 1000000 ...?-) >> However, it will be very rare for any given x to NOT be already >> memoized (i.e., fact is called with a very modest variety of >> arguments, compared to the number of calls to it, in any typical >> combinatorial-arithmetic application); while calls with x<0 are >> going to be exceedingly rare. So, the relative costs of array >> indexing vs (operator< + conditional) shouldn't matter -- trying >> to get the result off the array first _should_ be a win anyway >> (or at least, that's what my optimization experience as built on >> a wide variety of languages tells me -- admittedly I have no such >> experience in Ruby, but I fail to see how it would differ on this >> specific point). > > Ah, I see what you mean. I'm sure you're right that it's faster the way > you now have it. I didn't think things through fully and realize that > 95% of the time the number will be positive and so it will have to > evaluate the conditional then do the array lookup. It is also going to > be *much* faster to do the array lookup rather than the triple > conditional for the "comb" case. Ayup. I did try both ways, btw -- hard to measure the difference in my current benchmark (it just doesn't exercise fact enough, and comb barely enough), but fwiw it does seem to be faster with the "if" inside. (Anybody who's done enough optimization will tell you that rationalizing why A is faster than B is all nice and good, but until and unless you've MEASURED them, you don't really KNOW...!-). >> Actually, I find the oneliner body >> >> $_fact_memo[x] ||= if x<0 then 0 else x * fact(x-1) end >> >> quite clear and readable, given that one must grok the semantics >> of ||= anyway. > > Yeah. That "||=" is a little hard the first time you notice it, but Yeah, it _was_ a little hard, about 15 years ago I think, when I first met it -- in Griswold's "Icon" programming language;-). > once you realize how useful it is, you'll use it so often it becomes > second nature. No doubt. The Python equivalent (for a dict only, not a list, but Python dicts are so fast that they're preferable to lists in all of these cases -- yep I did measure both ways;-) would be: return fact_memo.setdefault(x, ...whatever...) BUT the applicative-order (vs normal-order -- aka eager vs lazy) semantics of function calls means that "whatever" would have to be computed in all cases (even when x IS already a key in fact_memo), making this construct useful in substantially fewer cases. > You could also use the other form, which as a C++ coder I'm sure you'd > recognize: > > $_fact_memo[x] ||= (x<0) ? 0 : fact(x-1) > > Which also works in Ruby. I don't like it as much, but it's a > preference I guess. A dream I'll surely never be able to afford would be to have the same projects tackled by pair of otherwise equivalent teams, but with two languages that differ in just ONE characteristic -- surely, in theory, that would be a wonderful way to measure the worth of that one feature. Out of all the possible "ONE characteristic"s to be so tested, the single one I'd be most interested in studying that way (if I had a few megabucks to spare in order to pay the temas;-) might be the enthusiastic "more than one way to do it" philosophy of Perl and Ruby, vs the principle which the C Standard phrases as "provide only one way to do an operation", and the Python Zen phrases as "There should be one-- and preferably only one --obvious way to do it.". My guess is that, while there might be tradeoffs depending on the quality, personality, etc, of the programmers, for _small_ teams, as teams grow larger, uniformity (with its attendant help in moving towards the "collective code ownership" ideal of XP) would no doubt become preferable. Of course, one could try to enforce uniformity by other means (static code checkers, regular code inspections, etc, etc), but they're all costlier than not having the variability in the language in the first place (and not having the variability also makes the language smaller and thus faster to learn, to master, to implement -- again, a "spirit of C" principle, "Keep the language small and simple", which I revere). Oh well, it will no doubt remain an (educated) guess -- and the uniformity, even in C or Python, is only an ideal, anyway. But for the life of me I just can't see why, when one has "if a then b else c end" working perfectly as both an expression and a control statement, one would WANT to weigh down the language with an alternative but equivalent syntax "a?b:c". I guess a definitely-NOT-postmodern aesthetics which appreciates simplicity, uniformity, etc, makes me characterially unsuited to appreciate this "enthusiastic redundance" idea of Perl and Ruby!-) >> Very good point, and an excellent suggestion at least for concision & >> readability purposes -- I'll definitely keep the idiom >> [] ||= >> in mind for the future -- thanks! > > Note it also works for non container objects: > > var ||= "new val"; Noted, thanks (I also notice the redundant trailing ";"""...?-) -- I guess this means that keeping variables initially undefined may be a sensible strategy in Ruby where it wouldn't be in C or Python. > and in other types of expression: > > var = other_var || "default val" Sure (but that's quite OK in Python too, since you can more legibly spell "||" as "or" in this case, and then it does short-circuit -- the Python issue is that there's no shortcircuiting "or=", as well as the fact that undefined variables raise exceptions rather than quiety returning nil [or None, in Python]). >> Performance-wise, there ain't all that much in it for this app. >> >> I've managed to scrounge some diskspace on Linux and compile both >> Python and Ruby (1.6.8 only, unfortunately -- the link I downloaded >> from mentioned 1.8, but I found out only when everything was built >> that I had 1.6.8 instead [how I found out: I had thoughtlessly left >> a trailing colon on an "if" statement -- 1.8 was quite cool about >> it, while 1.6.8 gave me three weird errors for that one mistake;-)]. > > If you have the chance to try it on Ruby 1.8 I (and others I'm sure) > would be interested in hearing how it stacks up both to Python and to > Ruby 1.6. No doubt I will once I'm back home -- I find that I can't work on this as much as I had hoped while on this business trip. > (In another post) Alex Martelli wrote: >>> While I doubt you should compare these languages on performance merits >> *blink* why not, pray? While clarity, productivity, simplicity, and so >> on, are surely all crucial attributes in a language, what's wrong with >> wanting to ascertain how it performs (when used in the best/fastest way, >> at least) in typical problems in one's domains of interest...? > > I don't think that it's wrong to compare the speed of the languages. I > just wouldn't be surprised if Python is faster by a healthy margin, > especially for a very simple numerical program like this. In Ruby, > *everything* is an object, including numbers. This has certain > advantages, but definitely carries some speed penalties. I fail to see where the speed penalties are inherent, given that the semantics of integer numbers in Python and Ruby are so similar now (e.g., fixnums, aka "int", now transparently give bignum, aka "long", results in Python, too). Indeed, in Python, there isn't that deep a distinction between int and anything else as there seems to be in Ruby between Fixnum (and true and false and nil - that's all I think?) and everything else -- surely by singling out fixnums that way Ruby IS gaining some speed advantage, at least for those computations (a fair chunk) that stay within the roughly-one-billion limit in this benchmark. Besides, it appears to me (from that one attempt to run the profiler) that the bottleneck isn't in the arithmetic, anyway, but rather in the generation of the subsets with that recursive iterator in each language (so perhaps my next step should be some attempt at recursion-elimination, muses he consequently). In other words, I'm not really sure this IS "a very simple numerical program"; where I come from (Fortran, my first language, back then...) by "a very simple numerical program" we mean something for which enumerating subset by recursion would never qualify (let alone iterators). Anyway, I'll grow this to definitely-NOT-simple eventually. Will the extra complication of the problem being solved favour Ruby's performance? Stay tuned...;-). I'll accept your evaluation that you're not surprised if Python is faster by a healthy margin, but I'd rather a different explanation than the one you give. *WHAT* do you think is NOT "an object" in the Python program, giving Python an intrinsic speed advantage...? I can't see anything, myself. Indeed, I'd look at the reverse issue: in Python "a tuple of numbers" is an object and thus can index right into a dictionary -- in Ruby, I was informed, when it LOOKS like I'm using a tuple to index into a Hash, in fact the interpreter is working hard under the covers at forming a string out of that tuple -- so, it seems to me, the performance handicap is that "tuple" is NOT an object which can (directly) index a Ruby Hash (here I've fixed it, as suggested, by using x<<16+y, rather than [x,y], as the key into the Hash, but I wonder about what happens when I have several numbers instead of just a pair of them -- as will soon be the case as my example script grows more ambitious... but the numbers will be small, so perhaps enough << and + will work well anyway -- we'll see!). > By using a scripting language rather than assembly or C, you've already > decided you're willing to sacrifice some performance for ease of use. I You might be surprised. Try coding in standard C or assembly a program that must work with integers as large as 52!, without using some finely tuned external library for the pupose, and we'll see what performance you do get...;-). [In practice, multiple-precision arithmetic is NOT trivial to code with decent performance]. Sure, I could use GMP, say -- but then I could use it for Ruby or C just as well, and the variability becomes unbounded. I _am_ interested in comparing standard languages as opposed to languages coupled with any one of a zillion potential add-on libraries, after all. And I'm not interested in blowing up my program by an order of magnitude so I can include in it complete sources for multi-precision arithmetic and hash tables. I want to compare programs of comparable complexity, thus, languages of comparable semantic level -- as indeed Python and Ruby easily prove to be on this problem, with the code in the two languages in such an obvious, nearly 1:1 correspondence. Given that the semantic level is just about the same, performance discrepancies as high as 2 or 3 times are _STILL_ pretty mysterious to me. > expect that both Perl and Python would beat Ruby in nearly any > benchmark, but I find Ruby much easier to use than both. For tasks > where a scripting language is appropriate, I choose Ruby. I know of no tasks where "a scripting language" is NOT appropriate, except operating systems (kernels and drivers), and _accelerators_ for "scripting languages" (and, in the pypy project, we're trying to drop the second proviso, and prove that higher level languages CAN in fact be made intrinsically faster than lower level ones -- but that's a longer-term goal, of course;-). So, anyway, I'm trying to see the "easier to use" part -- which includes, IMHO, performance issues where appropriate. I can easily see it wrt Perl -- the OO, the fact that the language is smaller and more elegant. But where does it come wrt Python, given that _Python_ is the smaller language (no deliberate redundance) and the "elegance" pro's and con's clearly go both ways (Ruby has some very elegant parts, those designed from scratch, but e.g. all of those $_ $` $' $! etc etc that it borrowed from Perl surely can't be considered *elegant*...???)...? That's what I'm trying to find out. Perhaps it's domain dependent and just doesn't show up at all in combinatorial-arithmetic? OK, then once I've exhausted that I'll move on to something else -- after all Python and Ruby are clearly both perfectly general-purpose languages, so it's not as if I'll soon run out of application domains to explore, hm?-) Alex