Почему Python такой медленный?
Популярность Python стремительно растет. Он используется в DevOps, Data Science, веб-разработке и информационной безопасности. Однако он не завоевал ни одной медали за скорость.
В этом материале попробуем ответить на вопрос: почему Python медленнее Java, C, C++ или C# и нельзя ли сделать его быстрее?
Примечание: далее в материале, говоря о Python, будем иметь ввиду именно CPython — это эталонная реализация Python, написанная на языке C.
Вот основные теории, объясняющие “медлительность языка”:
- Причина в GIL (Global Interpreter Lock).
- Причина в том, что это интерпретируемый, а не компилируемый язык.
- Причина в динамической типизации языка.
Так какая из них оказывает наибольшее влияние на производительность?
Причина в GIL
Современные компьютеры, как правило, оснащены процессорами с несколькими ядрами. Чтобы использовать всю эту дополнительную вычислительную мощность, операционная система определяет низкоуровневую структуру под названием поток. Один процесс (например, браузер Chrome) может порождать несколько потоков и иметь инструкции для системы. Таким образом, если какая-то задача слишком требовательна, нагрузка может быть распределена между ядрами, и это позволяет большинству приложений работать быстрее.
Имеем в виду, что структура и API потоков различаются в операционных системах на базе POSIX (например, Mac OS и Linux) и Windows OS. Операционная система также занимается планированием потоков.
Если вы ранее не занимались многопоточным программированием, то вам стоит ознакомиться с понятием блокировки. Она происходит, когда при изменении переменных несколько потоков одновременно пытаются получить доступ к одному и тому же адресу в памяти. Когда CPython создает переменные, он выделяет память, а затем подсчитывает, сколько ссылок на эту переменную существует. Это понятие известно как подсчет ссылок. Если количество ссылок равно 0, то CPython освобождает этот участок памяти из системы. Вот почему создание "временной" переменной, скажем, в рамках цикла for
, не приведет к увеличению потребления памяти. Проблемы возникают, когда переменные используются совместно в нескольких потоках. Срабатывает "глобальная блокировка интерпретатора" (GIL), которая тщательно контролирует исполнение/выполнение потоков. Интерпретатор может выполнять только одну операцию за раз, независимо от количества потоков.
Как это влияет на производительность?
Если у вас однопоточное приложение с одним интерпретатором, это не будет иметь никакого значения для скорости, отключение GIL никак не повлияет на производительность. Если бы вы хотели реализовать параллелизм в рамках одного интерпретатора (процесса Python) с помощью потоков, и ваши потоки были бы интенсивными в плане ввода-вывода (например, сетевой ввод-вывод или дисковый ввод-вывод), вы бы смогли проследить работу GIL в плане влияния на скорость операций.
Что насчет других реализаций Python?
- PyPy имеет GIL и обычно в 3 раза быстрее, чем CPython.
- Jython не имеет GIL, потому что поток Python в Jython представлен потоком Java и пользуется преимуществами системы управления памятью JVM.
- В JavaScript нет GIL потому что язык однопоточный. Асинхронное программирование в JavaScript достигается благодаря циклу событий и шаблону Promise/Callback вместо параллелизма.
Причина в том, что это интерпретируемый, а не компилируемый язык
Такое объяснение медлительности Python является грубым упрощением того, как на самом деле работает язык. Если бы вы написали в терминале python myscript.py
, то CPython начал бы длинную последовательность чтения, разбора, компиляции, интерпретации и выполнения этого кода.
Важным моментом в этом процессе является создание файла с расширением .pyc
. На этапе компиляции последовательность байткода записывается в файл внутри __pycache__/
. Это относится не только к вашему скрипту, но и ко всему импортированному вами коду, включая модули сторонних разработчиков. Таким образом, большую часть времени Python интерпретирует байткод и выполняет его локально.
Java компилируется в «промежуточный язык», а виртуальная машина Java считывает байткод и сразу же компилирует его в машинный код. NET Common-Language-Runtime работает по тому же принципу, что и Java.
Так почему же Python работает намного медленнее, чем Java и C#, если все они используют виртуальную машину и байткод?
Во-первых, .NET и Java являются JIT-компиляторами. JIT или Just-in-time compilation требует промежуточного языка, чтобы код можно было разделить на фрагменты (или фреймы). Компиляторы с опережением времени (AOT) предназначены для того, чтобы процессор мог понять каждую строку кода до того, как произойдет их исполнение. Сам по себе JIT не ускоряет работу кода, поскольку он по-прежнему выполняет те же последовательности байткода, однако он позволяет проводить оптимизацию во время выполнения. Хороший JIT-оптимизатор видит, какие части приложения выполняются часто, называя их «горячими точками». Затем он оптимизирует эти части кода, заменяя их более эффективными версиями.
Это означает, что когда ваше приложение выполняет одни и те же действия снова и снова, оно может работать значительно быстрее. Также следует помнить, что Java и C# являются сильно типизированными языками, поэтому оптимизатор может делать гораздо больше предположений о коде.
PyPy имеет JIT и, как упоминалось в предыдущем разделе, значительно быстрее, чем CPython.
Почему же CPython не использует JIT?
У JIT есть недостатки, и один из них — время запуска. У CPython оно сравнительно большое, PyPy запускается в 2-3 раза медленнее, чем CPython. Виртуальная машина Java, как известно, медленно загружается. CLR .NET обходит это, запускаясь при старте системы, но разработчики CLR написали операционную систему, на которой работает CLR.
Если у вас есть один процесс Python, выполняющийся в течение длительного времени, с кодом, который можно оптимизировать, поскольку он содержит «горячие точки», то тогда JIT, безусловно, нужен.
Однако Python — универсальный язык. Поэтому, если бы вы разрабатывали приложения для командной строки с использованием Python, ждать запуска JIT каждый раз, когда вызывается CLI, было бы ужасно долго. Если вы хотите получить преимущества JIT и у вас есть рабочая нагрузка, которая подходит для этого, используйте PyPy.
Причина в динамической типизации языка
В «статически типизированном» языке (например C, C++, Java, C#, Go) вы должны указать тип переменной при ее объявлении.
В динамически типизированном языке понятие типов сохраняется, но тип переменной является динамическим.
a = 1 a = "foo"
Здесь Python создает вторую переменную с тем же именем и типом str
и деаллоцирует память, созданную для первого экземпляра a
.
Языки со статической типизацией созданы не для того, чтобы усложнить вам жизнь, они созданы так из-за особенностей работы центрального процессора. Если все в конечном итоге должно быть приравнено к простой двоичной операции, вам придется преобразовывать объекты и типы в низкоуровневые структуры данных.
Python делает это за вас, вы просто никогда этого не увидите, и вам не нужно об этом заботиться. Отсутствие необходимости объявлять тип переменной не делает Python медленным, дизайн языка Python позволяет сделать почти все динамичным: вы можете заменить методы на объектах во время выполнения, вы можете подстроить низкоуровневые системные вызовы к значению, объявленному во время выполнения. Практически все возможно. Именно эта конструкция делает оптимизацию Python невероятно трудной.
Итак, делает ли динамическая типизация Python медленной?
Сравнение и преобразование типов требует больших затрат, ведь каждый раз, когда переменная читается, записывается или на нее ссылаются, проверяется ее тип. Трудно оптимизировать язык, который настолько динамичен. Причина, по которой многие альтернативы Python намного быстрее, заключается в том, что они идут на компромисс с гибкостью ради производительности. Есть CPython, который сочетает в себе C-Static Types и Python — он в 84 раза производительнее.
Заключение
Python такой медленный из-за своей динамической природы и многозадачности. Тем не менее, существуют способы оптимизации приложений на Python за счет понимания инструментов профилирования и использования нескольких интерпретаторов.
Для приложений, где время запуска неважно, а код только выиграет от JIT, рассмотрите PyPy.
Источник: Medium
Перевод и адаптация: Елена Гладких