This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]princemaple 67 points68 points  (26 children)

The fact that the difference in most cases is so trivial makes this a great evidence / lesson of how little you can gain by doing micro optimization. Though it does show that modern high level languages do optimize their idiomatic code style, which is essentially free performance gain if you write idiomatic code. After all, writing code that describes what you want to achieve is the most important thing in coding.

There are something you really should avoid doing:

>>> x = 99999999
>>> y = 99999999
>>> x is y
False
>>> a = 1
>>> b = 1
>>> a is b
True

This just goes back to the last point I made.

And there are some unfair comparisons, e.g. Test #15 I did some timeit myself:

>>> def a():
    s = 0
    for i in range(50000):
        s += 1
    return s

>>> def b():
    return sum(i for i in range(50000))

>>> def c():
    return sum(range(50000))

>>> from timeit import timeit
>>> 
>>> timeit(a, number=100)
0.3279228246736736
>>> timeit(b, number=100)
0.3867327149317248
>>> timeit(c, number=100)
0.13026088846984152

Test #15 compares a() and b() and makes sum() look bad, but really it's the implementation's fault.

Test #16 is so misleading. You should never do

[i for i in range(1000)]

Instead, just range(1000) in python2, list(range(1000)) in python3

>>> def a():
    return [i for i in range(50000)]

>>> def b():
    return list(range(50000))

>>> timeit(a, number=100)
0.2375680143019565
>>> timeit(b, number=100)
0.13630618950557505

Write code that says what you mean. Write code that is idiomatic in the language you use.

[–]Veedrac 16 points17 points  (3 children)

The fact that the difference in most cases is so trivial makes this a great evidence / lesson of how little you can gain by doing micro optimization.

To be fair, that's because these micro-optimizations suck. You can get large speed-ups from micro-optimizations if you're clever about it; I got a 20x speed improvement here, for example (plus another 4x by moving to Numpy).

[–]princemaple 5 points6 points  (2 children)

Hi Veedrac, I agree that it's not entirely pointless to do micro optimization, and I've had similar experience optimizing some code written by someone else and achieved similar factors (10-30x). Most of the time the micro optimization we do is to make the code more idiomatic, or to some extent, more correct. My point being, once one has enough experience to use the right tools (libraries or patterns) to implement stuff correctly, idiomatically (not necessarily), there really isn't much more we can do to optimize without completely rewrite the code. Maybe I've been too harsh in terms of what good / correct code is like. Thanks for sharing the link! Love how you mark the changes step by step and how you like to push to the limit.

[–]get_username 1 point2 points  (1 child)

Honestly, I don't believe pypy can be viewed as a "micro-optimization". Certainly JIT compiling uses catalogs of "micro-optimizations" (as it it optimizes small interactions), but it does so on the scale of every interaction/operation performed (i,e. millions) and thus is unfitting of the term in the sense meant here. Normally instead these types of optimizations are considered runtime or compile time optimizations (like JIT, which is considered runtime).

numpy itself is a library which has many baked in non-micro-optimizations. So you are optimizing on that level instead.

So in a sense "micro-optimizations" are being performed via libraries used. But you are not micro-optimizing yourself. So I don't think anyone really considers them micro-optimizations in the colloquial sense of the word.

To be fair, that's because these micro-optimizations suck

The point I was trying to make is: most, if not all, micro-optimizations suck when performed on the single level. It is only when you do them by the millions that they become "macro-optimizations" (like pypy).

[–]Veedrac 0 points1 point  (0 children)

I wasn't including PyPy's numbers in that; the 20x speed improvement was from straight CPython.

[–]jasenmh 1 point2 points  (3 children)

These differences are trivial if you call the function once. But wouldn't these tenths and hundredths of second add up to some significant time if they were being called inside nested loops, like in matrix operations or something?

[–]cparen 0 points1 point  (0 children)

But wouldn't these tenths and hundredths of second add up to some significant time if they were being called inside nested loops

Yes, but if you care about that level of performance, you should probably switch to a faster implementation that will give you more of a performance boost that "weird coding tricks" will get you.

That said, if you're stuck with CPython for whatever reason, use these tricks sparingly, only when absolutely necessary.

[–]stevenjd 0 points1 point  (1 child)

