Подтвердить что ты не робот

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

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

import numpy as np
s = 0
for i in range(10000000):
    s += np.float64(1) # replace with np.float32 and built-in float
  • встроенный поплавок: 4.9 с
  • float64: 10.5 с
  • float32: 45.0 с

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

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

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

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

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

РЕД. # 1:

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

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

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

Я обновил свой код, чтобы сделать его более ясным, где проблема. С новым кодом, похоже, я вижу десятикратное увеличение производительности при использовании типов данных 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.56s
  • float32: 35.11s
  • float: 3.53s

Просто, черт возьми, я также пробовал:

из datetime import datetime import 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 с; это фактически в 3 раза быстрее, чтобы преобразовать float64 в float и обратно, чем использовать его как есть. Тем не менее, преобразование имеет свои потери, поэтому в целом он более чем в 3 раза медленнее по сравнению с чистым питоном 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.8GHz).

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

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.1s
  • float32: 16.1s
  • float: 3.2s

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

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

4b9b3361

Ответ 1

Резюме

Если арифметическое выражение содержит как 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, поэтому это не будет причиной. Возможно, разрешение арифметических операторов занимает больше времени для пользовательских типов. Безусловно, это не большая проблема.

Ответ 2

Поплавки 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 takes the fast path

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

float32 takes the slow path

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

Ответ 3

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

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

Ответ 4

Возможно, именно поэтому вы должны использовать 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

Ответ 5

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

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

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

Ответ 6

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

Ответ 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.a должно разрешить оператор точки КАЖДОЕ время его вызова. (Вот почему, если у вас есть цикл, который вызывает instance.function(), вам лучше иметь переменную "function = instance.function", объявленную вне цикла).

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

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

Основываясь на вышеозначенном изложении затрат на вызовы функций python и накладных расходах, было бы довольно легко для numpy взять 9-кратное наказание, просто получая и от своих математических функций. Я могу полностью представить, что этот процесс занимает много времени дольше, чем простой вызов математической операции. Для каждой операции библиотеке 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-коду. Если у вас нет конкретной потребности в вычислении фиксированной точности или других проблемах с операторами по умолчанию, я не понимаю, почему нельзя использовать функции прямого питона для многих вещей.

Ответ 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.