numpy float: в 10 раз медленнее, чем встроенные арифметические операции?

#python #performance #numpy #floating-point

#python #Производительность #numpy #с плавающей запятой

Вопрос:

Я получаю действительно странные тайминги для следующего кода:

 import numpy as np
s = 0
for i in range(10000000):
    s  = np.float64(1) # replace with np.float32 and built-in float
  
  • встроенный float: 4,9 с
  • float64: 10,5 с
  • float32: 45.0 с

Почему float64 в два раза медленнее, чем float ? И почему float32 в 5 раз медленнее, чем float64?

Есть ли какой-либо способ избежать штрафа за использование np.float64 и заставить numpy функции возвращать встроенные float вместо float64 ?

Я обнаружил, что использование numpy.float64 намного медленнее, чем float в Python, и numpy.float32 еще медленнее (хотя я на 32-разрядной машине).

numpy.float32 на моей 32-разрядной машине. Поэтому каждый раз, когда я использую различные функции numpy, такие как numpy.random.uniform , я преобразую результат в float32 (чтобы дальнейшие операции выполнялись с 32-разрядной точностью).

Есть ли какой-либо способ установить одну переменную где-нибудь в программе или в командной строке и заставить все функции numpy возвращать float32 вместо float64 ?

ПРАВКА # 1:

numpy.float64 в арифметических вычислениях в 10 раз медленнее, чем float. Это настолько плохо, что даже преобразование в float и обратно перед вычислениями заставляет программу работать в 3 раза быстрее. Почему? Могу ли я что-нибудь сделать, чтобы это исправить?

Я хочу подчеркнуть, что мои тайминги не связаны ни с одним из следующих:

  • функция вызывает
  • преобразование между numpy и python float
  • создание объектов

Я обновил свой код, чтобы было понятнее, в чем проблема. С новым кодом, казалось бы, я вижу десятикратное снижение производительности от использования типов данных numpy:

 from datetime import datetime
import numpy as np

START_TIME = datetime.now()

# one of the following lines is uncommented before execution
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0

for i in range(10000000):
    s = (s   8) * s % 2399232

print(s)
print('Runtime:', datetime.now() - START_TIME)
  

Тайминги такие:

  • float64: 34,56 с
  • float32: 35,11 с
  • float: 3,53 с

Просто ради интереса я тоже попробовал:

из datetime import datetime импортирует numpy как np

 START_TIME = datetime.now()

s = np.float64(1)
for i in range(10000000):
    s = float(s)
    s = (s   8) * s % 2399232
    s = np.float64(s)

print(s)
print('Runtime:', datetime.now() - START_TIME)
  

Время выполнения составляет 13,28 с; на самом деле преобразование float64 в float и обратно происходит в 3 раза быстрее, чем использование его как есть. Тем не менее, преобразование требует своего, так что в целом это более чем в 3 раза медленнее по сравнению с чистым python float .

Моя машина:

  • Intel Core 2 Duo T9300 (2,5 ГГц)
  • WinXP Professional (32-разрядная версия)
  • ActiveState Python 3.1.3.5
  • Numpy 1.5.1

ПРАВКА # 2:

Спасибо за ответы, они помогают мне понять, как справиться с этой проблемой.

Но я все же хотел бы знать точную причину (возможно, основанную на исходном коде), почему приведенный ниже код выполняется в 10 раз медленнее с float64 , чем с float .

ПРАВКА # 3:

Я перезапускаю код под Windows 7 x64 (Intel Core i7 930 с частотой 3,8 ГГц).

Опять же, код:

 from datetime import datetime
import numpy as np

START_TIME = datetime.now()

# one of the following lines is uncommented before execution
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0

for i in range(10000000):
    s = (s   8) * s % 2399232

print(s)
print('Runtime:', datetime.now() - START_TIME)
  

Тайминги такие:

  • float64: 16,1 с
  • float32: 16,1 с
  • float: 3,2 с