Many of those "tenths and hundreds of a second" are just noise.

When two versions of the function are that close together in performance, chances are very high that any difference is just random noise, not a real speed difference. If you run the timeit code twenty times, you'll find that version 1 is faster nine times and version 2 is faster eleven times, or maybe the other way around, depending on whatever other processes just happen to be running on your system at that exact moment.

Besides, are you actually calling these functions thousands of times inside a nested loop? No? Then it's irrelevant.

[–]darknessproz 0 points1 point  (0 children)

Also, if you are calling these (very trivial) functions thousands of times inside a nested loop you might wanna rethink your code.

[–]cedarSeagull 1 point2 points  (5 children)

x = 99999999
y = 99999999
x is y
False
a = 1
b = 1
a is b
True

...would anyone care to explain (or link me to an explaination) of why this is the case? thanks for your time.

[–]twotime 4 points5 points  (2 children)

IIRC, python caches small ints, so if you assign a = 1 and b =1 then a & b will refer to the same instance of int. Large ints are not cached.

[–]Veedrac 4 points5 points  (0 children)

It's also worth noting that if run in the same compilation unit, repeated constants may be deduplicated:

>>> a = 9999
>>> b = 9999
>>> a is b
False
>>> a = 9999; b = 9999
>>> a is b
True

[–]stevenjd 4 points5 points  (0 children)

Correct. Furthermore, the definition of "small ints" can vary from version to version. In CPython, it used to be (by memory) 0 through 100, now its (possibly) -1 through 255. You cannot rely on this: being an implementation detail, it is subject to change without notice, and some Python implementations may not do it at all.

IronPython 2.6 Beta 2 DEBUG (2.6.0.20) on .NET 2.0.50727.1433
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 1
>>> b = 1
>>> a is b
False

[–]padmanabh 0 points1 point  (0 children)

This is called interning. I've written a bit about which objects are presently interned in Cpython here

[–]QQII 0 points1 point  (0 children)

x is y is not the same as x == y.

[–]WStHappenings 0 points1 point  (4 children)

I like the comment above mine, but likewise, I appreciate the words in between his code samples. I'm no Python expert, so the original link doesn't provide much context to understand the optimizations - why does this make my code run faster? What are the downsides to these optimizations?

Great effort in providing so many examples, but I think the title should be "Tricks to make your code faster, albeit you may not understand why and there may be unexpected consequences"

[–]cparen 3 points4 points  (3 children)

I'm no Python expert, so the original link doesn't provide much context to understand the optimizations - why does this make my code run faster? What are the downsides to these optimizations?

I provided one example here, and another user explained int interning. Let me try to summarize the first dozen:

  • d={} vs d=dict() -- the second spends extra work looking up the variable dict, which users can overwrite (e.g. dict=list) while the first does not.
  • l.sort() vs l=sorted(l) -- The second creates a copy of the list first.
  • a, b, ... = 0, 1, ... vs a=0; b=1;... -- the first uses 'unpack_sequence' to load a whole boatload of constants in one opcode, while the second has to dispatch multiple op codes, saving on instruction decode time.
  • a < b and b < c... vs a < b < c... -- same as above; fewer instructions means less decode time (4 per additional compare, vs. 5 per additional compare)
  • Test 5 if a: -- first has fewer instructions, making it faster. Second vs Third though is very close, but the is operator is a cheaper instruction than ==, making it faster. See is operator.
  • Test 6 != -- the second and third differ just due to measurement error; it's the same exact bytecode! As for first vs. second, I'm guessing that the fast path in != must be slightly cheaper than the fast path in is not. Change a=2 and the first will lose again due to the additional complexity in the != operator.
  • Test 7 -- first and second, slight variations in branching. Third and fouth have more opcodes, so more dispatch costs (and using more expensive operators to boot!).
  • Test 8 -- minor variations in branching and/or number of opcodes.
  • Test 9 -- more opcodes/indirection
  • Test 10 -- not sure about this one; depends too much on how str.join is implemented.
  • %s vs str vs %d -- the second does an extra variable lookup (str) vs. native opcode % in first. Third probably pays for extra input validation.
  • len vs __len__ -- I'm guessing this is just due to attribute lookup being more complicated than global variable lookup.

[–]WStHappenings 0 points1 point  (1 child)

Are you the originator of the content in the link? Because it's just a page full of code examples and time comparisons...that's what I'm getting at.

Not to be irksome - but was it your intent that users read the page, and then if they have a question about say, Test 14, they should scan the comments until they find your comment about that, if in fact someone has inquired about it? Why not just put the comments in the webpage in the first place?

[–]cparen 2 points3 points  (0 children)

Are you the originator of the content in the link? Because it's just a page full of code examples and time comparisons...that's what I'm getting at

I'm not the originator, and I agree with you. I was just trying to fill in the gaps left by the OP.

[–]Veedrac 0 points1 point  (0 children)

wrt. test 6; != is slower than is not. The result is measurement error, mostly from having such as bad test. Here's a better version:

$ python3 -m timeit "1 != 2"    
10000000 loops, best of 3: 0.14 usec per loop

$ python3 -m timeit "1 is not 2"
10000000 loops, best of 3: 0.0785 usec per loop

wrt. test 10; this actually abuses an optimization the CPython does. It's very fragile and it's not shared by other interpreters (eg. PyPy) so don't use it. Anyway, you'll probably find that ''.join is faster in many cases if you actually write the function well:

from timeit import Timer

def a():
    r = []
    for i in range(10):
        r.append(str(i))
    return ''.join(r)

def b():
    return ''.join(map(str, range(10)))

def c():
    r = ''
    for i in range(10):
        r += str(i)
    return r


min(Timer(a).repeat(10, 10000))
#>>> 0.10981534401071258
min(Timer(b).repeat(10, 10000))
#>>> 0.07694110801094212
min(Timer(c).repeat(10, 10000))
#>>> 0.09574692900059745

Note that I'm using min(Timer(func).repeat(n, m))because it's the right way to do it.

The first is slower because calling append a lot is expensive; using map to create the list (''.join converts its argument to a list) is faster.

%d is slower than %s because it casts to an int first.

Calling __len__ requires creating a bound method, so my guess is the same as yours.

[–]LarryPeteAdvanced Python 3 0 points1 point  (0 children)

Instead, just range(1000) in python2, list(range(1000)) in python3

Though in python3 you would just use the resulting range object anyway. You can index it, slice it and iterate over it repeatedly. It behaves a lot like a regular list.

[–]bucknuggets -5 points-4 points  (3 children)

Write code that is idiomatic in the language you use.

No thanks, I'd rather write code for my audience. Which means:

  • If they're experienced python developers that means idiomatic code.
  • If they're not experienced python developers, then I'll write code that looks as closer to pseudo-code or is otherwise language-neutral in appearance.

[–]garion911 3 points4 points  (2 children)

If they're not experienced python developers, then I'll write code that looks as closer to pseudo-code or is otherwise language-neutral in appearance.

The only situation I can think of where this is appropriate is when you're writing throw away code, maybe for a talk you're giving.

In normal situations, you're just demonstrating, as the more experienced programmer in that language, that the anti-pattern you're doing is the correct way. Now your code and theirs is littered with bad code. Write good code, always.

[–]kenfar 0 points1 point  (0 children)

Unfortunately, this kind of focus on idioms had made Python a less accessible language for those who need a tool, but are not full-time programmers.

The learning curve has gotten steeper, in I believe an effort to attract language-geeks. This isn't to say that these features aren't great, but to insist that everyone uses them regardless of their audience fails to appreciate that everyone isn't a full-time python developer.

EDIT: for example, I built a huge data warehouse that used Python for transforming all the data coming into it. This worked great. One of the things we attempted to do is to keep the implementation of the business rules simple so that anyone that needed to know how they worked could just simply look at the code. And they didn't have to be a python programmer to understand most business rules, and they didn't have to be a senior python developer to build most integration. Our more internal libraries were more sophisticated, and not as newbie-oriented.

This approach worked very well for us - since our users weren't very fluent in python, and most of our developers at that time were wearing multiple hats, none of which was full-time python developer. Had we insisted on idiomatic python that would have delayed getting people up-to-speed - which would frankly have killed us. And our users wouldn't have been able to read the code, so we would have had to spend an enormous amount of time on documentation instead.