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

Производительность NumPy: uint8 против float и умножения или деления?

Я только что заметил, что время выполнения script моих почти половин, только изменяя умножение на деление.

Чтобы исследовать это, я написал небольшой пример:

import numpy as np                                                                                                                                                                                
import timeit

# uint8 array
arr1 = np.random.randint(0, high=256, size=(100, 100), dtype=np.uint8)

# float32 array
arr2 = np.random.rand(100, 100).astype(np.float32)
arr2 *= 255.0


def arrmult(a):
    """ 
    mult, read-write iterator
    """
    b = a.copy()
    for item in np.nditer(b, op_flags=["readwrite"]):
        item[...] = (item + 5) * 0.5

def arrmult2(a):
    """ 
    mult, index iterator
    """
    b = a.copy()
    for i, j in np.ndindex(b.shape):
        b[i, j] = (b[i, j] + 5) * 0.5

def arrmult3(a):
    """
    mult, vectorized
    """
    b = a.copy()
    b = (b + 5) * 0.5

def arrdiv(a):
    """ 
    div, read-write iterator 
    """
    b = a.copy()
    for item in np.nditer(b, op_flags=["readwrite"]):
        item[...] = (item + 5) / 2

def arrdiv2(a):
    """ 
    div, index iterator
    """
    b = a.copy()
    for i, j in np.ndindex(b.shape):
           b[i, j] = (b[i, j] + 5)  / 2                                                                                 

def arrdiv3(a):                                                                                                     
    """                                                                                                             
    div, vectorized                                                                                                 
    """                                                                                                             
    b = a.copy()                                                                                                    
    b = (b + 5) / 2                                                                                               




def print_time(name, t):                                                                                            
    print("{: <10}: {: >6.4f}s".format(name, t))                                                                    

timeit_iterations = 100                                                                                             

print("uint8 arrays")                                                                                               
print_time("arrmult", timeit.timeit("arrmult(arr1)", "from __main__ import arrmult, arr1", number=timeit_iterations))
print_time("arrmult2", timeit.timeit("arrmult2(arr1)", "from __main__ import arrmult2, arr1", number=timeit_iterations))
print_time("arrmult3", timeit.timeit("arrmult3(arr1)", "from __main__ import arrmult3, arr1", number=timeit_iterations))
print_time("arrdiv", timeit.timeit("arrdiv(arr1)", "from __main__ import arrdiv, arr1", number=timeit_iterations))  
print_time("arrdiv2", timeit.timeit("arrdiv2(arr1)", "from __main__ import arrdiv2, arr1", number=timeit_iterations))
print_time("arrdiv3", timeit.timeit("arrdiv3(arr1)", "from __main__ import arrdiv3, arr1", number=timeit_iterations))

print("\nfloat32 arrays")                                                                                           
print_time("arrmult", timeit.timeit("arrmult(arr2)", "from __main__ import arrmult, arr2", number=timeit_iterations))
print_time("arrmult2", timeit.timeit("arrmult2(arr2)", "from __main__ import arrmult2, arr2", number=timeit_iterations))
print_time("arrmult3", timeit.timeit("arrmult3(arr2)", "from __main__ import arrmult3, arr2", number=timeit_iterations))
print_time("arrdiv", timeit.timeit("arrdiv(arr2)", "from __main__ import arrdiv, arr2", number=timeit_iterations))  
print_time("arrdiv2", timeit.timeit("arrdiv2(arr2)", "from __main__ import arrdiv2, arr2", number=timeit_iterations))
print_time("arrdiv3", timeit.timeit("arrdiv3(arr2)", "from __main__ import arrdiv3, arr2", number=timeit_iterations))

Отпечатывает следующие тайминги:

uint8 arrays
arrmult   : 2.2004s
arrmult2  : 3.0589s
arrmult3  : 0.0014s
arrdiv    : 1.1540s
arrdiv2   : 2.0780s
arrdiv3   : 0.0027s

float32 arrays
arrmult   : 1.2708s
arrmult2  : 2.4120s
arrmult3  : 0.0009s
arrdiv    : 1.5771s
arrdiv2   : 2.3843s
arrdiv3   : 0.0009s

Я всегда думал, что умножение вычисляется дешевле, чем деление. Однако для uint8 деление, по-видимому, почти в два раза эффективнее. Связано ли это с тем фактом, что * 0.5 должен вычислить умножение в поплавке и затем вернуть результат обратно к целому числу?

По крайней мере, для поплавков умножения кажутся быстрее, чем деления. Это вообще правда?

Почему умножение в uint8 более экспансивное, чем в float32? Я думал, что 8-разрядное целое число без знака должно быть намного быстрее для вычисления, чем 32-битные поплавки?!

Может кто-нибудь "демистифицировать" это?

EDIT: чтобы иметь больше данных, я включил векторизованные функции (например, предлагаемые) и добавил итераторы индекса. Эти векторизованные функции намного быстрее, что на самом деле не сравнимо. Однако, если timeit_iterations задано намного выше для векторизованных функций, оказывается, что умножение выполняется быстрее для обоих, uint8 и float32. Думаю, это смущает еще больше?!

Возможно, умножение на самом деле всегда быстрее, чем деление, но основные утечки производительности в for-loops - это не арифметическая операция, а сам цикл. Хотя это не объясняет, почему петли ведут себя по-разному для разных операций.

EDIT2. Как уже говорилось, @jotasi, мы ищем полное объяснение division vs. multiplication и int (или uint8) по сравнению с float ( или float32). Кроме того, было бы интересно пояснить различные тенденции векторизованных подходов и итераторов, как в векторизованном случае, деление кажется более медленным, тогда как в итераторе оно быстрее.