Теперь оба np числа с плавающей точкой (64 или 32) в 5 раз медленнее встроенных float . Тем не менее, существенная разница. Я пытаюсь выяснить, откуда это берется.

ОКОНЧАНИЕ РЕДАКТИРОВАНИЯ

Комментарии:

1. Какая версия Python? Какая версия numpy? Если Python 2.x, используйте xrange вместо range (range будет создавать огромный список). float (1) — это не та операция, которую многие ожидали бы использовать часто; float (i) может быть немного более реалистичным. С какой стати вы хотите использовать 32-разрядную точность?

2. Numpy говорит, что его значения с плавающей точкой по умолчанию 64-разрядные, что объясняет, почему 32-разрядные значения с плавающей точкой медленнее (он должен их изменить). Почему указание float64 делает это намного медленнее, я не знаю. Обратите внимание, что, AFAIK, ваша архитектура не влияет на данные с плавающей точкой: 32-разрядные или 64-разрядные архитектуры связаны только с адресами памяти.

3. Попробуйте s=10000000. , это должно быть быстрее. Более серьезно: вы профилируете скорость вызова функции, в то время как Numpy превосходит, когда он может векторизировать операции. Есть ли import оператор также в версии, которая использует встроенный float ?

4. Разве Core 2 Duos не 64-разрядные машины? ark.intel.com/Product.aspx?id=33917

5. вы могли бы использовать python -mtimeit -s "import numpy; s = numpy.float(1)" "(s 8) * s % 2399232" для определения времени. Замените numpy.float на numpy.float32(1) , numpy.float64(1) или 1.0 для других вариантов.

Ответ №1:

Плавающие значения CPython распределяются порциями

Ключевая проблема при сравнении скалярных распределений numpy с float типом заключается в том, что CPython всегда выделяет память для float и int объектов блоками размера N.

Внутри CPython поддерживает связанный список блоков, каждый из которых достаточно велик, чтобы вместить N float объектов. При вызове float(1) CPython проверяет, есть ли свободное место в текущем блоке; если нет, он выделяет новый блок. Как только в текущем блоке появляется свободное место, он просто инициализирует это пространство и возвращает указатель на него.

На моей машине каждый блок может содержать 41 float объект, поэтому при первом float(1) вызове возникают некоторые накладные расходы, но следующие 40 выполняются намного быстрее, поскольку память выделена и готова.

Медленный numpy.float32 против numpy.float64

Похоже, что у numpy есть 2 пути, которые он может использовать при создании скалярного типа: быстрый и медленный. Это зависит от того, имеет ли скалярный тип базовый класс Python, к которому он может обратиться для преобразования аргумента.

По какой-то причине numpy.float32 жестко запрограммирован для выбора более медленного пути (определенного _WORK0 макрокомандой), в то время как numpy.float64 предоставляется шанс выбрать более быстрый путь (определенный _WORK1 макрокомандой). Обратите внимание, что scalartypes.c.src это шаблон, который генерируется scalartypes.c во время сборки.

Вы можете визуализировать это в Cachegrind. Я включил снимки экрана, показывающие, сколько еще вызовов сделано для построения float32 vs float64 :

float64 использует быстрый путь

float64 выбирает быстрый путь

float32 использует медленный путь

float32 выбирает медленный путь

Обновлено — Какой тип выбирает медленный / быстрый путь, может зависеть от того, является ли ОС 32-разрядной или 64-разрядной. В моей тестовой системе, 64-разрядной версии Ubuntu Lucid, float64 тип в 10 раз быстрее, чем float32 .

Комментарии:

1. Прохладный. Я понимаю, как это может замедлить работу float32. Но почему float64 намного медленнее, чем встроенный float? (в моем последнем примере в 10 раз медленнее!) Это только из-за времени, необходимого для выделения памяти? Но в моем цикле память должна быть выделена только для нескольких объектов, а затем может быть повторно использована в последующих итерациях цикла, нет?

