last day (15 days later) » 

10:47 AM
Just to be clear: your proposed solution is still the fastest one. I was just looking for reasons for the speed-up.
 
What do you mean? "Reason for the speed up" as in how I did it, or why?
 
I originally also had a solution with no if that would use an intermediate table with i % 2 as index, but the performance boost was negligible. So I was interested which of the parts you left out brought the extra performance. At the moment, it looks like the enumerate plays the most important role.
 
It's hard to say, considering you have to have i % 2 and enumerate or an equivalent zip.
 
I modified your solution to include just an unnecessary enumerate like such:
```
@classmethod
def calculate_peil_crooked(cls, input_):
"""Calculate the check digit using Luhn's algorithm"""
sum_ = 0
for i, (digit, lut) in enumerate(zip(
reversed(input_),
itertools.cycle([cls.SUM_DOUBLE_MOD10_LUT,
cls.SUM_MOD10_LUT]))):
sum_ = lut[sum_][digit]
return cls.CHECK_DIGIT_LUT[sum_]
```
Oh... does not work to well :D
 
Press CTRL-k
or preindent with 4 spaces
 
11:00 AM
@classmethod
def calculate_peil_crooked(cls, input_):
    """Calculate the check digit using Luhn's algorithm"""
    sum_ = 0
    for i, (digit, lut) in enumerate(zip(
            reversed(input_),
            itertools.cycle([cls.SUM_DOUBLE_MOD10_LUT,
                             cls.SUM_MOD10_LUT]))):
        sum_ = lut[sum_][digit]
    return cls.CHECK_DIGIT_LUT[sum_]
Thanks.
This lead to the performance loss I mentioned below your answer.
 
That's just going to waste CPU cycles and so is OFC slower. If you add a i % 2 it's going to be slower too. Since you're wasting CPU cycles it's not accurate to measure, unless you're measuring against other things that have the same wasted cycles. Give me 2 mins and I'll have some code.
def luhn_peil_list(cls, input_):
    sum_ = 0
    for i, digit in enumerate(reversed(input_)):
        if i % 2:
            sum_ = cls.SUM_MOD10_LUT[sum_][digit]
        else:
            sum_ = cls.SUM_DOUBLE_MOD10_LUT[sum_][digit]
    return cls.CHECK_DIGIT_LUT[sum_]


def luhn_peil_without_if(cls, input_):
    tables = [cls.SUM_DOUBLE_MOD10_LUT, cls.SUM_MOD10_LUT]
    sum_ = 0
    for i, digit in enumerate(reversed(input_)):
        sum_ = tables[i % 2][sum_][digit]
    return cls.CHECK_DIGIT_LUT[sum_]
LuhnPeilList 0.281
LuhnPeilWithoutIf 0.254
LuhnPeilWithoutIfEnumerate 0.29
Luhn 0.212
This shows that removing the if leads to a 0.027 speedup. Changing from enumerate to zip however is slower than with the speedup. So enumerate is faster.
Given that I don't know a way to get between LuhnPeilWithoutIf and Luhn that is only one 'step' (change) then it's hard to accurately say what is causing the performance increase or decrease.
But on the whole we know that Luhn is faster, we know that's because we've gotten rid of the if but we don't know what part of the change from i % 2 to zip(..., cycle(...)) makes it faster.
Given that the enumerate is faster.
 
luhn_peil_without_if is basically exactly what I came up with to get rid of the if. The only difference was that tables was defined as class variable and not in the method.
calculate_lut_overkill == Luhn in the timings?
 
@AlexV Yes, sorry didn't rename that one
 
11:17 AM
@Peilonrayz: No problem.
My assumption was that the amount of time *wasted* when adding that extra `enumerate` would be an appropriate estimation on how the performance would be when used properly.
 
Yeah formatting doesn't work on multi-line messages. :/
 
Learned so much things about the chat today ;-)
 
Actually I think I've figured out the step, one sec
 
Cool. I was just thinking through your line of arguments to see if I would arrive at the same conclusion regarding enumerate.
 
def luhn_peil_list(cls, input_):
    sum_ = 0
    for i, digit in enumerate(reversed(input_)):
        if i % 2:
            sum_ = cls.SUM_MOD10_LUT[sum_][digit]
        else:
            sum_ = cls.SUM_DOUBLE_MOD10_LUT[sum_][digit]
    return cls.CHECK_DIGIT_LUT[sum_]


def luhn_peil_without_if(cls, input_):
    tables = [cls.SUM_DOUBLE_MOD10_LUT, cls.SUM_MOD10_LUT]
    sum_ = 0
    for i, digit in enumerate(reversed(input_)):
        sum_ = tables[i % 2][sum_][digit]
    return cls.CHECK_DIGIT_LUT[sum_]
LuhnPeilList 0.279
LuhnPeilWithoutIf 0.252
LuhnPeilWithoutIfEnumerate 0.288
LuhnPeilWithoutIfMod 0.23
LuhnPeilWithoutIfModEnumerate 0.208
Luhn 0.213
The difference between LuhnPeilWithoutIfMod and LuhnPeilWithoutIfModEnumerate is that table[i] is slow in Python, but fast in C. The speed increase outweighs the speed increase enumerate has over zip.
 
11:35 AM
luhn_peil_without_if_mod_enumerate would basically be the implementation of what you mentioned in your answer at "It doesn't matter too much whether you build the list with [] * len(input_) or [...]", correct?
 
Yes, it is actually the same as LuhnPeilAltTables.
They fluctuate from about 0.205 - 0.215. But they are roughly the same
 
11:59 AM
Now I'm wondering why building the whole list would be as fast as a generator/iterator I assume itertools.cycle to be. The itertools.cycle C implementation looks quite straightforward (for C code) and required auxiliary storage should be minimal. I will have to check for larger inputs. But that's a question for another day.
Anyway, your analysis was pretty interesting. It would be great if some of this would find it's way into the answer. I will have to check how to award a bounty to an existing answer. You have definitely earned it!
 
12:22 PM
@AlexV I can't imagine they're too different. The loop for building the list is in C and iterating over both is in C. If we'd manually built tables with tables.append() then performance would be terrible.
 

last day (15 days later) »