Python со скоростью света
Даже если каждая новая версия значительно повышает производительность, Python все еще остается высокоуровневым интерпретируемым языком без сильной типизации; поэтому его скорость отличается от низкоуровневых языков, таких как C. Более того, GIL (Global Interpreter Lock) делает Python фактически однопроцессным: вы можете иметь несколько потоков, но одновременно запущен только один.
К счастью, сообщество Python активно, и многочисленные специализированные пакеты, большинство из которых реализованы на C, могут предложить значительный прирост скорости и преодолеть проблему GIL.
Приведем тривиальный пример, заполнив список из n целых чисел.
def populate_python(size:int)->list:
b = []
for i in range(size):
b.append(i)
return b
Эта простая функция получает значение (целое число) и возвращает список, заполненный целыми числами. Я использовал подсказки типов, которые чрезвычайно полезны для проверки кода и повышения его читабельности, но вы не ограничены запрашиваемым типом. Python примет любое значение для функции и выдаст ошибку во время выполнения, если вы передадите несовместимое значение.
Первая проблема заключается в том, что список в Python может содержать любое значение, например, целое число, плавающее число, строку или даже другой список. Это очень удобно, но это нельзя оптимизировать. Таким образом, операции со списками в Python выполняются относительно медленно. Вторая проблема – цикл for, который в Python, как правило, медленный (даже если последние версии сделали гигантский шаг вперед) и не может быть распараллелен. Запуск функции с size = 10 000 000 занял в среднем 765 мс на моей машине. Неплохо, в конце концов. Можем ли мы сделать лучше?
Использование numpy
Массив numpy отличается от списка Python. В numpy каждый элемент массива должен быть одного типа. Это делает управление памятью более простым, а вычисления – более быстрыми. Более того, numpy часто использует C внутри. В нашем случае, однако, выигрыша нет; скорее, скорость снизилась.
import numpy as np
def populate_numpy(size:int)->np.ndarray:
b = np.empty((size),dtype=np.int64)
for i in range(size):
b[i] = i
return b
Запуск функции с тем же количеством элементов занял 964 мс. По сути, функция не использует преимущества векторизации numpy и только добавляет накладные расходы.
Использование numba
from numba import njit, prange
@njit
def populate_numba(size:int)->np.ndarray:
b = np.empty((size),dtype=np.int64)
for i in prange(size):
b[i] = i
return b
Как мы видим, функция почти такая же. Я просто добавил декоратор и prange (функция диапазона в numba, которая работает параллельно). На этот раз время вычислений составило всего 16 мс! Почти в 50 раз быстрее, чем в голом Python. Это впечатляющий выигрыш.
Julia
Julia – еще один язык, который набирает обороты. Его цель – предложить почти такую же гибкость и понятный синтаксис, как у Python, но с высокой скоростью компиляции кода.
function populate_array(size::Int)::AbstractVector{Int64}
b = Vector{Int64}(undef,size)
Threads.@threads for i=1:size
b[i] = i
end
return b
end
В Julia нет проблем с GIL, поэтому потоки могут работать параллельно. Это заняло всего 12 мс.
Mojo
Mojo – это новый язык, находящийся в стадии активной разработки. Согласно информации на сайте: Mojo сочетает в себе удобство использования Python и производительность C, открывая беспрецедентные возможности программирования оборудования ИИ и расширяемости моделей ИИ.
В настоящее время Mojo еще не доступен, но вы можете попросить доступ к тестовой площадке – среде, подобной Jupyterlab, где вы можете попробовать этот новый язык. Даже если вы можете использовать чистый Python в Mojo, вы должны использовать другой синтаксис, более близкий к C, чем Python, чтобы в полной мере воспользоваться преимуществами его превосходной скорости.
from Pointer import DTypePointer
from Random import rand, random_ui64
from DType import DType
from Range import range
from Functional import parallelize
import SIMD
struct Vect:
var data: DTypePointer[DType.uint64]
var rows: Int
fn __init__(inout self, rows: Int):
self.data = DTypePointer[DType.uint64].alloc(rows)
self.rows = rows
fn __del__(owned self):
self.data.free()
@always_inline
fn len(self)->UInt64:
return self.rows
fn zero(inout self):
memset_zero(self.data, self.rows)
@always_inline
fn __getitem__(self, x: Int) -> UInt64:
return self.data.load(x)
@always_inline
fn __setitem__(self, x: Int, val: UInt64):
return self.data.store( x, val)
fn populate_mojo(b:Vect):
@parameter
fn process_row(i:Int):
b[i] = i
parallelize[process_row](b.rows)
Приведенная выше функция заняла всего 7 мс на тестовой площадке, на 110 быстрее, чем Python, и намного быстрее, чем Julia. Это Python со скоростью света!