2. @max Я обновил свой ответ предположением. Поскольку вы используете 32-разрядную ОС, float64 тип может выбрать медленный путь на вашей платформе. Если у вас есть доступ к valgrind cachegrind, посмотрите, сможете ли вы воспроизвести мои трассировки вызовов на своей платформе.

3. Я пробовал 64-разрядную ОС (см. Мое обновление к вопросу). Оба np типа float в 5 раз медленнее встроенных float . У меня нет valgrind, поможет ли это в анализе этого конкретного снижения производительности?

4. Инструмент cachegrind от @max Valgrind может показать вам множество подробностей о том, как часто вызываются определенные функции и откуда. Одно из его основных применений — поиск узких мест в приложениях.

Ответ №2:

Работа с объектами Python в подобном тяжелом цикле, независимо от того, являются они float , np.float32 всегда медленная. NumPy быстр для операций с векторами и матрицами, потому что все операции выполняются с большими блоками данных частями библиотеки, написанными на C, а не интерпретатором Python. Код, выполняемый в интерпретаторе и / или с использованием объектов Python, всегда выполняется медленно, а использование неродных типов делает его еще медленнее. Этого следовало ожидать.

Если ваше приложение работает медленно и вам нужно его оптимизировать, вам следует попробовать либо преобразовать свой код в векторное решение, которое использует NumPy напрямую и работает быстро, либо вы могли бы использовать такие инструменты, как Cython, для создания быстрой реализации цикла на C.

Комментарии:

1. Хм .. Извините, возможно, я неправильно понял ваш комментарий. Но мой вопрос не в том, float чтобы быть медленным; речь идет о том, чтобы np.float64 быть намного медленнее, чем float . Если вы говорите, что даже float в цикле слишком медленно, я буду рад услышать ваши альтернативные предложения (хотя я не переключаюсь с Python на C).

2. Rosh имеет на это право. np.float64 являются неродными типами и будут иметь дополнительные уровни (медленной) косвенности в интерпретаторе python. Что делает numpy быстрым, так это то, что он избегает интерпретатора python для коллективных операций и может использовать преимущества последовательного доступа к памяти.

3. Ах, спасибо. Думаю, теперь я понял. numpy не подходит для операций с одним числом из-за накладных расходов на работу с не встроенными типами ( numpy отлично подходит для массивов, потому что эти накладные расходы распределяются по многим операциям). Чтобы добиться какого-либо улучшения скорости при операциях с одним числом, мне нужно либо найти способ выполнять их в массиве с помощью numpy , либо использовать что-то вроде CPython. Правильно?

4. Оксюморон @Rosh: «Использование ненативных типов делает это еще медленнее» … на каком основании вы это говорите?

5. @John Machin: Я имел в виду другое значение. Для многих типов объектов Python поддерживает список «освобожденных» объектов, которые «воскресают» при создании нового экземпляра объекта. Это позволяет избежать накладных расходов на выделение памяти и быстрее, чем создание объекта с нуля. Это отличается от создания нескольких ссылок на маленькие целые числа. (Я внедрил бесплатный список для объектов в gmpy, и это увеличило производительность на 20% в реальных приложениях.)

Ответ №3:

Возможно, именно поэтому вам следует использовать Numpy напрямую вместо использования циклов.

 s1 = np.ones(10000000, dtype=np.float)
s2 = np.ones(10000000, dtype=np.float32)
s3 = np.ones(10000000, dtype=np.float64)

np.sum(s1) <-- 17.3 ms
np.sum(s2) <-- 15.8 ms
np.sum(s3) <-- 17.3 ms
  

Комментарии:

1. Я согласен; на моей машине numpy array sum в 70-140 раз быстрее, чем встроенная сумма по встроенному списку (70 в случае float и 140 в случае np.float64 ). Но не всегда возможно использовать массив, как показывает мой обновленный пример. В таком случае несколько сбивает с толку тот факт, что использование np.float64 увеличивает скорость выполнения на огромный постоянный коэффициент (2 в случае простой суммы; 10 в случае моего кода).

