0

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

11

Описание проблемы

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

import numpy as np
s = 0
for i in range(10000000):
    s += np.float64(1)  # замените на np.float32 и встроенный float

Результаты замеров времени:

  • встроенный float: 4.9 с
  • float64: 10.5 с
  • float32: 45.0 с

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

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

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

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

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

EDIT #1:

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

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

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

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

from datetime import datetime
import numpy as np

START_TIME = datetime.now()

# одна из следующих строк раскомментирована перед выполнением
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0

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

print(s)
print('Время выполнения:', datetime.now() - START_TIME)

Времена выполнения:

  • float64: 34.56с
  • float32: 35.11с
  • float: 3.53с

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

from datetime import datetime
import numpy as 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('Время выполнения:', datetime.now() - START_TIME)

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

Мое оборудование:

  • Intel Core 2 Duo T9300 (2.5GHz)
  • WinXP Professional (32-bit)
  • ActiveState Python 3.1.3.5
  • Numpy 1.5.1

EDIT #2:

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

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

EDIT #3:

Я повторил код под Windows 7 x64 (Intel Core i7 930 @ 3.8GHz).

Снова код:

from datetime import datetime
import numpy as np

START_TIME = datetime.now()

# одна из следующих строк раскомментирована перед выполнением
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0

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

print(s)
print('Время выполнения:', datetime.now() - START_TIME)

Результаты:

  • float64: 16.1с
  • float32: 16.1с
  • float: 3.2с

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

END OF EDITS

5 ответ(ов)

0

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

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

0

Скорее всего, именно по этой причине стоит использовать 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 мс
np.sum(s2)  # <-- 15.8 мс
np.sum(s3)  # <-- 17.3 мс

Как видно из результатов замеров, использование Numpy для операций с массивами значительно ускоряет выполнение задач по сравнению с обычными циклами, так как Numpy оптимизирован для работы с большими объемами данных и позволяет использовать векторизированные операции.

0

Ответ довольно прост: выделение памяти может играть свою роль, но основная проблема заключается в том, что арифметические операции для скалярных значений NumPy выполняются с использованием "ufunc", которые предназначены для быстрого выполнения с несколькими сотнями значений, а не только с одним. Есть определенные накладные расходы на выбор правильной функции и настройку циклов — эти накладные расходы неоправданы для скалярных значений.

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

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

0

Резюме

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

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

0

Я тоже могу подтвердить результаты. Я попытался посмотреть, как все это будет выглядеть с использованием всех типов 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, они довольно похожи на использование тех, что из "import operator". Если вы замените 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'. Случай else отправляет нас к чему-то вроде a.add(b). Метод a.add может затем вызвать оптимизированный код numpy. Таким образом, на этом этапе мы имеем дополнительные накладные расходы от дополнительной ветки, одного обращения к свойству 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 "Долгая дорога", datetime.now() - startTime
startTime = datetime.now()
y = A.X
for i in xrange(length):
    x = y
print "Короткая дорога", datetime.now() - startTime

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

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

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

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь