Unusual Speed Difference between Python and C++

后端 未结 17 2240
庸人自扰
庸人自扰 2020-12-22 21:25

I recently wrote a short algorithm to calculate happy numbers in python. The program allows you to pick an upper bound and it will determine all the happy numbers below it.

17条回答
  •  旧巷少年郎
    2020-12-22 22:02

    This is my second answer; which caches things like sum of squares for values <= 10**6:

            happy_list[sq_list[x%happy_base] + sq_list[x//happy_base]]
    

    That is,

    • the number is split into 3 digits + 3 digits
    • the precomputed table is used to get sum of squares for both parts
    • these two results are added
    • the precomputed table is consulted to get the happiness of number:

    I don't think Python version can be made much faster than that (ok, if you throw away fallback to old version, that is try: overhead, it's 10% faster).

    I think this is an excellent question which shows that, indeed,

    • things that have to be fast should be written in C
    • however, usually you don't need things to be fast (even if you needed the program to run for a day, it would be less then the combined time of programmers optimizing it)
    • it's easier and faster to write programs in Python
    • but for some problems, especially computational ones, a C++ solution, like the ones above, are actually more readable and more beautiful than an attempt to optimize Python program.

    Ok, here it goes (2nd version now...):

    #!/usr/bin/env python3
    '''Provides slower and faster versions of a function to compute happy numbers.
    
    slow_happy() implements the algorithm as in the definition of happy
    numbers (but also caches the results).
    
    happy() uses the precomputed lists of sums of squares and happy numbers
    to return result in just 3 list lookups and 3 arithmetic operations for
    numbers less than 10**6; it falls back to slow_happy() for big numbers.
    
    Utilities: digits() generator, my_timeit() context manager.
    
    '''
    
    
    from time import time  # For my_timeit.
    from random import randint # For example with random number.
    
    upperBound = 10**5  # Default value, can be overridden by user.
    
    
    class my_timeit:
        '''Very simple timing context manager.'''
    
        def __init__(self, message):
            self.message = message
            self.start = time()
    
        def __enter__(self):
            return self
    
        def __exit__(self, *data):
            print(self.message.format(time() - self.start))
    
    
    def digits(x:'nonnegative number') -> "yields number's digits":
        if not (x >= 0): raise ValueError('Number should be nonnegative')
        while x:
            yield x % 10
            x //= 10
    
    
    def slow_happy(number, known = {1}, happies = {1}) -> 'True/None':
        '''Tell if the number is happy or not, caching results.
    
        It uses two static variables, parameters known and happies; the
        first one contains known happy and unhappy numbers; the second 
        contains only happy ones.
    
        If you want, you can pass your own known and happies arguments. If
        you do, you should keep the assumption commented out on the 1 line.
    
        '''
        # This is commented out because <= is expensive.
        # assert {1} <= happies <= known 
    
        if number in known:
            return number in happies
    
        history = set()
        while True:
            history.add(number)
            number = sum(x**2 for x in digits(number))
            if number in known or number in history:
                break
    
        known.update(history)
        if number in happies:
            happies.update(history)
            return True
    
    
    # This will define new happy() to be much faster ------------------------.
    
    with my_timeit('Preparation time was {0} seconds.\n'):
    
        LogAbsoluteUpperBound = 6 # The maximum possible number is 10**this.
        happy_list = [slow_happy(x)
                      for x in range(81*LogAbsoluteUpperBound + 1)]
        happy_base = 10**((LogAbsoluteUpperBound + 1)//2)
        sq_list = [sum(d**2 for d in digits(x))
                   for x in range(happy_base + 1)]
    
        def happy(x):
            '''Tell if the number is happy, optimized for smaller numbers.
    
            This function works fast for numbers <= 10**LogAbsoluteUpperBound.
    
            '''
            try:
                return happy_list[sq_list[x%happy_base] + sq_list[x//happy_base]]
            except IndexError:
                return slow_happy(x)
    
    # End of happy()'s redefinition -----------------------------------------.
    
    
    def calcMain(print_numbers, upper_bound):
        happies = [x for x in range(upper_bound + 1) if happy(x)]
        if print_numbers:
            print(happies)
    
    
    if __name__ == '__main__':
        while True:
    
            upperBound = eval(input(
                "Pick an upper bound [{0} default, 0 ends, negative number prints]: "
                .format(upperBound)).strip() or repr(upperBound))
            if not upperBound:
                break
    
            with my_timeit('This computation took {0} seconds.'):
                calcMain(upperBound < 0, abs(upperBound))
    
            single = 0
            while not happy(single):
                single = randint(1, 10**12)
            print('FYI, {0} is {1}.\n'.format(single,
                        'happy' if happy(single) else 'unhappy')) 
    
        print('Nice to see you, goodbye!')
    

提交回复
热议问题