2. Ваш обновленный пример отлично работает с numpy, цикл for там не нужен.

3. @tillsten как бы вы переписали его, чтобы он работал без цикла for?

4. IINM на 64-разрядной машине np.float является np.float64 . Это не то же самое, что встроенные float .

Ответ №4:

Ответ довольно прост: выделение памяти может быть частью этого, но самая большая проблема заключается в том, что арифметические операции для скаляров numpy выполняются с использованием «ufuncs», которые должны выполняться быстро для нескольких сотен значений, а не только для 1. При выборе правильной функции для вызова и настройке циклов возникают некоторые накладные расходы. Накладные расходы, которые не являются необходимыми для скаляров.

Было проще просто преобразовать скаляры в 0-d массивы и затем передать в соответствующий numpy ufunc, а затем написать отдельные методы вычисления для каждого из множества различных типов скаляров, которые поддерживает NumPy.

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

Комментарии:

1. Я полагаю, что если разработчик numpy ответит на вопрос, то это в конечном итоге должно стать принятым ответом…

Ответ №5:

Краткие сведения

Если арифметическое выражение содержит как numpy , так и встроенные числа, арифметика Python работает медленнее. Отказ от этого преобразования устраняет почти все отмеченные мной потери производительности.

Подробные сведения

Обратите внимание, что в моем исходном коде:

 s = np.float64(1)
for i in range(10000000):
  s = (s   8) * s % 2399232
  

типы float и numpy.float64 перепутаны в одном выражении. Возможно, Python должен был преобразовать их все в один тип?

 s = np.float64(1)
for i in range(10000000):
  s = (s   np.float64(8)) * s % np.float64(2399232)
  

Если время выполнения не изменилось (а не увеличилось), это наводит на мысль, что именно это Python действительно делал под капотом, объясняя снижение производительности.

На самом деле, время выполнения сократилось в 1,5 раза! Как это возможно? Разве не самое худшее, что мог бы сделать Python, — это эти два преобразования?

Я действительно не знаю. Возможно, Python должен был динамически проверять, что во что нужно преобразовать, что требует времени, и получать информацию о том, какие точные преобразования выполнять, чтобы ускорить это. Возможно, для арифметики используется какой-то совершенно другой механизм (который вообще не включает преобразования), и он оказывается сверхмедленным при несовпадении типов. Чтение numpy исходного кода могло бы помочь, но это за пределами моих навыков.

В любом случае, теперь мы, очевидно, можем ускорить процесс еще больше, выведя преобразования из цикла:

 q = np.float64(8)
r = np.float64(2399232)
for i in range(10000000):
  s = (s   q) * s % r
  

Как и ожидалось, время выполнения существенно сокращается: еще в 2,3 раза.

Честно говоря, теперь нам нужно немного изменить float версию, убрав литеральные константы из цикла. Это приводит к незначительному (10%) замедлению.

С учетом всех этих изменений np.float64 версия кода теперь всего на 30% медленнее эквивалентной float версии; смехотворное 5-кратное снижение производительности в значительной степени исчезло.

Почему мы все еще видим задержку в 30%? numpy.float64 числа занимают тот же объем пространства, что и float , так что это не будет причиной. Возможно, разрешение арифметических операторов занимает больше времени для пользовательских типов. Конечно, это не серьезная проблема.

Комментарии:

1. Я многому научился из всех ответов, но я принимаю этот ответ, поскольку он напрямую касается исходного вопроса. Если кого-то интересует использование numpy.float для скалярной арифметики, они должны знать, что это не проблема, пока все есть numpy.float .

Ответ №6:

Если вам нужна быстрая скалярная арифметика, вам следует обратить внимание на библиотеки, подобные gmpy , а не numpy (как отмечали другие, последняя больше оптимизирована для векторных операций, а не для скалярных).

