company_banner

Разработка чрезвычайно быстрых программ на Python

Автор оригинала: Martin Heinz
  • Перевод
Ненавистники Python всегда говорят, что одной из причин того, что они не хотят использовать этот язык, является то, что Python — это медленно. Но то, что некая программа, независимо от используемого языка программирования, может считаться быстрой или медленной, очень сильно зависит от разработчика, который её написал, от его знаний и от умения создавать оптимизированный и высокопроизводительный код.



Автор статьи, перевод которой мы сегодня публикуем, предлагает доказать то, что те, кто называет Python медленным, неправы. Он хочет рассказать о том, как улучшить производительность Python-программ и сделать их по-настоящему быстрыми.

Измерение времени и профилирование


Прежде чем приступить к оптимизации какого-либо кода, сначала надо выяснить то, какие его части замедляют всю программу. Иногда узкое место программы может быть очевидным, но если программист не знает, где оно находится, он может воспользоваться некоторыми возможностями по его выявлению.

Ниже представлен код программы, который я буду использовать в демонстрационных целях. Он взят из документации к Python. Этот код возводит e в степень x:

# slow_program.py
from decimal import *

def exp(x):
    getcontext().prec += 2
    i, lasts, s, fact, num = 0, 0, 1, 1, 1
    while s != lasts:
        lasts = s
        i += 1
        fact *= i
        num *= x
        s += num / fact
    getcontext().prec -= 2
    return +s

exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))

Самый лёгкий способ «профилирования» кода


Для начала рассмотрим самый простой способ профилирования кода. Так сказать, «профилирование для ленивых». Он заключается в использовании команды Unix time:

~ $ time python3.8 slow_program.py

real 0m11,058s
user 0m11,050s
sys  0m0,008s

Такое профилирование вполне может дать программисту некие полезные сведения — в том случае, если ему нужно замерить время выполнения всей программы. Но обычно этого недостаточно.

Самый точный способ профилирования


На другом конце спектра методов профилирования кода лежит инструмент cProfile, который даёт программисту, надо признать, слишком много сведений:

~ $ python3.8 -m cProfile -s time slow_program.py
         1297 function calls (1272 primitive calls) in 11.081 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        3   11.079    3.693   11.079    3.693 slow_program.py:4(exp)
        1    0.000    0.000    0.002    0.002 {built-in method _imp.create_dynamic}
      4/1    0.000    0.000   11.081   11.081 {built-in method builtins.exec}
        6    0.000    0.000    0.000    0.000 {built-in method __new__ of type object at 0x9d12c0}
        6    0.000    0.000    0.000    0.000 abc.py:132(__new__)
       23    0.000    0.000    0.000    0.000 _weakrefset.py:36(__init__)
      245    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        2    0.000    0.000    0.000    0.000 {built-in method marshal.loads}
       10    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap_external>:1233(find_spec)
      8/4    0.000    0.000    0.000    0.000 abc.py:196(__subclasscheck__)
       15    0.000    0.000    0.000    0.000 {built-in method posix.stat}
        6    0.000    0.000    0.000    0.000 {built-in method builtins.__build_class__}
        1    0.000    0.000    0.000    0.000 __init__.py:357(namedtuple)
       48    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap_external>:57(_path_join)
       48    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap_external>:59(<listcomp>)
        1    0.000    0.000   11.081   11.081 slow_program.py:1(<module>)

Тут мы запускаем исследуемый скрипт с использованием модуля cProfile и применяем аргумент time. В результате строки вывода упорядочены по внутреннему времени (cumtime). Это даёт нам очень много информации. На самом деле то, что показано выше, это лишь около 10% вывода cProfile.

Проанализировав эти данные, мы можем увидеть, что причиной медленной работы программы является функция exp (вот уж неожиданность!). После этого мы можем заняться профилированием кода, используя более точные инструменты.

Исследование временных показателей выполнения конкретной функции


Теперь мы знаем о том месте программы, куда нужно направить наше внимание. Поэтому мы можем решить заняться исследованием медленной функции, не профилируя другой код программы. Для этого можно воспользоваться простым декоратором:

