Все мы знаем, что Python намного медленнее, чем языки программирования со статической типизацией, такие как C, C ++, Java, а также некоторые динамические языки, такие как JavaScript и PHP. Давайте посмотрим на причины, почему Python намного медленнее по сравнению с этими языками, и что мы можем сделать, чтобы увеличить скорость его выполнения.

Почему Python медленный?

Реализация Python по умолчанию CPython использует GIL (Global Interpreter Lock) для одновременного выполнения ровно одного потока, даже если он выполняется на многоядерном процессоре как GIL. работает только на одном ядре независимо от количества ядер, присутствующих в машине. Каждое ядро ​​ЦП имеет свой собственный GIL, поэтому четырехъядерный ЦП будет иметь 4 GIL, работающих отдельно со своим собственным интерпретатором. Чтобы наши программы на Python работали параллельно, мы используем многопоточность и многопроцессорность.

Многопоточность не имеет большого значения во времени выполнения, поскольку использует то же пространство памяти и один GIL, поэтому любые задачи, связанные с процессором, не влияют на производительность многопоточных программ. поскольку блокировка распределяется между потоками в одном ядре, и только один поток выполняется, пока они ждут завершения обработки другими задачами. Кроме того, потоки используют одну и ту же память, поэтому необходимо принять меры предосторожности, иначе два потока будут записывать в одну и ту же память одновременно. Это причина, по которой требуется глобальная блокировка интерпретатора.

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

Является ли GIL агентом, вызывающим проблему? Почему бы нам не удалить его?

Поскольку GIL позволяет одновременно выполнять только один поток даже в многопоточной архитектуре с более чем одним ядром ЦП, GIL приобрел репутацию «печально известной» особенности Python. Таким образом, это ограничивает скорость выполнения программ Python и не использует предоставленные ресурсы в полной мере.

Так почему бы нам не удалить GIL? CPython использует подсчет ссылок для управления памятью. Это означает, что объекты, созданные в CPython, имеют переменную счетчика ссылок, которая отслеживает количество ссылок, указывающих на объект. Когда этот счетчик достигает нуля, память, занятая объектом, освобождается.

Если мы удалим GIL из CPython, то переменная счетчика ссылок больше не будет защищена, поскольку два потока могут одновременно увеличивать или уменьшать ее значение. И если это произойдет, это может привести либо к утечке памяти, которая никогда не освобождается, либо, что еще хуже, к неправильному освобождению памяти, пока ссылка на этот объект все еще существует. Это может вызвать сбои или другие «странные» ошибки в наших программах Python.

Кроме того, было несколько попыток удалить GIL из CPython, но дополнительные накладные расходы для однопоточных машин, как правило, были слишком большими. Некоторые случаи могут быть медленнее даже на многопроцессорных машинах из-за конкуренции блокировок.

Существуют альтернативные подходы к GIL, такие как Jython и IronPython, которые используют поточный подход их базовой виртуальной машины, а не подход GIL.

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

Преимущества реализации GIL в Python:

  • Повышена скорость работы однопоточных программ.
  • Простая интеграция библиотек C, которые обычно не являются потокобезопасными.
  • Простая реализация, поскольку наличие одного GIL намного проще реализовать, чем интерпретатор без блокировок или интерпретатор с мелкими блокировками.

Python медленный из-за своей динамической природы?

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

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

Дизайн языка Python позволяет нам делать практически все что угодно динамичным. Мы можем заменить методы на объектах во время выполнения, мы можем обезопасить низкоуровневые системные вызовы для значения, объявленного во время выполнения. Возможно практически все. Так что отсутствие объявления типа - это не то, что замедляет Python, это дизайн, который делает невероятно трудным оптимизацию Python.

«CPython интерпретируется во время выполнения». Это проблема медленного выполнения программ Python?

Как только мы запускаем нашу программу Python, файл .py исходного кода сначала компилируется с использованием CPython (написанного на языке программирования C) в промежуточный файл байт-кода .pyc, сохраненный в папке __pycache__ (Python 3), а затем интерпретируется виртуальной машиной Python для Машинный код.

Поскольку CPython использует интерпретатор, который выполняет сгенерированный байт-код непосредственно во время выполнения, это значительно замедляет выполнение, поскольку каждая строка интерпретируется во время выполнения программы. В то время как другие языки программирования, такие как C, C ++, непосредственно компилируются в машинный код перед выполнением с использованием компиляции с опережением времени (AOT). Кроме того, Java компилируется в «промежуточный язык», а виртуальная машина Java считывает байт-код и точно в срок (JIT) компилирует его в машинный код. Общий промежуточный язык .NET (CIL) тот же, .NET Common-Language-Runtime (CLR), использует JIT-компиляцию для Машинный код.

Мы понимаем, что компиляция AOT выполняется быстрее, чем интерпретация, поскольку программа уже была скомпилирована в машиночитаемый код до того, как произойдет какое-либо выполнение. Но как JIT-компиляция позволяет запускать программы быстрее, чем программы, реализованные в CPython?