Комментарии:

1. Я не уверен, что gmpy здесь действительно помогает: в основном речь идет о быстром выполнении арифметики произвольной точности . Во всяком случае, я бы ожидал небольшого замедления при использовании gmpy типов в качестве замены плавающих значений Python и небольших целых чисел Python.

2. В наши дни, я бы согласился с вами, в 2011 году, я не думаю, что знал что-то лучше 🙂

3. Да, извиняюсь; это была ошибка чтения с моей стороны. Вопрос SO был связан с недавним внутренним обсуждением, и я не заметил дат до тех пор, пока не прокомментировал.

Ответ №7:

Я также могу подтвердить результаты. Я попытался посмотреть, как это будет выглядеть, используя все типы numpy, и разница сохраняется. Итак, мои тесты были:

 def testStandard(length=100000):
    s = 1.0
    addend = 8.0
    modulo = 2399232.0
    startTime = datetime.now()
    for i in xrange(length):
        s = (s   addend) * s % modulo
    return datetime.now() - startTime

def testNumpy(length=100000):
    s = np.float64(1.0)
    addend = np.float64(8.0)
    modulo = np.float64(2399232.0)
    startTime = datetime.now()
    for i in xrange(length):
        s = (s   addend) * s % modulo
    return datetime.now() - startTime
  

Итак, на данный момент все типы numpy взаимодействуют друг с другом, но разница в 10 раз сохраняется (2 секунды против 0,2 секунды).

Если бы мне пришлось угадывать, я бы сказал, что есть две возможные причины, по которым типы float по умолчанию намного быстрее. Первая возможность заключается в том, что python выполняет значительную оптимизацию под капотом для работы с определенными числовыми операциями или циклом в целом (например, разворачивание цикла). Вторая возможность заключается в том, что типы numpy включают дополнительный уровень абстракции (т. Е. необходимость чтения с адреса). Чтобы изучить эффекты каждого из них, я выполнил несколько дополнительных проверок.

Одно из отличий может быть результатом того, что python должен предпринимать дополнительные шаги для разрешения типов float64. В отличие от скомпилированных языков, которые генерируют эффективные таблицы, python 2.6 (и, возможно, 3) требует значительных затрат для решения задач, которые вы обычно считаете бесплатными. Даже простой X. разрешение должно разрешать оператор dot при КАЖДОМ его вызове. (Вот почему, если у вас есть цикл, который вызывает instance.function(), вам лучше иметь переменную «function = instance.function», объявленную вне цикла).

Насколько я понимаю, когда вы используете стандартные операторы python, они довольно похожи на использование операторов из «import operator». Если вы замените , * и% на add, mul и mod, вы увидите статическое снижение производительности примерно на 0,5 секунды по сравнению со стандартными операторами (в обоих случаях). Это означает, что при переносе операторов стандартные операции python с плавающей точкой становятся в 3 раза медленнее. Если вы сделаете еще одну, используя operator.add, и эти варианты увеличат время примерно на 0,7 секунды (более 1 млн попыток, начиная с 2 сек и 0,2 сек соответственно). Это граничит с 5-кратной медленностью. Итак, в принципе, если каждая из этих проблем возникает дважды, вы, по сути, в 10 раз медленнее.

Итак, давайте на мгновение предположим, что мы интерпретатор python. Случай 1, мы выполняем операцию над собственными типами, скажем, a b. Под капотом мы можем проверить типы a и b и отправить наше дополнение к оптимизированному коду python. В случае 2 мы имеем операцию двух других типов (также a b). Под капотом мы проверяем, являются ли они собственными типами (это не так). Мы переходим к случаю ‘else’. Случай else отправляет нас к чему-то вроде a.add(b). a.add затем может выполнить отправку в оптимизированный код numpy. Итак, на данный момент у нас возникли дополнительные накладные расходы на дополнительную ветвь, одно свойство ‘.’ get slots и вызов функции. И мы приступили только к операции сложения. Затем мы должны использовать результат для создания нового float64 (или изменить существующий float64). Между тем, собственный код python, вероятно, обманывает, обрабатывая свои типы специально, чтобы избежать такого рода накладных расходов.