4b9b3361

Ответ 1

Проблема заключается в вашем предположении, что вы измеряете время, необходимое для деления или умножения, что неверно. Вы измеряете накладные расходы, необходимые для деления или умножения.

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

Проблема заключается в том, что простой int не является простым в python: это реальный объект, который должен быть зарегистрирован в сборщике мусора, он растет по размеру с его значением - за все, что вам нужно заплатить: например, для 8-битной целочисленной 24-байтовой памяти необходимы! аналогично для python-floats.

С другой стороны, массив numpy состоит из простых целых чисел /float c-style без накладных расходов, вы сохраняете много памяти, но платите за нее во время доступа к элементу numpy-массива. a[i] означает: должно быть построено целое число python, зарегистрированное в сборщике мусора и только, чем оно может быть использовано - накладные расходы много.

Рассмотрим этот код:

li1=[x%256 for x in xrange(10**4)]
arr1=np.array(li1, np.uint8)

def arrmult(a):    
    for i in xrange(len(a)):
        a[i]*=5;

arrmult(li1) на 25 быстрее, чем arrmult(arr1), потому что целые числа в списке уже являются python-int и не обязательно должны быть созданы! Львиная доля времени вычисления необходима для создания объектов - все остальное можно почти пренебречь.


Давайте взглянем на ваш код, сначала умножим:

def arrmult2(a):
    ...
    b[i, j] = (b[i, j] + 5) * 0.5

В случае uint8 должно произойти следующее (для простоты я пренебрегаю +5):

  • должен быть создан python-int
  • он должен быть отправлен в float (создание python-float), чтобы иметь возможность делать float-умножение
  • и отбрасывается на python-int или/и uint8

Для float32 меньше работы (умножение не очень дорого): 1. Создан python-float 2. отбрасывается назад float32.

Итак, float-версия должна быть быстрее, и она есть.


Теперь посмотрим на разделение:

def arrdiv2(a):
    ...
    b[i, j] = (b[i, j] + 5)  / 2 

Ловушка здесь: все операции являются целыми операциями. Таким образом, по сравнению с умножением нет необходимости бросать в python-float, поэтому у нас меньше накладных расходов, чем в случае умножения. Раздел "быстрее" для unint8, чем умножение в вашем случае.

Однако деление и умножение одинаково быстрые/медленные для float32, потому что в этом случае почти ничего не изменилось - нам все равно нужно создать python-float.


Теперь векторизованные версии: они работают с c-style "raw" float32s/uint8s без преобразования (и его стоимости!) на соответствующие объекты python под капотом. Чтобы получить значимые результаты, вы должны увеличить количество итераций (прямо сейчас время работы слишком мало, чтобы сказать что-то с уверенностью).

  • деление и умножение для float32 могут иметь одинаковое время работы, потому что я ожидал бы, что numpy заменит деление на 2 на умножение на 0.5 (но чтобы убедиться, что нужно смотреть в код).

  • умножение для uint8 должно быть медленнее, потому что каждое uint8-целое должно быть отброшено до float до умножения с 0.5 и затем отбрасываться обратно на uint8 после.

  • для случая uint8, numpy не может заменить деление на 2 путем умножения на 0,5, потому что это целочисленное деление. Целочисленное деление медленнее, чем float-multipication для многих архитектур - это самая медленная векторная операция.


PS: Я бы не стал слишком много говорить о размножении затрат и делении - слишком много других вещей, которые могут иметь больший удар по производительности. Например, создавая ненужные временные объекты или массив numpy большой и не вписывается в кеш, чем доступ к памяти будет шеей бутылки - вы не увидите разницы между умножением и делением вообще.

Ответ 2

В этом ответе рассматриваются только векторизованные операции, поскольку причиной медленных действий других операций является ead.

Множество "оптимизаций" основано на старом оборудовании. Предположения, которые означают, что оптимизация, применяемая на более старых аппаратных средствах, не устарела на более новом оборудовании.

Трубопроводы и деление

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

Однако в блоке обработки с плавающей запятой (FPU) [обычном для большинства современных процессоров] есть выделенные блоки, расположенные в "конвейере" для инструкции деления. Как только блок сделан, этот блок не нужен для остальной части операции. Если у вас есть несколько операций деления, вы можете получить эти единицы, чтобы ничего не начать с операции следующего деления. Таким образом, хотя каждая операция выполняется медленно, FPU может фактически обеспечить высокую пропускную способность операций деления. Трубопровод - это не то же самое, что и векторизация, но результаты в основном одинаковы - более высокая пропускная способность, когда у вас есть много операций.

Подумайте о конвейерном трафике. Сравните три полосы движения, движущиеся со скоростью 30 миль/ч по сравнению с одной полосой движения, движущейся со скоростью 90 миль в час. Более медленный трафик определенно медленнее по отдельности, но трехполосная дорога по-прежнему имеет одинаковую пропускную способность.

Ответ 3

Это потому, что вы умножаете int на float и сохраняете результат как int. Попробуйте выполнить тесты arr_mult и arr_div с разными значениями integer или float для умножения/деления. В частности, сравните умножение на '2' и умножьте на '2.'

Ответ 4

Это первая операция, которая обычно занимает больше времени до "разогрева" (например, выделенной памяти, кэширования).

См. тот же эффект, используя обратный порядок деления и умножения:

>>> print_time("arrdiv", timeit.timeit("arrdiv(arr2)", "from __main__ import arrdiv, arr2", number=timeit_iterations))
>>> print_time("arrmult", timeit.timeit("arrmult(arr2)", "from __main__ import arrmult, arr2", number=timeit_iterations))

arrdiv:  3.2630s
arrmult:  2.5873s