DEV Community

AheadKit
AheadKit

Posted on

I Rewrote the Same Function 3 Ways. The Fastest One Wasn't Worth It

Sum the squares of the even numbers in a list. That is the whole job. I wrote it three ways this morning, ran all three 200 times each on a list of 10,000 numbers, and the result embarrassed the version I was rooting for.

The three versions

Shortest first, the one-liner I would have written in a hurry:

def shortest(nums):
    return sum(x * x for x in nums if x % 2 == 0)
Enter fullscreen mode Exit fullscreen mode

Clearest second, the plain loop I would hand to a junior dev:

def clearest(nums):
    total = 0
    for x in nums:
        if x % 2 == 0:
            total += x * x
    return total
Enter fullscreen mode Exit fullscreen mode

Fastest third, the one with the bit trick. x & 1 instead of x % 2, because everyone knows modulo is slow and bitwise is fast:

def fastest(nums):
    total = 0
    for x in nums:
        if not x & 1:
            total += x * x
    return total
Enter fullscreen mode Exit fullscreen mode

Same input, same answer. I asserted that before timing anything, because a fast wrong answer is just a fast bug.

The real numbers

Here is the actual output from my machine, timeit, 200 runs each:

shortest  result=166616670000  200 runs in 0.103s  (0.514 ms/run)
clearest  result=166616670000  200 runs in 0.100s  (0.499 ms/run)
fastest   result=166616670000  200 runs in 0.099s  (0.497 ms/run)
all three agree: True
Enter fullscreen mode Exit fullscreen mode

Read those timings again. The "fastest" version beat the clearest one by 0.002 milliseconds per run. Two microseconds. On a list of 10,000 numbers. If this function ran a million times a day, the bit trick would save you two seconds, and you would spend far longer than that explaining x & 1 to the next person who reads it.

I ran the timing twice to be sure, and the ordering wobbled. Some runs the one-liner won. The differences are noise. At this scale there is no fastest version. There are three versions of the same speed, and one of them is harder to read for nothing.

Which one I would keep

The clearest one. Not because loops are morally better, and not because I have anything against one-liners. Because of what happens six months from now, when someone opens this file at 11 p.m. because a billing total is wrong.

They will read the loop version in one pass. total starts at zero, evens get squared and added, done. The one-liner takes two passes: first you parse the generator, then you check the filter. It is not hard. It is just slower to read, and reading is what code spends most of its life doing.

The bit-trick version is the worst of the three. It saves nothing measurable and it makes the reader stop and think about binary. Every reader who stops is a reader who might misread it. if not x & 1 is the kind of line that looks clever in a pull request and confusing in a production incident.

I know the counterargument. Someone will say the bit trick matters in a hot loop, on big data. Fine. That is a different function in a different program, and you would measure it there. In this function, on this data, the trick bought two microseconds and cost a reader's attention. Bad trade.

The habit this broke

I used to reach for the clever version first. It felt like the job. What I actually shipped was code that only I could read quickly, and even I slowed down on it a month later.

Now I write the boring version first and only get clever when a measurement tells me to. The measurement has to be real, on real data, with a real difference. "Everyone knows modulo is slow" is not a measurement. It is a rumour with a long career.

Try it on your own code. Pick a function you think is slow, write the plain version next to the clever one, and time both. Half the time you will find what I found: the clever version wins by an amount nobody will ever feel, and the plain version wins every time someone has to read it.

Top comments (0)