Вход | Регистрация

Python performance quiz

  • Thumbs_up

    Коя от функциите е най-бърза (на око) и защо?

    N = 10000000
    
    def f1():
        s = ''
        for i in range(N):
            s = s + 'abcdefg'[i % 7]
        return s
    
    def f2():
        s = ''
        for i in range(N):
            s += 'abcdefg'[i % 7]
        return s
    
    def f3():
        a = []
        for i in range(N):
            a.append('abcdefg'[i % 7])
        return ''.join(a)
    
    def f4():
        return ''.join('abcdefg'[i % 7] for i in range(N))
    
    def f5():
        return ''.join(['abcdefg'[i % 7] for i in range(N)])
    10.04.2009 (променeно 10.04.2009)
  • Залагам на f4 :) - с надеждата, че join е реализиран интелигентно като "string builder". Отностно итерацията, залагам на итератора, като най-пестелив.

    10.04.2009 (променeно 10.04.2009)
  • Добър опит =-)

    Поствам кода, с който можеш да видиш цифрите:

    N = 10000000
    
    def f1():
        s = ''
        for i in range(N):
            s = s + 'abcdefg'[i % 7]
        return s
    
    def f2():
        s = ''
        for i in range(N):
            s += 'abcdefg'[i % 7]
        return s
    
    def f3():
        a = []
        for i in range(N):
            a.append('abcdefg'[i % 7])
        return ''.join(a)
    
    def f4():
        return ''.join('abcdefg'[i % 7] for i in range(N))
    
    def f5():
        return ''.join(['abcdefg'[i % 7] for i in range(N)])
    
    import sys
    print(sys.version)
    if sys.version_info[:3] < (3, 0, 0):
        range = xrange
        def timeit(f, number=1000000):
            from timeit import Timer
            return Timer(f.__name__ + '()', 'from __main__ import ' + f.__name__).timeit(number)
    else:
        from timeit import timeit
    
    def mytimeit(f):
        print('timeit(' + f.__name__ + ') = ' + str(timeit(f, number=1)))
    
    mytimeit(f1)
    mytimeit(f2)
    mytimeit(f3)
    mytimeit(f4)
    mytimeit(f5)

    PS: Работи и за по-стари версии на Python.

    10.04.2009 (променeно 12.04.2009)
  • Не съм особено изненадан от резултатите. Бих заложил, че join е оптимизиран за списъци.

    11.04.2009
  • Всъщност, не съм убеден в последното, но с 10 минути мързеливо гледане на кода в Python не открих нищо. Загубих интерес като открих няколко redundant макроса.

    11.04.2009
  • Thumbs_up

    Добре, финално:

    ''.join обръща резултата си до list, освен ако не е list или tuple. С цел да може да изчисли отрано колко дълъг низ му трябва, нали. Създаването на списък от генератор е очевидно по-бавно от създаването на списък от list comprehension. 0.5те секунди разлика (на моята машина) идват точно от там.

    Поуката - premature optimization...

    11.04.2009 (променeно 11.04.2009)
  • Thumbs_up

    Малко по-интересно е тестването на 2.x

    11.04.2009
  • Много яко! А защо за версиите преди 3.0 се налага range = xrange? Така не става ли по-мързеливо => бавно?

    12.04.2009
  • Сегашният range е предишния xrange. Идеята е, че било по-бързо да вика __next__(), отколкото да създава целият списък наведнъж.

    12.04.2009
  • Star Thumbs_up

    @Зорница Костадинова @Марий Йонов За Python 2.x се използва xrange, просто за да бъде честен теста. Дали xrange е по-бърза от range, както ще се опитам да обясня по-долу, не е много ясно =-).

    Ето ги цифрите, които моята машина изкара за Python 3.0:

    slavi@slavi-desktop:~$ python3.0 what.py 
    3.0rc1+ (py3k, Oct 28 2008, 09:22:29) 
    [GCC 4.3.2]
    timeit(f1) = 4.67462801933
    timeit(f2) = 6.26769399643
    timeit(f3) = 7.48840403557
    timeit(f4) = 5.50503206253
    timeit(f5) = 4.57028889656

    Първото, което ми направи впечатление бе, че f1 е осезаемо по-бърза от f2. Единствената разлика между им е в .. = .. + .. и .. += ... Гледайки само кода, бих очаквал двете да са еднакво бързи. (Ако трябва да съм честен докрай, бих очаквал += да е по-бърза, просто заради това, че последните няколко години съм писал главно C++). Цифрите, обаче говорят друго.

    Busted Myth 1: foo += bar е по-бърз (или поне не по-бавен) от foo = foo + bar

    Малко неочаквано. Дори гледайки кода на Python, не можах да си отговоря защо е така (в Python/ceval.c, case BINARY_ADD: и case INPLACE_ADD: са абсолютно идентични).

    В почти всеки турориал за Python, се споменава, че стринговете в Python са immutable и че правилния (разбирай бързия) начин, за конкатениране на низове е ''.join. С други думи, f4 е за предпочитане пред наивния код от f1. Ако, обаче, пак погледнем към цифричките:

    Busted Myth 2: ''.join е по-бърз от 'наивното' конкатениране на низове

    Гледайки само f4 и f5, веднага бих предположил, че f4 е по-бързата, защото не се налага да се заделя (и обхожда) излишно памет. Повечето модерни машини смятат по-бързо, отколкото четат и пишат в паметта. Част от причината, f5 да е по-бърза (както Стефан спомена) е, че ''.join винаги първо разпъва итератора в списък и после изпълнява конкатернирането. Което ни води към:

    Busted Myth 3: Итерирането е по-бързо от строенето на списъци

    Тъжния извод от горните разсъждения е, че в Python не може да се каже нищо за бързодействието на даден код, без да се направи benchmark. Освен, ако не се казваш Guido van Rossum или Димитър Димитров.

    Причината най-вероятно е, че на разработчиците на Python, просто не ги е грижа особено за performance. Все пак, killer app-а за Python днес си остават web приложенията. Ако ви пука за performance пишете C и използвайте ctypes или Python API, за да си викате кода от Python.

    Всеки спор относно, колко е бързо едно или друго в Python е просто ментална матрубация =-)

    12.04.2009 (променeно 13.04.2009)
  • Току що намерих интересно есе от Guido van Rossum. Явно и той има проблеми с предвиждането на Python performance.

    Обмислям да му задраскам името от последния пост =-)

    12.04.2009 (променeно 12.04.2009)
  • Мислех си, да си задържа мнението до вторника, когато ще имам повече време да напиша нещо по въпроса, но не се стърпях за някои неща...

    1. slavi@slavi-desktop:~$ python3.0 what.py -> 3.0rc1+ - Хайде да си update-неш python-a (има един speedup в 3.0.1, който поне при мен работи...)
    2. статията е за python < 3, а python3 и python2* са доста различни по отношение на низовете (If-Modified-Since: Fri, 26 Aug 2005 08:28:55 GMT)
    3. преди да се пише на C е хубаво да си зададем въпроса не може ли алгоритмично да се оптимизира (e.g. 'abcdefg'*(N//7) + 'abcdefg'[:N%7] (да това ще лети))
    4. gc.enable (макар че едва ли ще има голяма разлика за точно тази функция)
    12.04.2009 (променeно 12.04.2009)
  • Ще почна отзад напред:

    4. timeit сам си изключва gc, за да е честен относно какво е измерил.

    3. Абсолютно съм съгласен. Идеята на примера, бе не да се оптимизира, а да илюстрира нещо друго.

    Основния извод в поста, бе че в Python никога не можеш да си сигурен кое колко струва, дори и когато ти се струва очевидно (като foo += bar vs. foo = foo + bar). Това е единственото, което твърдя.

    Не се опитвам да говоря за 'оптимизиране по принцип'. Това е много по-дълбока тема, отколкото искам да навлизам. Искам само да кажа, че Python бидейки един наистина интуитивен език, е абсолютно непредсказуем относно performance, дори и за тривиални примери.

    2. (tongue in cheek) Оттогава нещата са станали само по-бавни.

    1. Използвам версията която идва с моя ОС.

    Мисля си, обаче, че причините за извода, който се опитвам да вкарам са по-дълбоки, от това коя версия на Python ползвам. Цифрите, които 2.x изкарва, са също толкова интересни и изненадващи.

    Въпросът е, дали това което пишеш на Python струва толкова, колкото си мислиш, че струва. Т.е. дали човек може да има модел в главата си отностно performance характеристиките на Python и той да отговарят на това, което наблюдава на benchmark (както в C например). Моят отговор е 'не'.

    12.04.2009 (променeно 12.04.2009)
  • 4. timeit сам си изключва gc, за да е честен относно какво е измерил

    имах впредвид че е хубаво да се види и с включен, макар че на тази функция не би следвало да повлияе с повече от 5% (just guessing)

    2. Оттогава нещата са станали само по-бавни.

    Това си е така (официално py3 е с 10% по-бавен), но имах впредвид че str тогава е ~ bytes сега, а сегашния str е UTF-16, което променя функцията доста, просто реших, че хората могат да се заблудят...

    1. Използвам версията която идва с моя ОС.

    с какво си, gentoo (ако си) уж бързо ги update-ват

    12.04.2009
  • Не, с Убунту 8.10. Отказах 'техничарските' дистрибуции преди години. =-)

    Прав си за str, bytes и Python 2.x. Ама... (как беше 'misses the point' на български).

    12.04.2009 (променeно 12.04.2009)
  • @Стефан Кънев

    Не съм особено изненадан от резултатите.

    Изненадан съм, че не си изненадан. =-)

    12.04.2009
  • Хоп, ето нещо и от мен. Един "известен" пример в няколко варианта. Отново интересното са разликите в производителността. И четирите функции разбиват итератор на n-орки. Първите три взимат елемени докато могат да образуват n-орка с <= n елемента. Последната е с == n елемента.

    from functools import partial
    from itertools import takewhile, zip_longest, islice
    from operator import is_not
    
    def tuples1(n, seq):
        return takewhile(len, map(tuple, iter( partial(islice, iter(seq), n), None )))
    
    def tuples2(n, seq):
        seq = iter(seq)
        return iter(lambda: tuple(islice(seq, n)), ())
    
    NoItem = object()
    def tuples3(n, seq):
        return map(partial(filter, partial(is_not, NoItem)), zip_longest(*n*(iter(seq),), fillvalue=NoItem))
    
    def tuples4(n, seq):
        return zip(*n*(iter(seq),))
    12.04.2009 (променeно 12.04.2009)
  • Малък ъпдейт. Днес попаднах на една страхотнa библиотека за работа с Python bytecode (обаче работи само на Python 2.x).

    Ето как изглежда bytecode-а на f1 и f2:

    slavi@slavi-desktop:~$ python
    Python 2.5.2 (r252:60911, Oct  5 2008, 19:29:17) 
    [GCC 4.3.2] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> from byteplay import *
    >>> from what import *
    >>> print(Code.from_code(f1.func_code).code)
    
      4           1 LOAD_CONST           ''
                  2 STORE_FAST           s
    
      5           4 SETUP_LOOP           to 27
                  5 LOAD_GLOBAL          range
                  6 LOAD_GLOBAL          N
                  7 CALL_FUNCTION        1
                  8 GET_ITER             
    
      5     >>   11 FOR_ITER             to 24
                 12 STORE_FAST           i
    
      6          14 LOAD_FAST            s
                 15 LOAD_CONST           'abcdefg'
                 16 LOAD_FAST            i
                 17 LOAD_CONST           7
                 18 BINARY_MODULO        
                 19 BINARY_SUBSCR        
                 20 BINARY_ADD           
                 21 STORE_FAST           s
                 22 JUMP_ABSOLUTE        to 11
            >>   24 POP_BLOCK            
    
      7     >>   27 LOAD_FAST            s
                 28 RETURN_VALUE         
    
    >>> print(Code.from_code(f2.func_code).code)
    
     10           1 LOAD_CONST           ''
                  2 STORE_FAST           s
    
     11           4 SETUP_LOOP           to 27
                  5 LOAD_GLOBAL          range
                  6 LOAD_GLOBAL          N
                  7 CALL_FUNCTION        1
                  8 GET_ITER             
    
     11     >>   11 FOR_ITER             to 24
                 12 STORE_FAST           i
    
     12          14 LOAD_FAST            s
                 15 LOAD_CONST           'abcdefg'
                 16 LOAD_FAST            i
                 17 LOAD_CONST           7
                 18 BINARY_MODULO        
                 19 BINARY_SUBSCR        
                 20 INPLACE_ADD          
                 21 STORE_FAST           s
                 22 JUMP_ABSOLUTE        to 11
            >>   24 POP_BLOCK            
    
     13     >>   27 LOAD_FAST            s
                 28 RETURN_VALUE         

    Както може да се очаква, единствената разлика е в 20-та инструкция: BINARY_ADD vs INPLACE_ADD. С други думи, хубавата новина е, че f2 не е по-бавна заради bytecode компилатора.

    По-рано написах, че интерпретатора обработва BINARY_ADD и INPLACE_ADD с идентичен код (разбирай copy-paste-нат код). В случая на конкатерниране на стрингове има няколко оптимизации в unicode_concatenate в Python/ceval.c. Хипотезата е, е че в един от двата случая в unicode_concatenate се минава през различен (по-бавен) if/else branch.

    Предлагам да се дадат 15 бонус точки на първия, който разбере и оправи проблема =-).

    21.04.2009 (променeно 21.04.2009)
  • dis ? - то реално и другото ако не се лъжа вика dis

    rattus@rattus-laptop:~$ python3.0 
    Python 3.0.1 (r301:69556, Mar  3 2009, 22:39:11) 
    [GCC 4.2.4 (Ubuntu 4.2.4-1ubuntu3)] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> def f(): print('dada')
    ... 
    >>> import dis
    >>> dis.disassemble(f.__code__)
      1           0 LOAD_GLOBAL              0 (print) 
                  3 LOAD_CONST               1 ('dada') 
                  6 CALL_FUNCTION            1 
                  9 POP_TOP              
                 10 LOAD_CONST               0 (None) 
                 13 RETURN_VALUE 
    21.04.2009 (променeно 21.04.2009)
  • Nice. Не знаех (а трябваше =-), че има нещо подобно in the box.

    На byteplay попаднах случайно. В някакъв блог, пича го ползваше, за да си направи tail recursive call декоратор. Плюса на byteplay май е, че можеш да промениш кода и да го превърнеш отново във функция.

    21.04.2009 (променeно 21.04.2009)
  • ...и ако някой ден решиш да направиш това, определено си объркал езика. Трябва ти някой друг такъв език.

    22.04.2009 (променeно 22.04.2009)
  • Busted Myth 2: ''.join е по-бърз от 'наивното' конкатениране на низове

    При мен при всички тестове (и с 3, и с 2.5) f4 работи по-бързо от f1

    24.04.2009
  • 3.0.1 (r301:69561, Feb 13 2009, 20:04:18) [MSC v.1500 32 bit (Intel)]
    timeit(f1) = 46.0123324897
    timeit(f2) = 46.345843387
    timeit(f3) = 2.48716487324
    timeit(f4) = 2.15105581282
    timeit(f5) = 1.66081634748
    Как си успял да ги постигнеш тези времена на първите 2 функции?

    24.04.2009 (променeно 24.04.2009)
  • Как си успял да ги постигнеш тези времена на първите 2 функции?

    По втория начин =-).

    Шегата настрана, по начина който и ти: просто си пуснах скрипта.

    Хубаво е, че се постват повече цифри от хора с различни машини и версии. По това, което си дал изглежда някой е "работил" по тези проблеми в последните ревизии на Python.

    В момента си ъпгрейдвам дистрибуцията и като завърши ще видя как изглеждат нещата при мен (с каквато версия на Python от Убунту са сметнали за стабилна тоя месец).

    24.04.2009 (променeно 24.04.2009)
  • @Йордан Темелков: Да не би да ти е малко RAM-а, впрочем? Като гледам f1 и f2, има логика те да ядат повече от него. Ако удряш до swap, това би обяснило времето.

    24.04.2009