У меня есть фон Matlab, и когда я купил год назад ноутбук, я тщательно выбрал тот, который обладает большой вычислительной мощностью, машина имеет 4 потока и предлагает мне 8 потоков с частотой 2,4 ГГц. Машина зарекомендовала себя как очень мощная, и, используя простые циклы parfor, я мог использовать все потоки процессора, с которыми я получил ускорение около 8 для многих задач и экспериментов.
В это прекрасное воскресенье я экспериментировал с numpy, люди часто говорят мне, что основной бизнес numpy эффективно реализован с использованием libblas и, возможно, даже с использованием нескольких ядер и библиотек, таких как OpenMP (с OpenMP вы можете создавать парфор-подобные циклы, используя прагму в стиле c ).
Это общий подход для многих числовых и машинных алгоритмов обучения, вы выражаете их с помощью дорогостоящих операций высокого уровня, таких как умножение матриц, но для удобства в дорогом языке высокого уровня, таком как Matlab и python. Более того, c (++) позволяет нам обходить GIL.
Крутая часть в том, что линейная алгебраическая обработка должна выполняться очень быстро в python, когда бы вы ни использовали numpy. У вас просто есть накладные расходы при вызове некоторых функций, но тогда, если вычисления за ними велики, это незначительно.
Итак, даже не касаясь темы о том, что не все может быть выражено в линейной алгебре или других пустых операциях, я изложил это:
t = time.time(); numpy.dot(range(100000000), range(100000000)); print(time.time() - t)
40.37656021118164
Таким образом, я за эти 40 секунд увидел, что ОДИН из 8 потоков на моей машине работал на 100%, а остальные — около 0%. Мне это не понравилось, но даже с одним работающим потоком я ожидал, что это будет выполняться примерно через 0 секунд. Точечный продукт выполняет 100M + и *, поэтому мы имеем 2400M / 100M = 24 такта в секунду для одного +, одного * и любых дополнительных затрат.
Тем не менее, алгоритму нужно 40 * 24 = приблизительно = 1000 тиков (!!!!!) для +, * и накладных расходов. Давайте сделаем это в C ++:
#include<iostream>
int main() {
unsigned long long result = 0;
for(unsigned long long i=0; i < 100000000; i++)
result += i * i;
std::cout << result << '\n';
}
БЛИЦ:
herbert@machine:~$ g++ -std=c++11 dot100M.cc
herbert@machine:~$ time ./a.out
662921401752298880
real 0m0.254s
user 0m0.254s
sys 0m0.000s
0,254 секунды, почти в 100 раз быстрее, чем numpy.dot.
Я подумал, что, возможно, генератор диапазона python3 — это медленная часть, поэтому я помешал реализации c ++ 11, сначала сохранив все 100M чисел в std :: vector (используя итеративные push_back-ы), а затем итерируя по нему. Это было намного медленнее, это заняло чуть меньше 4 секунд, что все еще в 10 раз быстрее.
Я установил свой numpy с помощью ‘pip3 install numpy’ в ubuntu, и он некоторое время начал компилироваться, используя как gcc, так и gfortran, более того, я видел упоминания о файлах blas-header, проходящих через выходные данные компилятора.
По какой причине numpy.dot так медленно работает?
Так что ваше сравнение несправедливо. В вашем примере с Python вы сначала генерируете два объекта диапазона, конвертируете их в numpy-массивы, а затем выполняете скалярный продукт. Расчет занимает наименьшую часть. Вот цифры для моего компьютера:
>>> t=time.time();x=numpy.arange(100000000);numpy.dot(x,x);print time.time()-t
1.28280997276
И без генерации массива:
>>> t=time.time();numpy.dot(x,x);print time.time()-t
0.124325990677
Для завершения C-версия занимает примерно столько же времени:
real 0m0.108s
user 0m0.100s
sys 0m0.007s
range
генерирует список на основе ваших заданных параметров, где в качестве вашего for
Цикл в C просто увеличивает число.
Я согласен, что с точки зрения вычислений довольно затратно тратить так много времени на создание одного списка — опять же, это большой список, и вы запрашиваете два из них 😉
РЕДАКТИРОВАТЬ: как упоминалось в комментариях range
генерирует списки, а не массивы.
Попробуйте заменить ваш range
метод с приращением while
цикл или подобное и посмотреть, если вы получите более терпимые результаты.