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 со скоростью света!

+1
0
+1
1
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *