Как ускорить код на Python в тысячу раз

Kate

Administrator
Команда форума

Обычно говорят, что Python очень медленный​


В любых соревнованиях по скорости выполнения программ Python обычно занимает последние места. Кто-то говорит, что это из-за того, что Python является интерпретируемым языком. Все интерпретируемые языки медленные. Но мы знаем, что Java тоже язык такого типа, её байткод интерпретируется JVM. Как показано, в этом бенчмарке, Java намного быстрее, чем Python.
Вот пример, способный показать медленность Python. Используем традиционный цикл for для получения обратных величин:

import numpy as np
np.random.seed(0)
values = np.random.randint(1, 100, size=1000000)
def get_reciprocal(values):
output = np.empty(len(values))
for i in range(len(values)):
output = 1.0/values
%timeit get_reciprocal(values)

Результат:
3,37 с ± 582 мс на цикл (среднее значение ± стандартное отклонение после 7 прогонов по 1 циклу)

Ничего себе, на вычисление всего 1 000 000 обратных величин требуется 3,37 с. Та же логика на C выполняется за считанные мгновения: 9 мс; C# требуется 19 мс; Nodejs требуется 26 мс; Java требуется 5 мс(!), а Python требуется аж целых 3,37 СЕКУНДЫ. (Весь код тестов приведён в конце).

Первопричина такой медленности​


Обычно мы называем Python языком программирования с динамической типизацией. В программе на Python всё представляет собой объекты; иными словами, каждый раз, когда код на Python обрабатывает данные, ему нужно распаковывать обёртку объекта. Внутри цикла for каждой итерации требуется распаковывать объекты, проверять тип и вычислять обратную величину. Все эти 3 секунды тратятся на проверку типов.

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

8avu6zccmcs9p4icvffshcz0g2a.png


Даже простое присвоение числового значения — это долгий процесс.

a = 1


Шаг 1. Задаём a->PyObject_HEAD->typecode тип integer

Шаг 2. Присваиваем a->val =1

Подробнее о том, почему Python медленный, стоит прочитать в чудесной статье Джейка: Why Python is Slow: Looking Under the Hood

Итак, существует ли способ, позволяющий обойти проверку типов, а значит, и повысить производительность?

Решение: универсальные функции NumPy​


В отличие list языка Python, массив NumPy — это объект, созданный на основе массива C. Доступ к элементу в NumPy не требует шагов для проверки типов. Это даёт нам намёк на решение, а именно на Universal Functions (универсальные функции) NumPy, или UFunc.

e3gobrrfqknrb4t7w2p1ueaikae.png


Если вкратце, благодаря UFunc мы можем проделывать арифметические операции непосредственно с целым массивом. Перепишем первый медленный пример на Python в версию на UFunc, она будет выглядеть так:

import numpy as np
np.random.seed(0)
values = np.random.randint(1, 100, size=1000000)
%timeit result = 1.0/values

Это преобразование не только повышает скорость, но и укорачивает код. Отгадаете, сколько теперь времени занимает его выполнение? 2,7 мс — быстрее, чем все упомянутые выше языки:

2,71 мс ± 50,8 мкс на цикл (среднее значение ± стандартное отклонение после =7 прогонов по 100 циклов каждый)

Вернёмся к коду: самое важное здесь — это 1.0/values. values — это не число, а массив NumPy. Наряду с оператором деления есть множество других.

w7jzrgru6ebryh9qfrfc5bvr0oe.png


Здесь можно найти все операторы Ufunc.

Подводим итог​


Если вы пользуетесь Python, то высока вероятность того, что вы работаете с данными и числами. Эти данные можно хранить в NumPy или DataFrame библиотеки Pandas, поскольку DataFrame реализован на основе NumPy. То есть с ним тоже работает Ufunc.

UFunc позволяет нам выполнять в Python повторяющиеся операции быстрее на порядки величин. Самый медленный Python может быть даже быстрее языка C. И это здорово.

Приложение — код тестов на C, C#, Java и NodeJS​


Язык C:

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

int main(){
struct timeval stop, start;
int length = 1000000;
int rand_array[length];
float output_array[length];
for(int i = 0; i<length; i++){
rand_array = rand();
}
gettimeofday(&start, NULL);
for(int i = 0; i<length; i++){
output_array = 1.0/(rand_array*1.0);
}
gettimeofday(&stop, NULL);
printf("took %lu us\n", (stop.tv_sec - start.tv_sec) * 1000000 + stop.tv_usec - start.tv_usec);
printf("done\n");
return 0;
}

C#(dotnet 5.0):

using System;
namespace speed_test{
class Program{
static void Main(string[] args){
int length = 1000000;
double[] rand_array =new double[length];
double[] output = new double[length];
var rand = new Random();
for(int i =0; i<length;i++){
rand_array = rand.Next();
//Console.WriteLine(rand_array);
}
long start = DateTimeOffset.Now.ToUnixTimeMilliseconds();
for(int i =0;i<length;i++){
output = 1.0/rand_array;
}
long end = DateTimeOffset.Now.ToUnixTimeMilliseconds();
Console.WriteLine(end - start);
}
}
}

Java:

import java.util.Random;

public class speed_test {
public static void main(String[] args){
int length = 1000000;
long[] rand_array = new long[length];
double[] output = new double[length];
Random rand = new Random ();
for(int i =0; i<length; i++){
rand_array = rand.nextLong();
}
long start = System.currentTimeMillis();
for(int i = 0;i<length; i++){
output = 1.0/rand_array;
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}

NodeJS:

let length = 1000000;
let rand_array = [];
let output = [];
for(var i=0;i<length;i++){
rand_array = Math.floor(Math.random()*10000000);
}
let start = (new Date()).getMilliseconds();
for(var i=0;i<length;i++){
output = 1.0/rand_array;
}
let end = (new Date()).getMilliseconds();
console.log(end - start);



Источник статьи: https://habr.com/ru/company/vdsina/blog/552378/
 
Сверху