Основываясь на приведенном выше анализе дороговизны вызовов функций python и накладных расходов, для numpy было бы довольно легко понести 9-кратный штраф, просто получая доступ к своим математическим функциям c и обратно. Я вполне могу представить, что этот процесс занимает во много раз больше времени, чем вызов простой математической операции. Для каждой операции библиотеке numpy придется пробираться через слои python, чтобы добраться до ее реализации на C.

Итак, на мой взгляд, причина этого, вероятно, заключается в этом эффекте:

 length = 10000000
class A():
    X = 10
startTime = datetime.now()
for i in xrange(length):
    x = A.X
print "Long Way", datetime.now() - startTime
startTime = datetime.now()
y = A.X
for i in xrange(length):
    x = y
print "Short Way", datetime.now() - startTime
  

Этот простой случай показывает разницу в 0,2 секунды против 0,14 секунды (короткий путь быстрее, очевидно). Я думаю, то, что вы видите, в основном, это просто совокупность этих проблем.

Чтобы избежать этого, я могу придумать пару возможных решений, которые в основном повторяют то, что было сказано. Первое решение — попытаться максимально сохранить ваши оценки внутри NumPy, как сказал Селинап. Большое количество потерь, вероятно, связано с интерфейсом. Я бы изучил способы отправки вашей работы в numpy или какую-либо другую числовую библиотеку, оптимизированную на C (упоминался gmpy). Цель должна состоять в том, чтобы одновременно ввести как можно больше данных в C, а затем получить результат (ы) обратно. Вы хотите выполнять большие задания, а не множество мелких.

Вторым решением, конечно, было бы выполнять больше промежуточных и небольших операций на python, если вы можете. Очевидно, что использование собственных объектов будет быстрее. Они будут первыми параметрами во всех операторах перехода и всегда будут иметь кратчайший путь к C-коду. Если у вас нет особой потребности в вычислении с фиксированной точностью или других проблемах с операторами по умолчанию, я не понимаю, почему бы не использовать прямые функции python для многих вещей.

Комментарии:

1. Это очень полезно. Я использую numpy, потому что мне нужны его случайные функции; они намного быстрее, чем функции Python (особенно когда я запрашиваю массив из множества случайных чисел). Но, к сожалению, им нельзя сказать, чтобы они возвращали встроенные float . Итак, я обнаружил, что дешевле преобразовать np.float64 во встроенный float , прежде чем выполнять арифметику…

Ответ №8:

Действительно странно…Я подтверждаю результаты в Ubuntu 11.04 32bit, python 2.7.1, numpy 1.5.1 (официальные пакеты):

 import numpy as np
def testfloat():
    s = 0
    for i in range(10000000):  
        s = float(1)
def testfloat32():
    s = 0
    for i in range(10000000):  
        s = np.float32(1)
def testfloat64():
    s = 0
    for i in range(10000000):  
        s = np.float64(1)

%time testfloat()
CPU times: user 4.66 s, sys: 0.06 s, total: 4.73 s
Wall time: 4.74 s

%time testfloat64()
CPU times: user 11.43 s, sys: 0.07 s, total: 11.50 s
Wall time: 11.57 s


%time testfloat32()
CPU times: user 47.99 s, sys: 0.09 s, total: 48.08 s
Wall time: 48.23 s
  

Я не понимаю, почему float32 должен быть в 5 раз медленнее, чем float64.

Комментарии:

1. Похоже, вы получаете те же результаты, что и я изначально. Но с моим обновленным кодом, float64 и float32 почти одинаковы по производительности. Я бы действительно хотел сосредоточиться на float64 vs float . В конце концов, кому интересно использовать float32, если он медленный.