Медлительность Python: причины проблем со скоростью
Медлительность Python иногда выводит из себя. Этот ЯП находится на пике популярности, поэтому давайте разбираться с причинами низкой скорости.
Почти все тесты производительности языков твердят, что Python самый медленный. Обычно в сравнении принимают участие Java, C#, Go, JavaScript, C++, а тестируется это все при помощи инструмента The Computer Language Benchmarks Game.
Мы не будем заниматься проведением своих тестов, а разберем основные известные причины, влияющие на медлительность Python.
Влияние GIL
Современные компьютеры поставляются с многоядерными и многопроцессорными системами. Чтобы использовать всю дополнительную вычислительную мощность, в операционной системе применяется многопоточность – без нее сейчас никуда. Таким образом, если один процесс является очень ресурсоемким, эта нагрузка может быть распределена между ядрами, что повышает эффективность работы приложений.
Важно помнить, что структура и API потоков отличаются между POSIX-системами (Mac OS, Linux) и Windows. Управлением и планированием потоков занимается не только операционная система, но и приложение (например, Google Chrome), порождающее копии себя.
Если ранее вы не сталкивались с многопоточным программированием, то стоит постараться познакомиться с этой темой поближе. В отличие от однопоточной разработки, в многопоточной приходится следить, чтобы несколько потоков не пытались одновременно получить доступ к одному и тому же адресу памяти.
Когда CPython создает переменные, он выделяет память и подсчитывает, сколько существует ссылок на эту переменную. Такой процесс называется подсчет ссылок. Если число ссылок равно 0, то этот фрагмент памяти освобождается. Вот почему создание "временной" переменной, не увеличивает потребление памяти вашим приложением.
Проблемы начинаются, когда переменные совместно используются в нескольких потоках, в то время как CPython заблокировал счетчик ссылок. Для этого и существует GIL, который тщательно контролирует выполнение потока. Интерпретатор может выполнять только одну операцию за раз, независимо от количества потоков.
Как это отражается на производительности?
Если у вас однопоточное приложение, то отсутствие или наличие GIL не повлияет на производительность кода.
Если вы хотите реализовать параллелизм в рамках одного процесса Python с помощью многопоточности и интенсивного ввода/вывода, то вы заметите результат от применения GIL.
Допустим, вы создали веб-приложение, в котором используется WSGI. Каждый запрос к приложению является отдельным интерпретатором Python, а значит есть только одна блокировка на запрос. Поскольку интерпретатор запускается медленно, некоторые реализации WSGI имеют "режим демона", который поддерживает процессы Python в рабочем состоянии.
Поведение GIL в аналогах CPython
- В PyPy есть GIL, и это дает ему тройной прирост в скорости по сравнению с CPython.
- В Jython нет GIL потому, что поток Python в Jython представлен потоком Java, использующим систему управления памятью JVM.
Как это работает в JavaScript?
- Все движки Javascript используют алгоритм mark-and-sweep для сборки мусора.
- В JavaScript нет GIL, т. к. он однопоточный.
- В JavaScript применяется асинхронное программирование, реализованное на event-loop и Promise / Callback вместо параллелизма, в то время как в Python используется asyncio.
Python – интерпретируемый язык
Данное высказывание – просто грубое упрощение того, как CPython действительно работает. Когда в терминале пишут python myscript.py, CPython начинает длинную последовательность из чтения, отладки, разбора, компиляции, интерпретации и выполнения этого кода.
Важным моментом в этом процессе является создание файла .pyc на этапе компиляции. Последовательность байт-кода записывается в файл внутри __pycache__ / в Python 3 или в том же каталоге в Python 2. Это относится не только к скрипту, но и ко всему импортированному коду, включая сторонние модули.
Если только вы не пишете код для однократного запуска, большую часть времени Python интерпретирует байт-код и выполняет его локально.
По бенчмаркам Python намного медленнее чем .NET и Java, потому как они являются JIT-компилируемыми.
Для JIT-компиляции требуется промежуточный язык, позволяющий разбивать код на блоки (или фреймы). Тут на помощь приходит AOT-компилятор, разработанный для того, чтобы гарантировать, что ЦП сможет понять каждую строку в коде до какого-либо действия.
Сам по себе JIT не делает выполнение быстрее, ведь он по-прежнему выполняет те же последовательности байт-кода. Однако, он позволяет выполнять оптимизацию в рантайме. Хороший JIT-оптимизатор видит, какие части приложения выполняются чаще, и помечает их тегом "hot spot", чтобы в следующий раз выполнение прошло быстрее.
Но у JIT есть один большой недостаток: CPython стартует медленно, PyPy – в 2-3 раза медленнее CPython, а JVM запускается дольше всех. Поэтому, если бы пришлось разрабатывать CLI-приложение на Python, пришлось бы очень долго ждать, пока JIT сделает свое дело.
Python – динамически типизированный язык
Статически типизированные языки, такие как C, C++, Java, C#, Go, требуют указывать тип переменной при ее объявлении.
В динамически типизированных языках понятие типов по-прежнему существует, но тип переменной является динамическим.
a = 1 a = "foo"
В этом примере создается вторая переменная с тем же именем, и типом str, а также освобождается память, выделенная для первого экземпляра переменной. Python всегда преобразовывает объекты и типы в низкоуровневую структуру данных незаметно.
Медлительность Python заключается не в отсутствие необходимости объявлять тип данных, а в структуре языка. Он позволяет сделать почти все динамичным: можно заменить методы объектами во время выполнения, подменить методы и значения атрибутов классов программы в рантайме, etc.
Такое положение вещей делает оптимизацию Python невероятно трудной.
Влияет ли динамическая типизация на медлительность Python?
- Сравнение и преобразование типов является ресурсоемким в том случае, когда переменная читается, записывается или ссылается на проверяемый тип.
- Трудно оптимизировать язык, который настолько динамичен. Причина того, что многие альтернативы Python гораздо быстрее в том, что они жертвуют гибкостью ради производительности.
- Глядя на Cython, сочетающий в себе C-Static типы и Python для оптимизации кода, замечаешь, что его производительность значительно выше, чем у CPython.
Заключение
Медлительность Python – в его динамической природе и универсальности. Он может использоваться как инструмент для всех видов задач, но есть более оптимизированные и быстрые альтернативы.
Однако существуют способы ускорения и оптимизации приложений Python путем использования асинхронности, применения профайлеров и нескольких интерпретаторов.
Для приложений, в которых время запуска не играет большой роли – рассмотрите вариант использования PyPy.
Для частей кода, где производительность критична, и в которых присутствуют статически типизированные переменные, – используйте Cython.