def timeit_wrapper(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()  # В качестве альтернативы тут можно использовать time.process_time()
        func_return_val = func(*args, **kwargs)
        end = time.perf_counter()
        print('{0:<10}.{1:<8} : {2:<8}'.format(func.__module__, func.__name__, end - start))
        return func_return_val
    return wrapper

Этот декоратор можно применить к функции, которую нужно исследовать:

@timeit_wrapper
def exp(x):
    ...


print('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time'))
exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))

Теперь после запуска программы мы получим следующие сведения:

~ $ python3.8 slow_program.py
module     function   time
__main__  .exp      : 0.003267502994276583
__main__  .exp      : 0.038535295985639095
__main__  .exp      : 11.728486061969306

Тут стоит обратить внимание на то, какое именно время мы планируем измерять. Соответствующий пакет предоставляет нам такие показатели, как time.perf_counter и time.process_time. Разница между ними заключается в том, что perf_counter возвращает абсолютное значение, в которое входит и то время, в течение которого процесс Python-программы не выполняется. Это значит, что на этот показатель может повлиять нагрузка на компьютер, создаваемая другими программами. Показатель process_time возвращает только пользовательское время (user time). В него не входит системное время (system time). Это даёт нам только сведения о времени выполнения нашего процесса.

Ускорение кода


А теперь переходим к самому интересному. Поработаем над ускорением программы. Я (по большей части) не собираюсь показывать тут всякие хаки, трюки и таинственные фрагменты кода, которые волшебным образом решают проблемы производительности. Я, в основном, хочу поговорить об общих идеях и стратегиях, которые, если ими пользоваться, могут очень сильно повлиять на производительность. В некоторых случаях речь идёт о 30% повышении скорости выполнения кода.

▍Используйте встроенные типы данных


Использование встроенных типов данных — это совершенно очевидный подход к ускорению кода. Встроенные типы данных чрезвычайно быстры, в особенности — если сравнить их с пользовательскими типами, вроде деревьев или связных списков. Дело тут, в основном, в том, что встроенные механизмы языка реализованы средствами C. Если описывать нечто средствами Python — нельзя добиться того же уровня производительности.

▍Применяйте кэширование (мемоизацию) с помощью lru_cache


Кэширование — популярный подход к повышению производительности кода. О нём я уже писал, но полагаю, что о нём стоит рассказать и здесь:

import functools
import time

# кэширование до 12 различных результатов
@functools.lru_cache(maxsize=12)
def slow_func(x):
    time.sleep(2)  # Имитируем длительные вычисления
    return x

slow_func(1)  # ... ждём 2 секунды до возврата результата
slow_func(1)  # результат уже кэширован - он возвращается немедленно!

slow_func(3)  # ... опять ждём 2 секунды до возврата результата

Вышеприведённая функция имитирует сложные вычисления, используя time.sleep. Когда её в первый раз вызывают с параметром 1 — она ждёт 2 секунды и возвращает результат только после этого. Когда же её снова вызывают с тем же параметром, оказывается, что результат её работы уже кэширован. Тело функции в такой ситуации не выполняется, а результат возвращается немедленно. Здесь можно найти примеры применения кэширования, более близкие к реальности.

▍Используйте локальные переменные


Применяя локальные переменные, мы учитываем скорость поиска переменной в каждой области видимости. Я говорю именно о «каждой области видимости», так как тут я имею в виду не только сопоставление скорости работы с локальными и глобальными переменными. На самом деле, разница в работе с переменными наблюдается даже, скажем, между локальными переменными в функции (самая высокая скорость), атрибутами уровня класса (например — self.name, это уже медленнее), и глобальными импортированными сущностями наподобие time.time (самый медленный из этих трёх механизмов).

Улучшить производительность можно, используя следующие подходы к присваиванию значений, которые несведущему человеку могут показаться совершенно ненужными и бесполезными:

#  Пример #1
class FastClass:

    def do_stuff(self):
        temp = self.value  # это ускорит цикл
        for i in range(10000):
            ...  # Выполняем тут некие операции с `temp`

#  Пример #2
import random

def fast_function():
    r = random.random
    for i in range(10000):
        print(r())  # здесь вызов `r()` быстрее, чем был бы вызов random.random()

▍Оборачивайте код в функции


Этот совет может показаться противоречащим здравому смыслу, так как при вызове функции в стек попадают некие данные и система испытывает дополнительную нагрузку, обрабатывая операцию возврата из функции. Однако эта рекомендация связана с предыдущей. Если вы просто поместите весь свой код в один файл, не оформив в виде функции, он будет выполняться гораздо медленнее из-за использования глобальных переменных. Это значит, что код можно ускорить, просто обернув его в функцию main() и один раз её вызвав:

def main():
    ...  # Весь код, который раньше был глобальным

main()

▍Не обращайтесь к атрибутам


Ещё один механизм, способный замедлить программу — это оператор точка (.), который используется для доступа к атрибутам объектов. Этот оператор вызывает выполнение процедуры поиска по словарю с использованием __getattribute__, что создаёт дополнительную нагрузку на систему. Как ограничить влияние этой особенности Python на производительность?

#  Медленно:
import re

def slow_func():
    for i in range(10000):
        re.findall(regex, line)  # Медленно!

#  Быстро:
from re import findall

def fast_func():
    for i in range(10000):
        findall(regex, line)  # Быстрее!

▍Остерегайтесь строк


Операции на строках могут сильно замедлить программу в том случае, если выполняются в циклах. В частности, речь идёт о форматировании строк с использованием %s и .format(). Можно ли их чем-то заменить? Если взглянуть на недавний твит Раймонда Хеттингера, то можно понять, что единственный механизм, который надо использовать в подобных ситуациях — это f-строки. Это — самый читабельный, лаконичный и самый быстрый метод форматирования строк. Вот, в соответствии с тем твитом, список методов, которые можно использовать для работы со строками — от самого быстрого к самому медленному:

f'{s} {t}'  # Быстро!
s + '  ' + t
' '.join((s, t))
'%s %s' % (s, t)
'{} {}'.format(s, t)
Template('$s $t').substitute(s=s, t=t)  # Медленно!

▍Знайте о том, что и генераторы могут работать быстро


Генераторы — это не те механизмы, которые, по своей природе, являются быстрыми. Дело в том, что они были созданы для выполнения «ленивых» вычислений, что экономит не время, а память. Однако экономия памяти может привести к тому, что программы будут выполняться быстрее. Как это возможно? Дело в том, что при обработке большого набора данных без использования генераторов (итераторов) данные могут привести к переполнению L1-кэша процессора, что значительно замедлит операции по поиску значений в памяти.

Если речь идёт о производительности, очень важно стремиться к тому, чтобы процессор мог бы быстро обращаться к обрабатываемым им данным, чтобы они находились бы как можно ближе к нему. А это значит, что такие данные должны помещаться в процессорном кэше. Этот вопрос затрагивается в данном выступлении Раймонда Хеттингера.

Итоги


Первое правило оптимизации заключается в том, что оптимизацией заниматься не нужно. Но если без оптимизации никак не обойтись, тогда я надеюсь, что советы, которыми я поделился, вам в этом помогут.

Уважаемые читатели! Как вы подходите к оптимизации производительности своего Python-кода?

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Похожие публикации

