Содержание
Python — это медленно. Почему? / Хабр
В последнее время можно наблюдать рост популярности языка программирования Python. Он используется в DevOps, в анализе данных, в веб-разработке, в сфере безопасности и в других областях. Но вот скорость… Здесь этому языку похвастаться нечем. Автор материала, перевод которого мы сегодня публикуем, решил выяснить причины медлительности Python и найти средства его ускорения.
Общие положения
Как Java, в плане производительности, соотносится с C или C++? Как сравнить C# и Python? Ответы на эти вопросы серьёзно зависят от типа анализируемых исследователем приложений. Не существует идеального бенчмарка, но, изучая производительность программ, написанных на разных языках, неплохой отправной точкой может стать проект The Computer Language Benchmarks Game.
Я ссылаюсь на The Computer Language Benchmarks Game уже больше десяти лет. Python, в сравнении с другими языками, такими, как Java, C#, Go, JavaScript, C++, является одним из самых медленных. Сюда входят языки, в которых используется JIT-компиляция (C#, Java), и AOT-компиляция (C, C++), а также интерпретируемые языки, такие, как JavaScript.
Тут мне хотелось бы отметить, что говоря «Python», я имею в виду эталонную реализацию интерпретатора Python — CPython. В этом материале мы коснёмся и других его реализаций. Собственно говоря, здесь мне хочется найти ответ на вопрос о том, почему Python требуется в 2-10 раз больше времени, чем другим языкам, на решение сопоставимых задач, и о том, можно ли сделать его быстрее.
Вот основные теории, пытающиеся объяснить причины медленной работы Python:
- Причина этого — в GIL (Global Interpreter Lock, глобальная блокировка интерпретатора).
- Причина в том, что Python — это интерпретируемый, а не компилируемый язык.
- Причина — в динамической типизации.
Проанализируем эти идеи и попытаемся найти ответ на вопрос о том, что сильнее всего оказывает влияние на производительность Python-приложений.
GIL
Современные компьютеры обладают многоядерными процессорами, иногда встречаются и многопроцессорные системы. Для того чтобы использовать всю эту вычислительную мощь, операционная система применяет низкоуровневые структуры, называемые потоками, в то время как процессы (например — процесс браузера Chrome) могут запускать множество потоков и соответствующим образом их использовать. В результате, например, если какой-то процесс особенно сильно нуждается в ресурсах процессора, его выполнение может быть разделено между несколькими ядрами, что позволяет большинству приложений быстрее решать встающие перед ними задачи.
Например, у моего браузера Chrome, в тот момент, когда я это пишу, имеется 44 открытых потока. Тут стоит учитывать то, что структура и API системы работы с потоками различается в операционных системах, основанных на Posix (Mac OS, Linux), и в ОС семейства Windows. Операционная система, кроме того, занимается планированием работы потоков.
Если раньше вы не встречались с многопоточным программированием, то сейчас вам нужно познакомиться с так называемыми блокировками (locks). Смысл блокировок заключается в том, что они позволяют обеспечить такое поведение системы, когда, в многопоточной среде, например, при изменении некоей переменной в памяти, доступ к одной и той же области памяти (для чтения или изменения) не могут одновременно получить несколько потоков.
Когда интерпретатор CPython создаёт переменные, он выделяет память, а затем подсчитывает количество существующих ссылок на эти переменные. Эта концепция известна как подсчёт ссылок (reference counting). Если число ссылок равняется нулю, тогда соответствующий участок памяти освобождается. Именно поэтому, например, создание «временных» переменных, скажем, в пределах областей видимости циклов, не приводит к чрезмерному увеличению объёма памяти, потребляемого приложением.
Самое интересное начинается тогда, когда одними и теми же переменными совместно пользуются несколько потоков, а главная проблема тут заключается в том, как именно CPython выполняет подсчёт ссылок. Тут и проявляется действие «глобальной блокировки интерпретатора», которая тщательно контролирует выполнение потоков.
Интерпретатор может выполнять лишь одну операцию за раз, независимо от того, как много потоков имеется в программе.
▍Как GIL влияет на производительность Python-приложений?
Если у нас имеется однопоточное приложение, работающее в одном процессе интерпретатора Python, то GIL никак на производительность не влияет. Если, например, избавиться от GIL, никакой разницы в производительности мы не заметим.
Если же, в пределах одного процесса интерпретатора Python, надо реализовать параллельную обработку данных с применением механизмов многопоточности, и используемые потоки будут интенсивно использовать подсистему ввода-вывода (например, если они будут работать с сетью или с диском), тогда можно будет наблюдать последствия того, как GIL управляет потоками. Вот как это выглядит в случае использования двух потоков, интенсивно нагружающих процессов.
Визуализация работы GIL (взято отсюда)
Если у вас имеется веб-приложение (например, основанное на фреймворке Django), и вы используете WSGI, то каждый запрос к веб-приложению будет обслуживаться отдельным процессом интерпретатора Python, то есть, у нас имеется лишь 1 блокировка на запрос. Так как интерпретатор Python запускается медленно, в некоторых реализациях WSGI имеется так называемый «режим демона», при использовании которого процессы интерпретатора поддерживаются в рабочем состоянии, что позволяет системе быстрее обслуживать запросы.
▍Как ведут себя другие интерпретаторы Python?
В PyPy есть GIL, он обычно более чем в 3 раза быстрее, чем CPython.
В Jython нет GIL, так как потоки Python в Jython представлены в виде потоков Java. Такие потоки используют возможности по управлению памятью JVM.
▍Как управление потоками организовано в JavaScript?
Если говорить о JavaScript, то, в первую очередь, надо отметить, что все JS-движки используют алгоритм сборки мусора mark-and-sweep. Как уже было сказано, основная причина необходимости использования GIL — это алгоритм управления памятью, применяемый в CPython.
В JavaScript нет GIL, однако, JS — это однопоточный язык, поэтому в нём подобный механизм и не нужен. Вместо параллельного выполнения кода в JavaScript применяются методики асинхронного программирования, основанные на цикле событий, промисах и коллбэках. В Python есть нечто подобное, представленное модулем asyncio
.
Python — интерпретируемый язык
Мне часто приходилось слышать о том, что низкая производительность Python является следствием того, что это — интерпретируемый язык. Подобные утверждения основаны на грубом упрощении того, как, на самом деле, работает CPython. Если, в терминале, ввести команду вроде python myscript.py
, тогда CPython начнёт длительную последовательность действий, которая заключается в чтении, лексическом анализе, парсинге, компиляции, интерпретации и выполнении кода скрипта. Если вас интересуют подробности — взгляните на этот материал.
Для нас, при рассмотрении этого процесса, особенно важным является тот факт, что здесь, на стадии компиляции, создаётся .pyc
-файл, и последовательность байт-кодов пишется в файл в директории __pycache__/
, которая используется и в Python 3, и в Python 2.
Подобное применяется не только к написанным нами скриптам, но и к импортированному коду, включая сторонние модули.
В результате, большую часть времени (если только вы не пишете код, который запускается лишь один раз) Python занимается выполнением готового байт-кода. Если сравнить это с тем, что происходит в Java и в C#, окажется, что код на Java компилируется в «Intermediate Language», и виртуальная машина Java читает байт-код и выполняет его JIT-компиляцию в машинный код. «Промежуточный язык» .NET CIL (это то же самое, что .NET Common-Language-Runtime, CLR), использует JIT-компиляцию для перехода к машинному коду.
В результате, и в Java и в C# используется некий «промежуточный язык» и присутствуют похожие механизмы. Почему же тогда Python показывает в бенчмарках гораздо худшие результаты, чем Java и C#, если все эти языки используют виртуальные машины и какие-то разновидности байт-кода? В первую очередь — из-за того, что в .NET и в Java используется JIT-компиляция.
JIT-компиляция (Just In Time compilation, компиляция «на лету» или «точно в срок») требует наличия промежуточного языка для того, чтобы позволить осуществлять разбиение кода на фрагменты (кадры). Системы AOT-компиляции (Ahead Of Time compilation, компиляция перед исполнением) спроектированы так, чтобы обеспечить полную работоспособность кода до того, как начнётся взаимодействие этого кода с системой.
Само по себе использование JIT не ускоряет выполнение кода, так как на выполнение поступают, как и в Python, некие фрагменты байт-кода. Однако JIT позволяет выполнять оптимизации кода в процессе его выполнения. Хороший JIT-оптимизатор способен выявить наиболее нагруженные части приложения (такую часть приложения называют «hot spot») и оптимизировать соответствующие фрагменты кода, заменив их оптимизированными и более производительными вариантами, чем те, что использовались ранее.
Это означает, что когда некое приложение снова и снова выполняет некие действия, подобная оптимизация способна значительно ускорить выполнение таких действий. Кроме того, не забывайте о том, что Java и C# — это языки со строгой типизацией, поэтому оптимизатор может делать о коде больше предположений, способствующих улучшению производительности программ.
JIT-компилятор есть в PyPy, и, как уже было сказано, эта реализация интерпретатора Python гораздо быстрее, чем CPython. Сведения, касающиеся сравнения разных интерпретаторов Python, можно найти в этом материале.
▍Почему в CPython не используется JIT-компилятор?
У JIT-компиляторов есть и недостатки. Один из них — время запуска. CPython и так запускается сравнительно медленно, а PyPy в 2-3 раза медленнее, чем CPython. Длительное время запуска JVM — это тоже известный факт. CLR .NET обходит эту проблему, запускаясь в ходе загрузки системы, но тут надо отметить, что и CLR, и та операционная система, в которой запускается CLR, разрабатываются одной и той же компанией.
Если у вас имеется один процесс Python, который работает длительное время, при этом в таком процессе имеется код, который может быть оптимизирован, так как он содержит интенсивно используемые участки, тогда вам стоит серьёзно взглянуть на интерпретатор, имеющий JIT-компилятор.
Однако, CPython — это реализация интерпретатора Python общего назначения. Поэтому, если вы разрабатываете, с использованием Python, приложения командной строки, то необходимость длительного ожидания запуска JIT-компилятора при каждом запуске этого приложения сильно замедлит работу.
CPython пытается обеспечить поддержку как можно большего количества вариантов использования Python. Например, существует возможности подключения JIT-компилятора к Python, правда, проект, в рамках которого реализуется эта идея, развивается не особенно активно.
В результате можно сказать, что если вы, с помощью Python, пишете программу, производительность которой может улучшиться при использовании JIT-компилятора — используйте интерпретатор PyPy.
Python — динамически типизированный язык
В статически типизированных языках, при объявлении переменных, необходимо указывать их типы. Среди таких языков можно отметить C, C++, Java, C#, Go.
В динамически типизированных языках понятие типа данных имеет тот же смысл, но тип переменной является динамическим.
a = 1 a = "foo"
В этом простейшем примере Python сначала создаёт первую переменную a
, потом — вторую с тем же именем, имеющую тип str
, и освобождает память, которая была выделена первой переменной a
.
Может показаться, что писать на языках с динамической типизацией удобнее и проще, чем на языках со статической типизацией, однако, такие языки созданы не по чьей-то прихоти. При их разработке учтены особенности работы компьютерных систем. Всё, что написано в тексте программы, в итоге, сводится к инструкциям процессора. Это означает, что данные, используемые программой, например, в виде объектов или других типов данных, тоже преобразуются к низкоуровневым структурам.
Python выполняет подобные преобразования автоматически, программист этих процессов не видит, и заботиться о подобных преобразованиях ему не нужно.
Отсутствие необходимости указывать тип переменной при её объявлении — это не та особенность языка, которая делает Python медленным. Архитектура языка позволяет сделать динамическим практически всё, что угодно. Например, во время выполнения программы можно заменять методы объектов. Опять же, во время выполнения программы можно использовать технику «обезьяньих патчей» в применении к низкоуровневым системным вызовам. В Python возможно практически всё.
Именно архитектура Python чрезвычайно усложняет оптимизацию.
Для того чтобы проиллюстрировать эту идею, я собираюсь воспользоваться инструментом для трассировки системных вызовов в MacOS, который называется DTrace.
В готовом дистрибутиве CPython нет механизмов поддержки DTrace, поэтому CPython нужно будет перекомпилировать с соответствующими настройками. Тут используется версия 3. 6.6. Итак, воспользуемся следующей последовательностью действий:
wget https://github.com/python/cpython/archive/v3.6.6.zip unzip v3.6.6.zip cd v3.6.6 ./configure --with-dtrace make
Теперь, пользуясь python.exe
, можно применять DTRace для трассировки кода. Об использовании DTrace с Python можно почитать здесь. А вот тут можно найти скрипты для измерения с помощью DTrace различных показателей работы Python-программ. Среди них — параметры вызова функций, время выполнения программ, время использования процессора, сведения о системных вызовах и так далее. Вот как пользоваться командой dtrace
:
sudo dtrace -s toolkit/<tracer>.d -c ‘../cpython/python.exe script.py’
А вот как средство трассировки py_callflow
показывает вызовы функций в приложении.
Трассировка с использованием DTrace
Теперь ответим на вопрос о том, влияет ли динамическая типизация на производительность Python. Вот некоторые соображения по этому поводу:
- Проверка и конверсия типов — операции тяжёлые. Каждый раз, когда выполняется обращение к переменной, её чтение или запись, производится проверка типа.
- Язык, обладающей подобной гибкостью, сложно оптимизировать. Причина, по которой другие языки настолько быстрее Python, заключается в том, что они идут на те или иные компромиссы, выбирая между гибкостью и производительностью.
- Проект Cython объединяет Python и статическую типизацию, что, например, как показано в этом материале, приводит к 84-кратному росту производительности в сравнении с применением обычного Python. Обратите внимание на этот проект, если вам нужна скорость.
Итоги
Причиной невысокой производительности Python является его динамическая природа и универсальность. Его можно использовать как инструмент для решения разнообразнейших задач. Для достижения тех же целей можно попытаться поискать более производительные, лучше оптимизированные инструменты. Возможно, найти их удастся, возможно — нет.
Приложения, написанные на Python, можно оптимизировать, используя возможности по асинхронному выполнению кода, инструменты профилирования, и — правильно подбирая интерпретатор. Так, для оптимизации скорости работы приложений, время запуска которых неважно, а производительность которых может выиграть от использования JIT-компилятора, рассмотрите возможность использования PyPy. Если вам нужна максимальная производительность и вы готовы к ограничениям статической типизации — взгляните на Cython.
Уважаемые читатели! Как вы решаете проблемы невысокой производительности Python?
Python как компилируемый статически типизированный язык программирования / Хабр
alec_kalinin
Python *
По данным широко известного в узких кругах Tiobe Index язык Python скорее всего станет языком 2020 года, в четвертый раз в своей карьере. Кроме того, скорее всего он обгонит Java и займет вторую строчку в общем рейтинге языков программирования вслед за языком C.
Основным драйвером роста популярности языка Python стало его широкое использование в задачах машинного обучения. Но как же так? Python динамически типизируемый и интерпретируемый язык. Это же все очень медленно. Как его использовать в научных вычислениях, которые требуют максимальной производительности.
Обычно считается, что Python это только обертка над вычислительным ядром, написанным на C++. А что еще можно ожидать от такого медленного языка? Но ядро то ядром, но местами хочется обработать данные здесь и сейчас на таком удобном Python.
За всю историю Python было придумано большое количество решений, позволяющих ускорить Python код: a) Fortran, C, C++ модули; b) NumPy массивы; c) Cython расширения, и много другого. Это все работало, конечно, но все это было лишь внешним кодом по отношению к Python. Местами довольно неуклюжим.
А что собственно можно еще было сделать? Для быстрого кода нужны типы. А потом, все это как-то надо было комплировать в рамках интерпретируемого языка. Звучит как-то не очень реально. Но! Это все работает, прямо сейчас.
Две дороги: python аннотации типов и LLVM jit компиляция сошлись в релизе Numba 0.52.0. Смотрим на код, который говорит сам за себя.
from typing import List from numba.experimental import jitclass from numba.typed import List as NumbaList @jitclass class Counter: value: int def __init__(self): self.value = 0 def get(self) -> int: ret = self.value self.value += 1 return ret @jitclass class ListLoopIterator: counter: Counter items: List[float] def __init__(self, items: List[float]): self.items = items self.counter = Counter() def get(self) -> float: idx = self.counter.get() % len(self.items) return self.items[idx] items = NumbaList([3.14, 2.718, 0.123, -4.]) loop_itr = ListLoopIterator(items)
Каждый класс компилируется с помощью LLVM в нативный код платформы с использованием аннотаций типов.
Добро пожаловать в строго типизированный компилируемый Python!
Теги:
- python
- numba
Хабы:
- Python
Всего голосов 28: ↑22 и ↓6 +16
Просмотры
15K
Комментарии
38
Alexander Kalinin
@alec_kalinin
Пользователь
Комментарии
Комментарии 38
языков программирования — Почему Python не нужен компилятор?
У Python есть компилятор! Вы просто не замечаете этого, потому что он работает автоматически. Однако вы можете сказать, что он там есть: посмотрите на файлы .pyc
(или .pyo
, если у вас включен оптимизатор), которые генерируются для модулей, которые вы импортируете
.
Кроме того, он не компилируется в машинный код. Вместо этого он компилируется в байт-код, который используется виртуальной машиной. Виртуальная машина сама по себе является скомпилированной программой. Это очень похоже на то, как работает Java; настолько похоже, что существует вариант Python (Jython), который вместо этого компилируется в байт-код виртуальной машины Java! Также есть IronPython, который компилируется в CLR Microsoft (используется .NET). (Обычный компилятор байт-кода Python иногда называют CPython, чтобы отличить его от этих альтернатив.)
C++ необходимо раскрыть процесс компиляции, поскольку сам язык неполный; он не указывает все, что нужно знать компоновщику для сборки вашей программы, и не может указать параметры компиляции переносимым образом (некоторые компиляторы позволяют вам использовать #pragma
, но это не стандарт). Так что остальную часть работы вам придется делать с make-файлами и, возможно, с auto hell (autoconf/automake/libtool). На самом деле это просто пережиток того, как это сделал C. И C сделал это таким образом, потому что он сделал компилятор простым, что является одной из основных причин его популярности (любой мог создать простой компилятор C в 80-х).
Некоторые вещи, которые могут повлиять на работу компилятора или компоновщика, но не указаны в синтаксисе C или C++:
- разрешение зависимостей
- требования к внешней библиотеке (включая порядок зависимостей)
- уровень оптимизатора
- настройки предупреждения
- версия спецификации языка
- сопоставления компоновщика (какой раздел куда идет в окончательной программе)
- целевая архитектура
Некоторые из них можно обнаружить, но их нельзя указать; например Я могу определить, какой С++ используется с __cplusplus
, но я не могу указать, что C++98 используется для моего кода внутри самого кода; Я должен передать его как флаг компилятору в Makefile или сделать настройку в диалоговом окне.
Хотя вы можете подумать, что в компиляторе существует система «разрешения зависимостей», автоматически генерирующая записи о зависимостях, эти записи говорят только о том, какие файлы заголовков использует данный исходный файл. Они не могут указать, какие дополнительные модули исходного кода необходимы для связывания с исполняемой программой, потому что в C или C++ нет стандартного способа указать, что данный заголовочный файл является определением интерфейса для другого модуля исходного кода, а не просто набором линии, которые вы хотите показать в нескольких местах, чтобы не повторяться. Существуют традиции в соглашениях об именах файлов, но они не известны и не соблюдаются компилятором и компоновщиком.
Некоторые из них можно установить с помощью #pragma
, но это нестандартно, а я говорил о стандарте. Все эти вещи могли бы быть определены стандартом, но не в интересах обратной совместимости. Преобладает мнение, что make-файлы и IDE не сломаны, поэтому не чините их.
Python обрабатывает все это на языке. Например, import
указывает явную зависимость модуля, подразумевает дерево зависимостей, а модули не разделяются на заголовочные и исходные файлы (т. е. интерфейс и реализация).
языков программирования — Python интерпретируется или компилируется?
спросил
Изменено
3 года, 10 месяцев назад
Просмотрено
150 тысяч раз
Это просто вопрос, который возник у меня, когда я читал об интерпретируемых и компилируемых языках.
Ruby , без сомнения, является интерпретируемым языком, поскольку исходный код обрабатывается интерпретатором в момент выполнения.
Напротив, C является компилируемым языком, так как исходный код нужно сначала скомпилировать в соответствии с машиной, а затем выполнить. Это приводит к гораздо более быстрому выполнению.
Теперь перейдем к Python :
- Код Python ( somefile. py ) при импорте создает файл ( somefile.pyc ) в том же каталоге. Допустим, импорт выполняется в оболочке python или модуле django. После импорта я немного изменяю код и снова запускаю импортированные функции, чтобы обнаружить, что он все еще работает со старым кодом. Это говорит о том, что файлы *.pyc представляют собой скомпилированные файлы python, похожие на исполняемый файл, созданный после компиляции файла C, хотя я не могу выполнить файл *.pyc напрямую.
- Когда файл python (somefile.py) выполняется напрямую ( ./somefile.py или python somefile.py ), файл .pyc не создается, а код выполняется, что указывает на интерпретируемое поведение.
Это предполагает, что код Python компилируется каждый раз, когда он импортируется в новый процесс для создания .pyc, в то время как он интерпретируется при непосредственном выполнении.
Итак, к какому типу языка я должен относиться? Интерпретируется или компилируется?
И как его эффективность сравнивается с интерпретируемыми и компилируемыми языками?
Согласно странице интерпретируемых языков вики, он указан как язык, скомпилированный в код виртуальной машины, что под этим подразумевается?
- языки программирования
- python
- компилятор
- эффективность
- интерпретаторы
10
Стоит отметить, что языки не интерпретируются и не компилируются, а реализации языка либо интерпретируют, либо компилируют код. Вы отметили, что Ruby — это «интерпретируемый язык», но вы можете скомпилировать Ruby а-ля MacRuby, так что это не всегда интерпретируемый язык.
Почти каждая реализация Python состоит из интерпретатора (а не компилятора). Файлы .pyc
, которые вы видите, представляют собой байтовый код для виртуальной машины Python (аналогично файлам Java .class
). Они не совпадают с машинным кодом, сгенерированным компилятором C для собственной архитектуры машины. Некоторые реализации Python, однако, состоят из компилятора точно в срок , который компилирует байт-код Python в собственный машинный код.
(я говорю «практически каждый», потому что я не знаю ни одного собственного машинного компилятора для Python, но я не хочу утверждать, что его нигде нет.)
7
Python будет подпадать под интерпретацию байтового кода. Исходный код .py
сначала компилируется в байтовый код как . pyc
. Этот байт-код можно интерпретировать (официальный CPython) или скомпилировать JIT (PyPy). Исходный код Python ( .py
) может быть скомпилирован в другой байтовый код, такой как IronPython (.Net) или Jython (JVM). Существует несколько реализаций языка Python. Официальный — это интерпретируемый байт-код. Существуют также JIT-компилированные реализации байт-кода.
Для сравнения скорости различных реализаций языков вы можете попробовать здесь.
12
Компиляция и интерпретация могут быть полезны в некоторых контекстах, но в техническом смысле это ложная дихотомия.
Компилятор (в самом широком смысле) — это транслятор . Он переводит программу A в программу B и для последующего выполнения с помощью машины M.
Интерпретатор (в самом широком смысле) — это исполнитель . Это машина M, которая выполняет программу A. Хотя мы обычно исключаем из этого определения физические машины (или нефизические машины, которые действуют точно так же, как физические). Но с теоретической точки зрения это различие несколько условно.
Например, возьмем re.compile
. Он «компилирует» регулярное выражение в промежуточную форму, и эта промежуточная форма интерпретируется/оценивается/выполняется.
В конце концов, это зависит от того, о каком уровне абстракции вы говорите и что вас волнует. Люди говорят «компилируется» или «интерпретируется» для общего описания наиболее интересных частей процесса, но на самом деле почти каждая программа компилируется (переводится) и интерпретируется (выполняется) тем или иным способом.
CPython (самая популярная реализация языка Python) в основном интересен для выполнения кода. Таким образом, CPython обычно называют интерпретируемым. Хотя это свободная этикетка.
Код виртуальной машины представляет собой более компактную версию исходного кода (байт-код). Его все равно нужно интерпретировать виртуальной машиной, так как это не машинный код. Однако его проще и быстрее разобрать, чем исходный код, написанный человеком.