Why Python is Slow: Looking Under the Hood
When I teach courses on Python for scientific computing, I make this point very early in the course, and tell the students why: it boils down to Python being a dynamically typed, interpreted language, where values are stored not in dense buffers but in scattered objects. And then I talk about how to get around this by using NumPy, SciPy, and related tools for vectorization of operations and calling into compiled code, and go on from there.
But I realized something recently: despite the relative accuracy of the above statements, the words «dynamically-typed-interpreted-buffers-vectorization-compiled» probably mean very little to somebody attending an intro programming seminar. The jargon does little to enlighten people about what’s actually going on «under the hood», so to speak.
So I decided I would write this post, and dive into the details that I usually gloss over. Along the way, we’ll take a look at using Python’s standard library to introspect the goings-on of CPython itself. So whether you’re a novice or experienced programmer, I hope you’ll learn something from the following exploration.
Why Python is Slow¶
Python is slower than Fortran and C for a variety of reasons:
1. Python is Dynamically Typed rather than Statically Typed.¶
What this means is that at the time the program executes, the interpreter doesn’t know the type of the variables that are defined. The difference between a C variable (I’m using C as a stand-in for compiled languages) and a Python variable is summarized by this diagram:

For a variable in C, the compiler knows the type by its very definition. For a variable in Python, all you know at the time the program executes is that it’s some sort of Python object.
So if you write the following in C:
the C compiler knows from the start that a and b are integers: they simply can’t be anything else! With this knowledge, it can call the routine which adds two integers, returning another integer which is just a simple value in memory. As a rough schematic, the sequence of events looks like this:
C Addition¶
- Assign <int> 1 to a
- Assign <int> 2 to b
- call binary_add<int, int>(a, b)
- Assign the result to c
The equivalent code in Python looks like this:
here the interpreter knows only that 1 and 2 are objects, but not what type of object they are. So the interpreter must inspect PyObject_HEAD for each variable to find the type information, and then call the appropriate summation routine for the two types. Finally it must create and initialize a new Python object to hold the return value. The sequence of events looks roughly like this:
Python Addition¶
- 1a. Set a->PyObject_HEAD->typecode to integer
- 1b. Set a->val = 1
- 2a. Set b->PyObject_HEAD->typecode to integer
- 2b. Set b->val = 2
call binary_add(a, b)
- 3a. find typecode in a->PyObject_HEAD
- 3b. a is an integer; value is a->val
- 3c. find typecode in b->PyObject_HEAD
- 3d. b is an integer; value is b->val
- 3e. call binary_add<int, int>(a->val, b->val)
- 3f. result of this is result , and is an integer.
Create a Python object c
- 4a. set c->PyObject_HEAD->typecode to integer
- 4b. set c->val to result
The dynamic typing means that there are a lot more steps involved with any operation. This is a primary reason that Python is slow compared to C for operations on numerical data.
2. Python is interpreted rather than compiled.¶
We saw above one difference between interpreted and compiled code. A smart compiler can look ahead and optimize for repeated or unneeded operations, which can result in speed-ups. Compiler optimization is its own beast, and I’m personally not qualified to say much about it, so I’ll stop there. For some examples of this in action, you can take a look at my previous post on Numba and Cython.
3. Python’s object model can lead to inefficient memory access¶
We saw above the extra type info layer when moving from a C integer to a Python integer. Now imagine you have many such integers and want to do some sort of batch operation on them. In Python you might use the standard List object, while in C you would likely use some sort of buffer-based array.
A NumPy array in its simplest form is a Python object build around a C array. That is, it has a pointer to a contiguous data buffer of values. A Python list, on the other hand, has a pointer to a contiguous buffer of pointers, each of which points to a Python object which in turn has references to its data (in this case, integers). This is a schematic of what the two might look like:

It’s easy to see that if you’re doing some operation which steps through data in sequence, the numpy layout will be much more efficient than the Python layout, both in the cost of storage and the cost of access.
So Why Use Python?¶
Given this inherent inefficiency, why would we even think about using Python? Well, it comes down to this: Dynamic typing makes Python easier to use than C. It’s extremely flexible and forgiving, this flexibility leads to efficient use of development time, and on those occasions that you really need the optimization of C or Fortran, Python offers easy hooks into compiled libraries. It’s why Python use within many scientific communities has been continually growing. With all that put together, Python ends up being an extremely efficient language for the overall task of doing science with code.
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 — это интерпретируемый, а не компилируемый язык.
- Причина — в динамической типизации.
Современные компьютеры обладают многоядерными процессорами, иногда встречаются и многопроцессорные системы. Для того чтобы использовать всю эту вычислительную мощь, операционная система применяет низкоуровневые структуры, называемые потоками, в то время как процессы (например — процесс браузера 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.
В динамически типизированных языках понятие типа данных имеет тот же смысл, но тип переменной является динамическим.
В этом простейшем примере Python сначала создаёт первую переменную a , потом — вторую с тем же именем, имеющую тип str , и освобождает память, которая была выделена первой переменной a .
Может показаться, что писать на языках с динамической типизацией удобнее и проще, чем на языках со статической типизацией, однако, такие языки созданы не по чьей-то прихоти. При их разработке учтены особенности работы компьютерных систем. Всё, что написано в тексте программы, в итоге, сводится к инструкциям процессора. Это означает, что данные, используемые программой, например, в виде объектов или других типов данных, тоже преобразуются к низкоуровневым структурам.
Python выполняет подобные преобразования автоматически, программист этих процессов не видит, и заботиться о подобных преобразованиях ему не нужно.
Отсутствие необходимости указывать тип переменной при её объявлении — это не та особенность языка, которая делает Python медленным. Архитектура языка позволяет сделать динамическим практически всё, что угодно. Например, во время выполнения программы можно заменять методы объектов. Опять же, во время выполнения программы можно использовать технику «обезьяньих патчей» в применении к низкоуровневым системным вызовам. В Python возможно практически всё.
Именно архитектура Python чрезвычайно усложняет оптимизацию.
Для того чтобы проиллюстрировать эту идею, я собираюсь воспользоваться инструментом для трассировки системных вызовов в MacOS, который называется DTrace.
В готовом дистрибутиве CPython нет механизмов поддержки DTrace, поэтому CPython нужно будет перекомпилировать с соответствующими настройками. Тут используется версия 3.6.6. Итак, воспользуемся следующей последовательностью действий:
Теперь, пользуясь python.exe , можно применять DTRace для трассировки кода. Об использовании DTrace с Python можно почитать здесь. А вот тут можно найти скрипты для измерения с помощью DTrace различных показателей работы Python-программ. Среди них — параметры вызова функций, время выполнения программ, время использования процессора, сведения о системных вызовах и так далее. Вот как пользоваться командой dtrace :
А вот как средство трассировки py_callflow показывает вызовы функций в приложении.
Трассировка с использованием DTrace
Теперь ответим на вопрос о том, влияет ли динамическая типизация на производительность Python. Вот некоторые соображения по этому поводу:
- Проверка и конверсия типов — операции тяжёлые. Каждый раз, когда выполняется обращение к переменной, её чтение или запись, производится проверка типа.
- Язык, обладающей подобной гибкостью, сложно оптимизировать. Причина, по которой другие языки настолько быстрее Python, заключается в том, что они идут на те или иные компромиссы, выбирая между гибкостью и производительностью.
- Проект Cython объединяет Python и статическую типизацию, что, например, как показано в этом материале, приводит к 84-кратному росту производительности в сравнении с применением обычного Python. Обратите внимание на этот проект, если вам нужна скорость.
Итоги
Причиной невысокой производительности Python является его динамическая природа и универсальность. Его можно использовать как инструмент для решения разнообразнейших задач. Для достижения тех же целей можно попытаться поискать более производительные, лучше оптимизированные инструменты. Возможно, найти их удастся, возможно — нет.
Приложения, написанные на Python, можно оптимизировать, используя возможности по асинхронному выполнению кода, инструменты профилирования, и — правильно подбирая интерпретатор. Так, для оптимизации скорости работы приложений, время запуска которых неважно, а производительность которых может выиграть от использования JIT-компилятора, рассмотрите возможность использования PyPy. Если вам нужна максимальная производительность и вы готовы к ограничениям статической типизации — взгляните на Cython.
Уважаемые читатели! Как вы решаете проблемы невысокой производительности Python?
Да, питон медленный, но мне всё равно
Болтовня о жертве производительности в угоду продуктивности
Беру перерыв в обсуждении asyncio в Python, чтобы поговорить о том, что в последнее время занимало мои мысли: скорость питона. Для тех, кто не знает, я типа питон-фанбоя и агрессивно использую его везде, где это возможно. Одна из самых частых жалоб относительно этого языка — его медлительность. Некоторые отказываются даже просто попробовать его, потому что он медленнее чем X. Здесь изложены мои мысли, почему тебе стоит попробовать питон, не смотря на то, что он медленный.
Скорость больше не имеет значения
Раньше программы действительно занимали очень много времени для выполнения. Процессоры были дорогими, память была дорогой. Время работы программы было важным показателем. Компьютеры стоили очень дорого, как и электричество, питающее их. Оптимизация этих ресурсов была обусловлена вечным законом бизнеса:
Оптимизируй свои самые важные ресурсы.
Исторически сложилось, что самым дорогим ресурсом было время работы компьютера. Это и привело к развитию компьютерных наук, сфокусированных на различных эффективных алгоритмах. Тем не менее, самый дорогой ресурс сейчас — это не время работы компьютера. Потому что последние сейчас стоят дёшево. То есть, очень дешёво. В наше время, самый дорогой ресурс компании — время сотрудника. Или, другими словами, твоё время. Гораздо важнее выполнить задачу, чем заставить её выполняться быстро. На самом деле, это настолько важно, что я напишу это ещё раз как цитату (для тех, кто просто листает страницу):
Гораздо важнее выполнить задачу, чем заставить её выполняться быстро.
Ты можешь сказать: “В моей компании важна скорость, я делаю веб-приложения и все запросы должны проходить быстрее, чем х миллисекунд”. Или: “Клиентам казалось, что наше приложение медленное и мы теряли их”. Я не пытаюсь сказать, что скорость вообще не имеет значения. Я просто хочу показать, что это не самая важная вещь; это не самый дорогой ресурс.
Скорость — это всё, что имеет значение
Когда ты упоминаешь скорость в контексте разработки, ты, как правило, подразумеваешь производительность, или такт процессора. Когда твой CEO упоминает скорость в том же контексте — он имеет в виду бизнес-скорость. Самый важный показатель — это время выхода на рынок. В конечном счёте, не имеет значения как быстро работает твой продукт/веб-приложение. Не важно на каком языке оно написано. Это даже не вопрос стоимости его запуска. В конце концов, единственное, что позволит твоей компании выжить или умереть — это время выхода на рынок. Я говорю не столько о промежутке времени “идея стартапа — получение дохода”, сколько о “идея — руки пользователей”. Единственный способ выжить в бизнесе — реализовывать инновации быстрее конкурентов. Количество хороших идей не будет иметь значения, если конкуренты запустятся раньше. Вы должны быть первыми на рынке, или хотя бы не отставать. Как только вы начнёте тормозить — всё, вам конец.
Единственный способ выжить в бизнесе — реализовывать инновации быстрее конкурентов.
Пример с микросервисами
Такие компании, как Amazon, Google и Netflix понимают важность быстрого продвижения. Они создали бизнес-систему, в которой могут двигаться быстро и оперативно применять инновации. Решением их проблем являются микросервисы. Эта статья не имеет никакого отношения к тому, стоит ли тебе использовать микросервисы в своей компании или нет. Но хотя бы прими к сведению, что Amazon и Google считают их использование обоснованным.
Микросервисы, по своей сути, довольно медлительны. Само их понятие подразумевает разбиение границ сетевыми запросами. Это значит, что ты берёшь вызов функции (несколько циклов ЦПУ) и превращаешь это в сетевой запрос. Нельзя придумать что-либо ещё хуже, в плане производительности. Сетевые запросы гораздо медленнее, по сравнению с процессорными. Но эти крупные компании всё равно предпочитают использовать микросервисы. Я не шучу, не существует архитектуры более тормозной, чем микросервисы. Их самый большой минус в производительности, но огромный плюс во времени выхода на рынок. Строя команды вокруг маленьких проектов и мелких кодовых баз, у компании есть возможность просеивать и внедрять инновации в более высоком темпе. Это просто говорит о том, что очень большие компании заботятся о времени выхода на рынок ничуть не меньше стартапов.
Проблема не в ЦПУ
Если ты пишешь сетевое приложение, например веб-сервер, то узким местом является, скорее всего, не процессор. Когда твой сервер обрабатывает запрос, он наверняка сам делает пару сетевых запросов, например к базе данных или кеширующему серверу типа Redis. И хотя сами по себе они могут работать быстро, запросы к ним выполняются долго. Вот тут есть очень хорошая статья по скорости различных операций. Циклы ЦПУ в ней приравниваются к более привычным для человека единицам измерения. Если один ЦПУ-цикл был бы равен одной секунде, то сетевой запрос от Калифорнии до Нью-Йорка занял бы четыре года. Примерно во столько раз сеть медленнее. Давайте сделаем пару грубых вычислений. Предположим, что нормальный запрос, в пределах одного дата-центра, занимает 3 миллисекунды. Это равнялось бы трём месяцам в человеческом отношении. Теперь представь, что твоя программа очень ресурсоёмка в плане процессора, она занимает 100 000 циклов, чтобы отдать ответ на один запрос. Это было бы чуть больше одного дня. Скажем, ты используешь язык, который в пять раз медленнее, то есть теперь на запрос уходит 5 дней. Сравни это с тремя месяцами сетевого запроса и разница в четыре дня перестаёт иметь какое-либо значение. Если кто-то вынужден ждать свою посылку 3 месяца, я не думаю, что 4 дополнительных дня будут играть для них большую роль.
В конечном счёте, это означает, что даже если питон медленный — это не имеет значения. Скорость языка (или время процессора) почти никогда не является проблемой. В Google даже провели исследование этой теории, написав целую статью об этом. В ней рассматривается разработка высокопропускной системы. В заключение, они пишут:
Использование интерпретируемого языка может выглядеть парадоксально в высокопропускной среде, но мы обнаружили, что процессорное время очень редко является ограничивающим фактором; выразительность языка означает, что большинство программ не занимают много места и проводят большую часть времени в режиме ввода/вывода и собственном времени выполнения кода. Более того, гибкость интерпретируемого решения оказалась полезной как в отношении простоты экспериментов на лингвистическом уровне, так и позволив нам исследовать способы распределения расчётов среди большого числа машин.
процессорное время очень редко является ограничивающим фактором
Что если проблема в процессорном времени?
Ты можешь сказать: “Это всё здорово, но у нас были проблемы, вызванные как раз из-за ЦПУ, которые привели к тормозам в нашем веб-приложении”, или “Язык x менее требователен к серверным ресурсам, чем y”. И это может быть так. Замечательная штука относительно веб-серверов в том, что ты можешь практически бесконечно распределять нагрузку между ними. Другими словами, подкинуть им больше железа. Конечно, питон может требовать железо получше, чем другие языки, например C. Просто выдели больше ресурсов под процессор. Это гораздо дешевле, по сравнению с твоим временем. Если ты сэкономишь пару недель продуктивности в год, это с головой окупит стоимость оборудования.
Ну и, питон быстрее?
Всё это время я говорил о том, что самая важная вещь — это время разработки. Так что вопрос остаётся: кто быстрее — питон или Х, когда речь заходит о времени разработки? Забавно, но я, гугл, и некоторые другие, могут рассказать о том, насколько питон продуктивнее. Он абстрагирует за тебя огромное количество вещей, помогая сконцентрироваться на том, что ты хочешь написать, не погрязнув в мелких сорняках типа вопросов “что лучше использовать вектор или массив?”. Но, возможно, тебе не нравится верить другим на слово, поэтому давай взглянем на некоторые эмпирические данные.
По большей части, дискуссия о продуктивности питона сводится к спору между скриптовыми (или динамическими) и статически типизированными языками. Мне кажется, всеми уже принято, что статические языки менее продуктивны, здесь хорошо объясняется почему. Говоря конкретно о питоне, тут есть хорошее обобщение исследования, в котором анализировалось время написания кода, обрабатывающего текст, в зависимости от используемого языка.
В вышеупомянутом исследовании видно, что питон в два раза продуктивнее джавы. Есть и другие, показывающие то же самое. В Rosetta Code провели довольно глубокое изучение различий языков программирования. В этой работе сравнивается питон и другие скриптовые/интерпретируемые языки, и говорится:
Питон, как правило, более “лаконичный”, даже по сравнению с функциональными языками (код, в среднем, короче в 1.2–1.6 раза)
В общем, похоже, что количество строк в питоне всегда меньше. “Количество строк” может звучать как ужасная метрика, но множественные исследования, включая два упомянутых выше, показывают, что время, потраченное на каждую строку кода, одинаково почти во всех языках. Таким образом, сокращение количества строк увеличивает продуктивность. Даже сам codinghorror (C#-программист) написал статью о том, на сколько питон продуктивнее.
Я считаю, что будет справедливо сказать, что питон более продуктивен по сравнению со множеством других языков. В основном это связано с тем, что он поставляется “в комплекте с батарейками” и к нему написано много сторонних библиотек. Здесь можно найти простую статью о различиях питона и Х. Если ты не понимаешь почему питон такой “маленький” и продуктивный, я хочу чтобы ты воспользовался возможностью немного изучить питон и самому на всё взглянуть. Вот твоя первая программа:
Но что если скорость действительно важна?
По тону всего вышесказанного можно подумать, что оптимизация и скорость вообще не имеют значения. Но, по правде говоря, производительность выполнения нужна много где. Один из примеров: у тебя есть веб-приложение и ответ с определённого запроса приходит действительно долго. Ты знаешь на сколько быстрым он должен быть и как его улучшить.
В нашем примере стоит подметить пару вещей:
- Мы нашли конкретный, медленно выполняемый запрос
- Мы поняли, что он медленный, потому что у нас есть показатель достаточно быстрого, и результат ему не соответствует.
Нам не нужно заниматься микрооптимизацией всего подряд в нашем приложении. Нужно добиться, чтобы всё было всего лишь “достаточно быстрым”. Твои пользователи могут заметить, что запрос отрабатывает за пару секунд, но они не заметят, что ты улучшил этот показатель с 35 до 25 миллисекунд. “Довольно хорошо” — вот всё, чего тебе нужно достичь. Отступление: Вероятно, стоит отметить, что есть некоторые приложения, нуждающиеся в микрооптимизации, в которых важна каждая доля секунды. Например, торги в реальном времени. Но это исключение, а не правило.
Чтобы найти способ оптимизации конкретной точки, твоим первым шагом будет обзор кода. Нужно понять где находится узкое место. В конце концов:
Улучшения, сделанные где-либо, кроме узкого места, являются иллюзией. — Джин Ким
Если оптимизация не затрагивает узкое место — ты попросту тратишь своё время и не исправляешь реальную проблему. Ты не получишь серьёзных улучшений, пока не оптимизируешь узкое место. Если попытаешься оптимизировать до того, как поймёшь где это самое место, то получится, что ты просто играешь в прибей крота с кусками своего кода. Оптимизация до оценки и определения узкого места известна как “поспешная оптимизация”. Следующую цитату часто приписывают Дональду Кнуту, но он утверждает, что сам стащил её у кого-то другого:
Поспешная оптимизация является корнем всех зол.
Говоря о поддержке кодовой базы, более полная цитата Кнута выглядит так:
Мы должны вообще забыть об оптимизации в, скажем, 97% случаев; более того, поспешная оптимизация является корнем всех зол. И напротив, мы должны уделить все внимание оставшимся 3%.
Другими словами, он говорит, что тебе не нужно помнить об оптимизации своего кода большую часть времени. Он практически всегда достаточно хорош. А в тех случаях, когда это не так, нам нужно обращать внимание на три процента кодовой базы. Ты не получишь никаких призов, выигрывая пару наносекунд при обработке запроса с помощью цикла if, а не вызова функции, к примеру. Оптимизируй только после того, как проведёшь измерения.
Поспешная оптимизация включает в себя вызов некоторых быстрых методов, или даже использование специфичной структуры данных, потому что обычно они быстрее. Информатика утверждает, что если у двух методов или алгоритмов одинаковый асимптотический рост (или О), то они одинаковы, даже если на практике один из них в два раза медленнее. Компьютеры на столько быстры, что при увеличении данных/использований — количество вычислений по алгоритму имеет гораздо большее значение, чем сама фактическая скорость. Иначе, если у тебя есть две О(log n) функции, но одна из них в два раза медленнее, это не имеет значения. При увеличении объёмов данных, они обе будут “замедляться” с одинаковым темпом. Вот почему поспешная оптимизация является корнем всех зол; она тратит наше время, и практически никогда не приносит пользы в общей производительности.
С точки зрения О, можно утверждать, что сложность твоей программы будет O(n) для всех языков, где n — количество строк кода или инструкций. Они все растут с одинаковым темпом для одинаковых инструкций. Не важно на сколько медленный язык/время выполнения, с точки зрения асимптотического роста, все языки созданы одинаковыми. Согласно такой логике, можно сказать, что конечной формой поспешной оптимизации будет выбор языка для приложения, просто потому что он “быстрый”. Ты выбираешь что-то предположительно быстрое без измерений, без понимания того, где будет узкое место.
Конечная форма поспешной оптимизации — выбор языка для приложения просто потому, что он “быстрый”.
Оптимизация питона
Одна из моих любимых особенностей питона в том, что он позволяет тебе оптимизировать код в небольших объёмах. Предположим, что у тебя есть метод, который ты определил как узкое место. Ты оптимизировал его несколько раз, возможно следуя рекомендациям от сюда и от туда, и сейчас ты в той стадии, когда думаешь, что питон сам по себе является узким местом. У питона есть возможность вызывать код на С, а это значит, что ты можешь переписать тот самый метод на С и уменьшить проблемы с производительностью. Ты можешь применять это к каждому конкретному методу. Таким образом ты можешь писать хорошо оптимизированные узкие места на любом языке, который компилируется в С-совместимый ассемблер. Это позволяет тебе оставаться в питоне большую часть времени, спускаясь на более низкий уровень когда это действительно нужно.
Есть такой язык, Cython — приемник питона. Это практически смесь питона и С, и является постепенно типизированным языком. Любой питон-код — это валидный Cython-код, который компилируется в C-код. С помощью Cython ты можешь написать модуль или метод и постепенно двигаться в сторону C-типизации и производительности. Ты можешь смешивать вместе типы C и утиную типизацию питона. Используя Cython, ты получишь идеальную смесь оптимизации только узких мест и красоту питона во всех остальных случаях.
Когда в конечном итоге ты упрёшься в питоновскую стену бед с производительностью, то не обязательно переносить всю кодовую базу на другой язык. Почти всегда ты можешь получить желаемую производительность, просто переписав пару методов на Cython. Такую стратегию применяет Eve Online. Это массивная многопользовательская компьютерная игра, весь стек которой составляют Python и Cython. Они достигают игрового уровня производительности оптимизируя узкие места на C/Cython. Если у них получилось, то должно получиться и у всех остальных. Кроме того, существуют и другие способы оптимизации твоего питон-кода. Например, PyPy является JIT-реализацией питона, позволяющей получить значительные улучшения для длительно запущенных приложений (например веб-серверы), просто оборачивая CPython (реализацию по умолчанию) в PyPy.
Повторю некоторые основные моменты:
- Оптимизируй свой самый дорогой ресурс. Это ТЫ, а не компьютер.
- Выбирай язык/фреймворк/архитектуру, помогающие тебе быстрее разрабатывать (например, питон). Не выбирай технологии просто потому что они быстрые.
- Если у тебя есть проблемы с производительностью: найди узкое место.
- Вероятность того, что процессор или питон являются узким местом — довольно мала.
- Если питон является узким местом (ты уже оптимизировал алгоритмы и т.д.), то применяй Cython/C где необходимо.
- Возвращайся к наслаждению от быстрого выполнения задач.
Надеюсь, тебе понравилось читать эту статью так же сильно, как мне понравилось писать её. Если хочешь сказать спасибо, просто кликни на сердечко. Если захочешь как-нибудь поговорить со мной о питоне, найди меня в твиттере (@nhumrich) или в слэк-канале о питоне.
Стоит ли винить Python в низкой производительности?

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

Реально ли Python слишком медленный?
С моей точки зрения подобные вопросы стоит задавать в рамках определенного контекста или случая применения. Действительно ли Python медленнее справляется с вычислением чисел, чем компилируемые языки вроде C? Да, так и есть. Причем данный факт известен давно, и именно поэтому библиотеки Python, для которых важна скорость, например NumPy, внутренне задействуют C.
Но во всех ли случаях Python медленнее других языков, которые, к слову, сложнее осваивать и использовать. Если рассмотреть бенчмарки производительности многих библиотек Python, оптимизированных под решение конкретной задачи, то по сравнению с компилируемыми языками справляются они вполне достойно. К примеру, взгляните на тест производительности FastAPI: очевидно, что Go, будучи компилируемым языком, оказывается намного быстрее Python. И все же FastAPI превосходит некоторые из библиотек Go при сборке REST API:
Бенчмарки веб-фреймворков
Сноска: в приведенный список не включены C++ и Java, которые демонстрируют еще большую производительность.
Аналогичным образом при сравнении Dask (написана на Python) со Spark (написана на Scala) для конвейеров нейровизуализации, работающих с большими данными, авторы сделали такой вывод.
В целом результаты не показывают существенной разницы производительности между этими движками.
По сути, нужно просто поставить вопрос: “Какая именно скорость нас фактически интересует?”. Если вы выполняете задачу ETL, которая запускается всего раз в день, то вас может вообще не волновать, выполнится ли она за 20 или 200 секунд. В таком случае вы можете предпочесть сделать код простым для понимания, упаковывания и обслуживания с учетом того, что вычислительные ресурсы становятся все более доступными по отношению к дорогостоящему времени разработки.
Вам быстрый или практичный код?
С практической точки зрения есть множество вопросов, которые стоит задать при выборе языка программирования для повседневной работы.
Можно ли с его помощью уверенно решать несколько бизнес-задач?
Если вас интересует лишь скорость, тогда не используйте Python и точка. Есть куда более производительные альтернативы для всевозможных случаев применения. Основные преимущества Python лежат в его читаемости, простоте использования и широком спектре решаемых с его помощью задач. Этот язык можно задействовать в качестве связующего компонента для огромного множества различных систем, сервисов и случаев использования.
Легко ли будет найти достаточное число знакомых с ним сотрудников?
Поскольку Python прост в освоении и применении, количество его пользователей постоянно растет. Бизнес-пользователи, которые ранее работали с числами в Excel, теперь могут быстро освоить программирование на Pandas и стать самодостаточными, избавившись от необходимости постоянно полагаться на ИТ-структуру компании. Это, в свою очередь, снизит нагрузку на департаменты ИТ и аналитики. Помимо прочего, таким образом сокращается время разработки и вывода продукта на рынок.
Сегодня гораздо проще найти инженеров знакомых с Python и способных обслуживать Spark-приложение по обработке данных на этом языке, чем тех, кто мог бы проделать то же самое на Java или Scala. Множество организаций постепенно переходят к реализации различных задач с помощью Python, просто потому что гораздо проще найти сотрудников, “говорящих” на этом языке.
В противоположность этому мне известны компании, которые отчаянно нуждаются в Java или C#-разработчиках, которые бы обслуживали существующие приложения. Однако на освоение этих языков уходят годы, в связи с чем они выглядят менее привлекательными для начинающих программистов, которые потенциально могут заработать больше на должностях, где требуется знание менее сложных языков, таких как Go или Python.
Синергия между экспертами из разных областей
Если ваша компания использует Python, то велики шансы, что тот же язык смогут применять бизнес-пользователи, аналитики/инженеры данных, бэкенд и фронтенд-разработчики, инженеры DevOps и даже системные администраторы. Это ведет к эффекту синергии в проектах, где люди из разных областей могут работать сообща и задействовать одни и те же инструменты.
Каковы истинные слабые места в обработке данных?
В своей работе я обычно сталкиваюсь с проблемными местами не в самом языке, а скорее во внешних ресурсах. Для пущей точности я просто приведу ряд примеров.
Запись в реляционные базы данных
При ETL-обработке данных в конечном итоге требуется загружать их в некое централизованное хранилище. И хотя можно задействовать многопоточность Python для ускорения записи данных в реляционную БД, скорее всего это приведет к тому, что увеличение числа параллельных процессов записи превысит ЦПУ-возможности этой базы данных.
На деле я сталкивалась с этим однажды, когда применяла многопоточность для ускорения записи в БД RDS Aurora на AWS. Тогда я заметила, что использование ЦПУ для узла записи настолько возросло, что мне пришлось намеренно замедлить код уменьшением числа потоков, чтобы не нарушить работу экземпляра БД.
Это означает, что в Python есть механизмы для распараллеливания и ускорения многих операций, но реляционная база данных (ограниченная по числу ядер ЦПУ) имеет предел возможностей, который не удастся преодолеть за счет более скоростного языка программирования.
Совершение вызовов к внешним API
Работа с внешними REST API, из которых вам может потребоваться извлечь данные для аналитики, является еще одним примером, где сам по себе язык не оказывается слабым местом. Мы, конечно, можем ускорить извлечение данных, задействовав параллелизм, но иногда это будет напрасным, так как многие внешние API ограничивают количество запросов, которые можно совершать за определенный промежуток времени.
Таким образом, вы зачастую будете оказываться в ситуации, когда приходится намеренно замедлять скрипт, обеспечивая тем самым соответствие установленному для API максимальному числу запросов:
Работа с большими данными
Из своего опыта работы с огромными датасетами могу сказать, что, независимо от используемого языка, загружать действительно “большие данные” в память ноутбука не получится. Для подобных случаев вам скорее понадобится применять фреймворки распределенной обработки, такие как Dask, Spark, Ray и т.д. Существует лимит на количество данных, которые можно обработать при использовании одного экземпляра сервера на ноутбуке.
Если вы хотите распределить фактическую обработку данных на кластер вычислительных узлов, возможно даже задействовав инстансы GPU, которые могут дополнительно ускорить вычисления, то в Python есть обширная экосистема фреймворков, которые эту задачу упростят.

- Хотите ускорить вычисления при обработке данных, задействовав GPU? Используйте Pytorch, Tensorflow, Ray или Rapids (даже с SQL — BlazingSQL).
- Хотите ускорить код Python для обработки больших данных? Используйте Spark (или Databricks), Dask либо Prefect (который внутренне абстрагируется от Dask).
- Хотите ускорить обработку данных при аналитике? Используйте быстрые специализированные резидентные столбчатые базы данных, которые обеспечат высокую скорость обработки при простом использовании SQL-запросов.
Если же вам нужно оркестрировать и отслеживать обработку данных, происходящую в кластере вычислительных узлов, то есть несколько платформ управления рабочим потоком, написанных на Python, которые ускорят разработку и повысят обслуживаемость конвейеров обработки данных. В их число входят Apache Airflow, Perfect и Dagster.
Дополнительно отмечу, что многие, кто жалуются на Python, просто не задействуют все его возможности или применяют для решаемой задачи не самые подходящие структуры данных.
Обобщим. Если вы хотите быстро обрабатывать большие объемы данных, то вам скорее потребуется больше вычислительных ресурсов, чем более скоростной язык программирования. Причем существуют библиотеки Python, которые упрощают процесс распределения работы по сотням узлов.
Заключение
В этой статье мы ответили на вопрос “Действительно ли Python является слабым местом в текущем ландшафте обработки данных?”. Несмотря на то, что он реально медленнее многих компилируемых языков программирования, Python легок в использовании и очень разносторонен. Мы также отметили, что для многих практическая сторона языка берет верх над его производительностью.
В завершении было сказано о том, что как минимум при работе с данными слабым местом скорее явится не сам язык, а внешние системы и большой объем данных, который препятствует их обработке на одной машине, независимо от используемого языка программирования.