oh, the dangers of over-optimizing a small bit of code

I use the (excellent) Python library dogpile.cache quite extensively. One web project can make hundreds of calls to the cache per page, so it’s often one of the first places I look to maximize performance.

I have one bit of code that uses the key_mangler functionality manipulate the keys used for a Redis data store.

An overly simplified example of using this is generating the key_mangler to “prefix” keys, and looks something like this:

LIB_1 = {}
for i in 'abcdefg':
    def func(_key):
        tmpl = "%s|%%s" % i
        return tmpl % _key
    LIB_1[i] = func

If we iterate them, you can see the output…

for i, func in LIB_1.items():
    print(i, func("Hello."))

a a|Hello.
c c|Hello.
b b|Hello.
e e|Hello.
d d|Hello.
g g|Hello.
f f|Hello.

While speeding this up a bit, I lazily moved the tmpl variable out of the block and into this general form:

LIB_2 = {}
for i in 'abcdefg':
    tmpl = "%s|%%s" % i
    def func(_key):
        return tmpl % _key
    LIB_2[i] = func

The result was this had quite the speedup – twice the speed of the original version. But… there was a huge caveat – all my unit tests broke. Digging into things, the following is the output of iterating over the same info:

for i, func in LIB_2.items():
    print(i, func("Hello."))

a g|Hello.
c g|Hello.
b g|Hello.
e g|Hello.
d g|Hello.
g g|Hello.
f g|Hello.

I was lazy when i rewrote the function, and did not expect Python to scope tmpl like that. It makes perfect sense, I know Python behaves like this, and I’ve taken this exact implementation detail into account before… I just didn’t expect it at the time.

Since I knew how/why this happened, the fix was simple — hinting the def with scope I want…

LIB_3 = {}
for i in 'abcdefg':
    tmpl = "%s|%%s" % i
    def func(_key, tmpl=tmpl):
        return tmpl % _key
    LIB_3[i] = func

This 3rd variant runs about the same speed as the second variant… which is around twice the speed of the original method, where the tmpl is assigned on each invocation.

for i, func in LIB_3.items():
    print(i, func("Hello."))

a a|Hello.
c c|Hello.
b b|Hello.
e e|Hello.
d d|Hello.
g g|Hello.
f f|Hello.

Leave a Reply

Your email address will not be published. Required fields are marked *