JIT-компиляция представляет собой комбинацию двух традиционных подходов к преобразованию в машинный код - предварительная компиляция (AOT) и интерпретация - и сочетает в себе некоторые преимущества и недостатки обоих. Таким образом, JIT-компиляция оптимизирует нашу программу, компилируя определенные части программы, которые часто используются, а затем выполняются с остальной частью кода во время выполнения программы.

Некоторые реализации Python, такие как PyPy, используют JIT-компиляцию, которая более чем в 4 раза быстрее, чем CPython. Итак, почему CPython не использует JIT?

У JIT есть и недостатки, одним из которых является задержка запуска. Реализации, использующие JIT, имеют значительно более медленное время загрузки по сравнению с CPython. CPython - это реализация общего назначения для разработки программ и проектов из командной строки (CLI), которые не требуют значительных усилий со стороны ЦП. Была возможность использовать JIT в CPython, но она в значительной степени застопорилась из-за его сложной реализации и отсутствия гибкости в Python.

«Если вы хотите, чтобы ваш код работал быстрее, вам, вероятно, следует просто использовать PyPy». - Гвидо ван Россум (создатель Python)

Какая альтернатива CPython?

PyPy считается самой быстрой реализацией Python с поддержкой популярных библиотек Python, таких как Django, и хорошо совместим с существующим кодом Python. PyPy имеет GIL и использует JIT-компиляцию, поэтому он сочетает в себе преимущества того, что выполнение в целом выполняется намного быстрее, чем CPython . Некоторые исследования показали, что это примерно в 7,5 раз быстрее. чем CPython.

Как работает PyPy?

PyPy сначала берет наш исходный код Python и преобразует его в RPython, который является статически типизированным ограниченным подмножеством Python. RPython легче скомпилировать в более эффективный код, поскольку это язык со статической типизацией. Затем PyPy переводит сгенерированный код RPython в форму байт-кода вместе с интерпретатором, написанным на языке программирования «C». Большая часть этого кода затем компилируется в машинный код, а байт-код выполняется в скомпилированном интерпретаторе.

Вот визуальное представление этой реализации:

Он также позволяет использовать подключаемые сборщики мусора, а также дополнительно включать функции Stackless Python. Наконец, он включает JIT-генератор, который встраивает JIT-компилятор в интерпретатор с учетом нескольких аннотаций в исходном коде интерпретатора. Сгенерированный JIT-компилятор - это трассирующий JIT.

Это было краткое объяснение того, как работает реализация, если вам интересно узнать больше о PyPy, вы можете прочитать больше здесь.

Почему бы нам не использовать PyPy в качестве стандартной реализации в Python?

Поскольку мы обсуждали обратную сторону JIT, являющуюся задержкой по времени запуска, PyPy следует за набором. Кроме того, PyPy несовместим со многими расширениями C, поскольку CPython написан на языке программирования «C», и сторонние расширения PyPI используют это преимущество. Numpy был бы хорошим примером, большая часть Numpy написана на оптимизированном коде C. Когда мы pip install numpy, он использует наш локальный компилятор C и создает двоичную библиотеку для нашей среды выполнения Python.

PyPy написан на Python, поэтому нам нужно убедиться, что модули, необходимые для нашего проекта, поддерживаются PyPy, прежде чем реализовывать его в нашем проекте.

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

Заключение

Результаты, которые я представил, показывают, что Python действительно является медленным языком из-за его динамической природы по сравнению с другими статически типизированными языками, такими как C, C ++, Java. Но стоит ли нам это сильно волновать?

Вероятно, нет, поскольку все мы знаем, сколько времени на разработку можно сэкономить, используя Python в наших проектах. Стартапы уже активно используют Python в своих проектах, чтобы как можно скорее вывести свой продукт на рынок. Это позволяет им сэкономить на трудозатратах и человеко-часах, затрачиваемых на один продукт. Такие фреймворки, как Django, сделали возможной разработку полного стека с уже предоставленным им множеством важных функций.

Разработчики Python сейчас используют оптимальную реализацию Python, если производительность является для них ограничением при работе над машинным обучением, большими данными, искусственным интеллектом в целом. Возможности безграничны, когда дело доходит до использования современного и динамичного языка с обширной поддержкой более 100 000 библиотек, доступных сегодня в Python Package Index (PyPI). Это упрощает и ускоряет работу разработчиков одновременно.

Дальнейшее чтение

Если вы хотите узнать больше о Python GIL, реализациях Python, байт-коде Python и о том, как они работают, я рекомендую эти ресурсы:

  • Вы можете узнать больше о реализациях Python на вики-странице Python для различных доступных реализаций Python.
  • Если вы хотите знать, как именно работает Python Bytecode, то это лучший ресурс, который я нашел до сих пор.
  • Также ознакомьтесь с докладом Дэвида Бизли о видеоверсии 2012 года Понимание Python GIL.
  • Вы также можете ознакомиться с предыдущей PDF-версией выступления Дэвида Бизли за 2009 год на тему Inside the GIL.
  • Если вам интересно узнать больше о PyPy, вы можете начать с этой документации PyPy.