Комментарии 47

    +12

    И ни слова про PyPy, Cython, Numba, NumPy)

      +2
      а зачем?
        0
        Вдогонку (для числодробилок): github.com/pydata/numexpr
          0

          Вообще Cython не только для числодробилок профит даёт. Ещё его можно использовать как простую обёртку над C-шными функциями, например, для работы с файлами. Профит проверенный опытным путём достигал одного порядка.

            0
            А где можно почитать про обертку С. И насколько хорошо надо знать С чтобы эти обертки накладывать?
              0

              Здесь почитать. Здесь за вас уже что-то обернули. Знания нужны на уровне инклудов и указателей (хотя бы понимать что это и как работает). На гитхабе можно посмотреть что уже из стандартных библиотек обернуто и использовать.

          0

          А так же Pythran и Nuitka

          +31
          Всё-таки питон нужен для быстрой и удобной разработки легко поддерживаемого кода.
          Если вы настолько упёрлись в ограничения производительности, что вам приходится переписывать импорты и их использование в коде, лишь бы там не было обращения к атрибуту через точку — то вы изначально выбрали не тот язык для вашего проекта.
            0

            или выбрали слишком слабое железо

              +10
              Ну что вы, это же не java
              –5
              Всё-таки питон нужен для быстрой и удобной разработки легко поддерживаемого кода.

              А остальные высокоуровненые языки программирования разве нужны для чего-то другого?

                +2
                На чем я пишу быстрее и более поддерживаемый код на Pyton или C#, после 12 лет в .NET и трех лет в Python?
                На Python.
                Интересно это потому, что Python удобнее или потому, что у меня 15 лет опыта?
                  +1

                  Ну нет, чем больше программа на питоне, тем больше в ней хаоса от динамической типизации.
                  Я могу на питоне быстро написать маленькую программу, но что-то большое предпочту писать на статически типизированных языках.
                  Вдобавок, современные языки типа котлина/скалы, да даже C# позволяют писать красивый и лаконичный код.

                    +1

                    Вот мне интересно, а чем питон быстрее той же скалы для маленьких программ?


                    Единственное, что приходит в голову: питон предустановлен во многих линукс-дистрибутивах, а jvm и sbt — нет. Но с другой стороны, если я владею скалой, скорее всего, окружение у меня уже давно готово и настроено, и нет никаких причин не писать даже маленькую программу на скале.

                      +4
                      Там главное это библиотеки. В пару строк можно делать страшное.
                      +5
                      Да дело в том, что все интересные стартапы там где я живу на Питоне. На C# либо банки, либо галеры, есть пара продуктовых компаний, но там страшенный легаси.

                      Но после 3х лет Питона, я уже не хочу писать на статически типизированных языках.
                        0
                        А почему не используете статическую типизацию в питоне?
                          +5

                          Использую её вовсю, но есть проблемы:


                          1. В рантайме типы могут быть другими. Программа будет работать, но потом окажется, что вместо float у меня numpy.Scalar или что похуже.
                          2. У питоновских библиотек типа numpy и tensorflow аннтоаций типов нет — приходится либо писать типизированные обёртки, либо смириться.
                          3. Я регулярно сталкиваюсь с ситуациями, когда системой типов питона происходящее плохо описывается. Например, в np.zeros(shape=(3,4), dtype=np.float) возвращаемый тип зависит от аргументов. А функция загрузки png картинок может возвращать массивы с shape=(h,w), (h, w, 1), (h, w, 3) и (h, w, 4) и типы np.uint8 и np.float в зависимости от того, чем и с какими опциями картинки были сохранены. Дизайн некоторых библиотек сделан под динамическую типизацию и натянуть происходящее на статику может быть сложно.
                          4. Питон изначально делался под динамическую типизацию и в нём нет многих фишек статических языков: например, мне не хватает extension методов, перегрузки функций и нормальных интерфейсов. Перегрузку функций можно сделать через декораторы, но технически внутри будет много-много проверок типов, а не вызов сразу нужной функции. С интерфейсами тоже есть какие-то подвижки типа модуля abc, но опять же в других языках с этим лучше.
                            0
                            Например, в np.zeros(shape=(3,4), dtype=np.float) возвращаемый тип зависит от аргументов. А функция загрузки png картинок может возвращать массивы с shape=(h,w), (h, w, 1), (h, w, 3) и (h, w, 4) и типы np.uint8 и np.float в зависимости от того, чем и с какими опциями картинки были сохранены
                            — дженерики?
                          0

                          А что запрещает использовать статическую типизацию в питоне?

                            +9

                            А зачем тогда вообще питон, если можно сразу взять язык с хорошей статической типизацией, которая изначально продумана, а не прикручена сбоку непонятно как, да и где все библиотеки заведомо типизированы?

                              +3

                              Это аннотация типов, зачем вы подменяете понятия? В CPython нет никакой статической типизации!

                              +1

                              А чем питону мешает динамическая типизация? Никогда не возникало проблем с этим — всегда знаешь что должно быть в переменной по логике программы, если писать комментарии, логично рассуждать и документировать апи. Уже не первый раз слышу про динамическую типизацию в питоне в подобном ключе — что это его ложка дёгтя. Решительно не понимаю подобных проблем… В конце концов, если напутали с типами данных — то проект по хорошему должен быть покрыт тестами и тесты сразу же скажут где и что поправить, это же не беда вселенского масштаба?

                                –1

                                Плюсую. Никогда не понимал это возни и кудахтанья насчет типов. Сколько работаю с Python, я всегда знаю, какой тип у МОЕЙ переменной. И если я сам этот тип не изменю, его никто не изменит!

                                  +4

                                  Кааак сказать. Динамика хороша, иногда очень хороша, а иногда просто потрясающа. Утиная типизация иногда творит чудеса — просто за счёт реализации необходимых методов любая библиотека может начать работать с моим типом — разве не чудо? Не надо декларировать наследование определённого интерфейса, которые будут разными у разных библиотек — можно просто сделать что-то — и всё будет работать. Like a charm!


                                  Но. Оборотная сторона медали в потрясающем хаосе типизации. Ты никогда, ни в чём, абсолютно не можешь быть уверен. Вернётся тебе строка, bytearray или пользовательский string-like? Причём, что забавно, если в python2 это действительно практически взаимозаменяемые понятия, как только мы вспоминаем про мультибайтовые строки — всё переворачивается. python3 уже, внезапно, не так классно работает с этим всем многообразием, потому что строки — это UTF-строки, а bytearray — просто массив байт. Это хорошо в том смысле, что можно из коробки легко-непринуждённо нормально работать со строками. Но со старыми библиотеками будут проблемы. Равно как и несколько сломанный API. Подход, что строки — это буффер также сломан. Что в целом правильно. Но.


                                  Вспоминаем про утиную типизацию. И строки — и буфера реализуют абсолютно идентичный набор методов. Просто строки магически раскрываются на UTF-8 последовательности, а буффера это набор байт и всё. Хотя интерфейсы абсолютно идентичные.


                                  Соль в том, что для утиной типизации идентичный интерфейс равнозначно идентичной семантике. Но это не правда. Если что-то выглядит как утка, плавает как утка и крякает как утка — совсем не обязательно, что 1. это утка, а не селезень, например, 2. она вообще существует, а не плод вашего воображения, 3. никто особо хитрый не маскирует аллигатора под утку.


                                  Как обычно. У всего свои плюсы и минусы. Серебрянной пулей в каком-то смысле считают сильный вывод типов, но он в общем-то не решает проблемы явного указания типа, а без этого по факту не решить задачу разрешения семантики.


                                  Да, то что в пайтоне есть некое подобие статики в линтере проблемы не решает вовсе. В каком-то смысле становится только хуже в динамических местах — перегрузки функций, конечно, не завезли. В этом смысле довольно интересно сделано в том же typescript с их union typing. Плюс очень не плохо реализован вывод типов. Но иногда это лишь создаёт проблемы, так как не все библиотеки имеют одинаковые интерфейсы де юре, даже при одинаком оном де факто, даже при одинаковых их семантиках.


                                  Ну и да. Идеальный проект должен быть покрыт тестами. Идеальный программист не ошибается. Идеальный тестировщик находит все ошибки. Идеальный заказчик формирует дотошные требования. Идеальный пользователь идеально предсказуем. Но есть наш идеально не идеальный мир. Жить надо с ним, а не в мечтах. Ну это так. Лирика.

                                    0
                                    А чем питону мешает динамическая типизация?

                                    Для программиста динамическая типизация удобна. Она не удобна компьютеру :)
                                    Например, некоторая процедура принимает параметр А типа int, и в ней написано А+1. В случае статической типизации это будет одна (!) команда процессора. В случае динамической типизации начнутся пляски воокруг А на тему «а что там, вообще, лежит?» и «что означает этот + ?». Этот простейший +1 будет выполнятся в разы медленнее. Собственно, это базовая причина, по которой языки с динамической типизацией никогда не смогут достичь быстродействия языков со статической типизацией.
                                      +2

                                      Динамическая типизация не настолько уж и неудобна компьютеру, достаточно умные компиляторы™ её умеют оптимизировать (точнее, вычленять куски, в которых типы статически выводятся и не меняются, и JITить их).


                                      Но вот пайтоновская полностью утиная типизация — она даже программисту далеко не всегда удобна, аннотации типов не просто так вводят.

                                      0

                                      Она мешает не питону. Она просто мешает.


                                      Вот смотрите, вы готовы писать тесты (заменяющие тайпчекер), комментарии (которые не проверяются компилятором и устаревают), документацию (аналогично), помнить, что там в какой переменной, особенно написанной несколько месяцев назад… И всё это ради чего?

                                        +3

                                        Она просто мешает именно вам. И кому-то еще, кто на Python тоже не пишет. Я одинаково люблю С и Python, не испытывая необходимости агитировать против утиной типизации.

                                          +2

                                          Типизация в С, кстати, так себе, далеко не образец.

                                        0
                                        в этом то и проблема, что выйдет так что большинство тестов и будут напрямую или косвенно проверять тип выходной переменной.
                                      0

                                      Опыт — это конечно хорошо, но у каждого он свой. Можно ли узнать, какие конкретно фичи питона позволяют писать быстрее и более поддерживаемый код, чем в случае C#?

                                        0
                                        Да нет, я не хочу сказать, что код на Питоне «лучше». Я лишь говорю, что три года назад мой код на C# был хуже, чем сегодня на Питоне. И, как мне кажется, дело не в языке.
                                    +3

                                    Для легко поддерживаемого кода неплохо иметь типизацию из коробки, а не прикрученным сбоку как в mypy.

                                    +1
                                    def fast_function():
                                    r = random.random
                                    for i in range(10000):
                                    print(r()) # здесь вызов `r()` быстрее, чем был бы вызов random.random()
                                    Это и так понятно, что быстрее будет работать, поскольку в цикле мы обращаемся постоянно к объекту, это в большинстве языков программирования так не верно делать, априори, иногда такая точка в цикле может влиять на скорость выполнения решения на 90% (лично сталкивался с таким)
                                      +3

                                      Это в каких таких "большинстве"? В Java виртуальный метод в JIT разрезолвится, в C++/C# есть невиртуальные методы, там вообще резолвить нечего. Rust? Haskell? Objective C? Javascript?.. Даже он в JIT компилируется с адресной арифметикой с классическим прототипным наследованием.


                                      Python один из редких языков с лексическим пространством имён. Вообще, именно поэтому сами по себе скрипты катастрофически медленные. Но особой магии python не требует — знай себе, что кешируй всё подряд и не стоит дробить на нём числа — для этого есть numpy.

                                      +1
                                      main надо вызывать вот так:
                                      if __name__ == '__main__':
                                          main()
                                      
                                        0
                                        А так не лучше?
                                          +3
                                          А что с результатами-то? Вот в начале приводится медленная функция. Потом советы, как ускорить выполнение кода. А к медленной функции их применить и результат показать?
                                          Или советы не применимы к ней, как и к 90% остального python-кода?
                                            0
                                            автор:
                                            >Ненавистники Python всегда говорят, что Python — это медленно. Но то, что некая программа, независимо от используемого языка программирования, может считаться быстрой или медленной, очень сильно зависит от разработчика, который её написал, от его знаний и от умения создавать оптимизированный и высокопроизводительный код.

                                            он же:
                                            >Дело тут, в основном, в том, что встроенные механизмы языка реализованы средствами C. Если описывать нечто средствами Python — нельзя добиться того же уровня производительности.
                                              +1
                                              Не вижу тут противоречий, если вы на это намекаете. Медленно или быстро — субъективная оценка, каждый сам для себя решает что для него быстро или медленно. То что условно одинаковая конструкция на питоне медленнее такой же на C, не означает что программа работает медленно.
                                              0
                                              1. Преждевременные оптимизации — зло.
                                              Узнай место, которое замедляет программу, реализуй его на C(*).
                                              Собственно про профилирование кода сказали, а дальше свернули куда-то не туда и стали говорить про всякие микрооптимизации, зачем?
                                              *) по аналогии — мало кто пишет весь код сейчас на assembler, если узкое место CPU находят самую прожорливую ф-ию и переписывают её и только её (например используя параллельность по данным через SSE инструкции)

                                              2. Не упоминуть про CPython (и другие интерпретаторы байткода), почему?

                                              3. На фоне 1,2 то что описано в статье даст весьма незначительные улучшения.

                                              3.1 Вот это чрезмерная оптимизация, даже в сравнении с остальными пунктами статьи.
                                              >> Как это возможно? Дело в том, что при обработке большого набора данных без использования генераторов (итераторов) данные могут привести к переполнению L1-кэша процессора, что значительно замедлит операции по поиску значений в памяти.

                                              Даже на фоне присутствующих в статье советов это уж совсем экономия на спичках.
                                              Интерпретатор всё равно загрузится в L1-instraction-cache (который свой для кода\данных).
                                              А уж пара промахов по data-cache дадут доли процентов на фоне общей неспешности внутренних мехазинмов python, типа __getattribute__ (которые вовсе не для производительности так проектировались).
                                                0
                                                Для исследования временных показателей не указан прекрасный стандартный модуль — timeit
                                                  +1

                                                  timeit — игрушечка. Наткнулся сегодня на новую библиотеку для профайлинга, djn ссылка, библиотека scalene. Запустил тест функции, которую приводит автор в самом начале, вот результат:
                                                  python -m scalene main.py


                                                  main.py: % of CPU time = 100.00% out of  14.46s.
                                                           | CPU %    | CPU %    |
                                                    Line   | (Python) | (C)      | [main.py]
                                                  --------------------------------------------------------------------------------
                                                       1   |          |          | import sys
                                                       2   |          |          |
                                                       3   |          |          | import timeit
                                                       4   |          |          |
                                                       5   |          |          | import time
                                                       6   |          |          | from   functools import wraps
                                                       7   |          |          |
                                                       8   |          |          | from   decimal import *
                                                       9   |          |          |
                                                      21   |          |          | def timeit_wrapper(func):
                                                      22   |          |          |     @wraps(func)
                                                      23   |          |          |     def wrapper(*args, **kwargs):
                                                      24   |          |          |         start = time.process_time()
                                                      25   |          |          |         func_return_val = func(*args, **kwargs)
                                                      26   |          |          |         end = time.process_time()
                                                      27   |          |          |         print('{0:<10} {1:<8} {2:^8}'.format(
                                                      28   |          |          |             'module', 'function', 'time'))
                                                      29   |          |          |         print('{0:<10}.{1:<8} : {2:<8}'.format(
                                                      30   |          |          |             func.__module__,
                                                      31   |          |          |             func.__name__,
                                                      32   |          |          |             end - start))
                                                      33   |          |          |         return func_return_val
                                                      34   |  67.97%  |  32.03%  |     return wrapper
                                                      35   |          |          |
                                                      36   |          |          | @timeit_wrapper
                                                      37   |          |          | def exp(x):
                                                      38   |          |          |     getcontext().prec += 2
                                                      39   |          |          |     i, lasts, s, fact, num = 0, 0, 1, 1, 1
                                                      40   |          |          |     while s != lasts:
                                                      41   |          |          |         lasts = s
                                                      42   |          |          |         i += 1
                                                      43   |          |          |         fact *= i
                                                      44   |          |          |         num *= x
                                                      45   |          |          |         s += num / fact
                                                      46   |          |          |     getcontext().prec -= 2
                                                      47   |          |          |     return +s
                                                      48   |          |          |
                                                      49   |          |          | def test():
                                                      50   |          |          |     exp(Decimal(3000))
                                                      51   |          |          |
                                                      52   |          |          | if __name__ == '__main__':
                                                      53   |          |          |
                                                      54   |          |          |     if sys.argv[-1] == 'timeit':
                                                      55   |          |          |         print('timeit',
                                                      56   |          |          |               timeit.timeit("test()",
                                                      57   |          |          |               setup = "from __main__ import test",
                                                      58   |          |          |               number = 1))
                                                      59   |          |          |     else:
                                                      60   |          |          |         test()
                                                      61   |          |          |
                                                      62   |          |          | """
                                                      63   |          |          | Способы запуска:
                                                      64   |          |          | 1. time python3 main.py
                                                      65   |          |          | 2. python3 -m cProfile -s time main.py
                                                      66   |          |          | 3. python3 main.py timeit # но внутри
                                                      67   |          |          | 4. python3 -m scalene main.py
                                                      68   |          |          | ""
                                                  
                                                    0

                                                    Интересненько. А как с пакетами это работает? Он содержимое всех пакетов выведет или как? Интересно django-приложение как отпрофилирует.

                                                  +2
                                                  Статья интересная, но не хватает данных.
                                                  В первой части статьи описаны механизмы замеров времени, а дальше просто рекомендации. Насколько быстрее стало после изменения кода?
                                                  На сколько процентов улучшается ситуация с применением Ваших рекомендаций?
                                                  Один товарищ переписал программу с С++ на Ассемблер для решения проблем производительности при решении сложных расчетных задач. В результате решение задач ускорилось в 7-9 раз. И оптимизация происходила на уровне использования регистров процессора.
                                                    0
                                                    В результате решение задач ускорилось в 7-9 раз. И оптимизация происходила на уровне использования регистров процессора.

                                                    Уоу. Крутяк =)

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                  Самое читаемое