numpy float: в 10 раз медленнее, чем встроенные в арифметические операции? - PullRequest
47 голосов
/ 11 мая 2011

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

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 намного медленнее, чем поплавок Python, а numpy.float32 даже медленнее (даже если я на 32-битной машине).

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

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

EDIT # 1:

numpy.float64 в 10 раз медленнее, чем 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.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,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 с
  • с плавающей точкой: 3,2 с

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

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

Ответы [ 8 ]

44 голосов
/ 19 мая 2011

Плавающие CPython выделяются кусками

Ключевая проблема сравнения числовых скалярных распределений с типом 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 против float64:

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

float64 takes the fast path

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

float32 takes the slow path

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

22 голосов
/ 11 мая 2011

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

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

10 голосов
/ 11 мая 2011

Возможно, поэтому вам следует использовать 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
9 голосов
/ 12 ноября 2013

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

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

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

7 голосов
/ 12 апреля 2012

Резюме

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

1 голос
/ 22 мая 2011

Я также могу подтвердить результаты. Я попытался увидеть, как это будет выглядеть, используя все типы 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 с).

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

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

Насколько я понимаю, когда вы используете стандартные операторы python, они довольно похожи на те, которые используются в операторе импорта. Если вы замените 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). Под капотом мы проверяем, являются ли они нативными типами (они не являются). Мы переходим к другому делу. В другом случае мы получаем что-то вроде a. add (b). a. add может отправлять оптимизированный код numpy. Таким образом, в этот момент у нас были дополнительные издержки на дополнительную ветку, одну '.' получить свойство slots и вызов функции. И мы только вошли в операцию сложения. Затем мы должны использовать результат, чтобы создать новый 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. Если у вас нет особой необходимости в вычислениях с фиксированной точностью или других проблем с операторами по умолчанию, я не понимаю, почему нельзя было бы использовать прямые функции python для многих вещей.

1 голос
/ 11 мая 2011

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

0 голосов
/ 11 мая 2011

Действительно странно ... Я подтверждаю результаты в 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.

...