Автор Python-профайлера Sciagraph Векторизация NumPy ускоряет операции с объёмными данными за счёт быстрого низкоуровневого кода. На основе NumPy, чтобы предоставить столь же быструю функциональность, написана и Pandas. Но векторизация — не панацея. Иногда проблема решается за счёт памяти, нужная операция не поддерживается или просто не нужна. У каждой из этих проблем есть разные решения. Если резюмировать подробное объяснение векторизации, в контексте Python у векторизации три значения. Это: API для работы с массивными данными: например, код arr += 1 прибавит 1 к каждому элементу массива NumPy. Низкоуровневый API, например, на C или Rust. Он быстро работает с большим объёмом данных — это основная тема статьи. Инструкции SIMD, ускоряющие операции низкого уровня ещё сильнее. Подробности — в основном описании векторизации. Для начала предположим, что работаем с Python по умолчанию, то есть CPython. Список целых чисел Python в CPython — это серия указателей на довольно большие объекты. С точки зрения CPython это обычные объекты Python, и прибавление целого числа к каждому элементу списка состоит из следующих шагов: Поиск типа каждого элемента в списке. Поиск функции сложения данного типа чисел. Вызов найденной функции с исходным и добавляемым объектами. Конвертирование обоих объектов Python в машинные целые числа. Сложение машинных чисел. Оборачивание полученного целого в объект Python. Поиск следующего элемента в списке Python, что само по себе — дорогая операция, включающая поиск методов итератора для типа диапазона, затем поиск операции индексации для типа списка, преобразование объекта индекса в машинное целое число… Много работы! Напротив, массив целых чисел NumPy — это массив машинных целых чисел. Таким образом, добавление целого числа к каждому элементу — всего лишь несколько процессорных инструкций для каждого элемента; после инициализации это ничем не отличается от эквивалента на C. Вот код NumPy: NumPy работает намного быстрее: Реализация Прошло (сек) CPython 6.13 NumPy 0.07 Векторизация ускоряет код, это здорово… но решение не идеальное. Вот первая проблема: Допустим, нужно узнать среднее расстояние элементов массива от числа 0. Один из способов сделать это — вычислить абсолютное значение каждого элемента и взять среднее значение элементов: Это работает, и оба вычисления выполняются быстро, но нужен совершенно новый промежуточный массив абсолютных значений. Если исходный массив содержит 1000 МБ, появится лишняя 1000 МБ. Исходный массив можно перезаписать, чтобы сэкономить память, но он может понадобиться по другим причинам. Чтобы вместо N копий осталась только одна дополнительная копия, можно воспользоваться операциями на месте. Есть библиотеки, например numexpr и Dask Array, способные с сокращением нагрузки на память выполнять пакетные или более интеллектуальные обновления на месте. А можно просто рассчитать всё поэлементно, собственным циклом, но это приведёт ко второй проблеме: Чтобы векторизация работала, нужен низкоуровневый машинный код, который выполняет операцию и управляет циклом над данными. При переключении обратно, на циклы и функциональность обычного Python, скорость теряется. Например, очевидный способ реализовать mean_distance_from_zero() без увеличения потребляемой памяти выглядит так: Но мы вернулись к циклу и операциям в Python, код снова медленный — и вот одна из причин, почему NumPy приходится так много реализовывать самой: код замедляется при каждом возврате к Python. В компилируемом языке есть вероятность, что компилятор найдёт способ оптимизировать вторую реализацию, и это даст ускорение, которое невозможно с серией отдельных векторизованных операций. Иногда ваш код работает медленно потому, что выполняет одну и ту же операцию со многими элементами данных одного и того же типа — тогда и пригодится векторизация. В других случаях расчёт слишком медленный по другой причине, а значит, векторизация не поможет. Посмотрим три основных: Ускоренный интерпретатор Python, PyPy — реализация CPython намного умнее: она может использовать JIT-компиляцию, ускоряя выполнение генерацией специализированного машинного кода. Генерация машинного кода внутри CPython через Numba. Компиляция кастомного кода с помощью Cython, C, C++ или Rust. С Python-списком целых чисел CPython на самом деле не делает ничего хитрого, поэтому и нужны массивы NumPy. PyPy здесь разумнее CPython: для такого случая он генерирует специализированный код.  Вернёмся к исходному примеру: И сравним Cython с PyPy: Скорость PyPy сравнима со скоростью NumPy — а это обычный цикл Python. К сожалению, PyPy не особенно хорошо работает с NumPy; наивный невекторизованный цикл над массивом NumPy (например, реализованная выше mean()) в PyPy намного медленнее, чем в CPython. Поэтому PyPy бесполезен в попытке справиться с недостающим функционалом NumPy, а медленный невекторизованный цикл он даже замедлит ещё сильней. Но, если вы имеете дело с кодом на стандартных объектах Python, PyPy может оказаться намного быстрее CPython, поэтому лучше всего он подходит для ускорения, когда NumPy или Pandas не используются вообще. Numba также выполняет JIT-компиляцию, но в отличие от PyPy действует как дополнение к CPython и создан для работы с NumPy. Посмотрим, как это решит наши проблемы: Недостающие операции для Numba не проблема: просто напишите собственную функцию. Вот, например, mean_distance_from_zero() на чистом Python и Numba: Первый вызов Numba небыстрый: библиотека должна скомпилировать кастомный машинный код. После этого вызовы ускоряются и CPython сильно уступает Numba: Поскольку мы можем писать собственные дополнительные функции для NumPy с помощью Numba, отсутствующие операции — не проблема. Это также означает, что из-за ограничений доступных массивов NumPy не нужно создавать временные массивы, поэтому Numba может помочь решить первые две проблемы, с которыми мы столкнулись. Numba не так полезна в невекторизованных операциях, поскольку они не входят в задачи проекта. Мы увидели два примера JIT-компиляции: целый JIT-интерпретатор и единичные JIT-функции. Третий способ ускориться — предварительная компиляция. При помощи Cython, Rust или C вы сможете: Создавать быстрые операции для NumPy через встроенную в Cython поддержку NumPy, rust-numpy в Rust или просто C API Python. Сама NumPy в основном написана на C, а её расширения — и на других языках, таких как Fortran или C++. Для случаев без векторизации, чтобы писать быстрые расширения, опять же можно использовать эти языки. Но, чтобы скомпилировать эти расширения перед запуском Python, придётся добавить много настроек конфигурации упаковки или настроить сборку. Cython умеет компилировать при импорте, но это осложняет распространение вашего ПО: его пользователям будет нужен компилятор. Numba таких ограничений не имеет. С векторизованной операцией PyPy не поможет. Хотя сделать последний полезнее могут проекты типа HPy Project.  Остаются Numba и скомпилированные расширения. Компилировать нужно заранее, а это требует компилятора и сложной упаковки, поэтому обычно легче настроить Numba. Numba без дополнительной работы создаёт разные версии кода для разных типов: целых чисел или чисел с плавающей точкой; с компилируемым кодом вам нужно скомпилировать версию для каждого типа числа и, возможно, также для каждого типа написать код. С помощью Rust или C++ необходимость дублирования кода можно обойти дженериками и шаблонами. Numba — это сильно ограниченное подмножество Python, так что отладка, связанная с его ограничениями, может оказаться затруднительной, а компилируемые языки более гибкие; Cython поддерживает почти весь Python, Rust — сложный язык с отличными инструментами, и т. д. PyPy часто ускоряет невекторизованные операции без изменений кода; можно просто попробовать свой код и посмотреть его скорость. Иначе остаётся оптимизация существующего кода, или выбор подходящего компилируемого языка. А пока Python ускоряется, мы поможем прокачать ваши навыки или с самого начала освоить профессию, актуальную в любое время: Профессия Fullstack-разработчик на Python Профессия Data Scientist Выбрать другую востребованную профессию.