Почему здесь происходит циклическое индексирование битов?

Несколько лет назад кто-то опубликовал в Active State Recipes для целей сравнения, три функции Python / NumPy; каждый из них принимал одни и те же аргументы и возвращал один и тот же результат - матрицу расстояний .

Два из них были взяты из опубликованных источников; они оба - или они кажутся мне - идиоматическим кодом. Повторяющиеся вычисления, необходимые для создания матрицы расстояний, основаны на элегантном синтаксисе индекса numpy. Вот один из них:

from numpy.matlib import repmat, repeat

def calcDistanceMatrixFastEuclidean(points):
  numPoints = len(points)
  distMat = sqrt(sum((repmat(points, numPoints, 1) - 
             repeat(points, numPoints, axis=0))**2, axis=1))
  return distMat.reshape((numPoints,numPoints))

Третий создал матрицу расстояний, используя один цикл (который, очевидно, является большим количеством циклов, учитывая, что матрица расстояний всего 1000 2D точек имеет миллион записей). На первый взгляд эта функция выглядела как код, который я использовал для написания, когда изучал NumPy, и я писал бы код NumPy, сначала написав код Python, а затем переводя его построчно.

Через несколько месяцев после публикации Active State результаты тестов производительности, сравнивающие три, были опубликованы и обсуждены в потоке в списке рассылки NumPy.

Функция с циклом фактически значительно превзошла два других:

from numpy import mat, zeros, newaxis

def calcDistanceMatrixFastEuclidean2(nDimPoints):
  nDimPoints = array(nDimPoints)
  n,m = nDimPoints.shape
  delta = zeros((n,n),'d')
  for d in xrange(m):
    data = nDimPoints[:,d]
    delta += (data - data[:,newaxis])**2
  return sqrt(delta)

Один из участников потока (Кейр Мирле) предложил причину, по которой это могло бы быть правдой:

Причина, по которой я подозреваю, что это будет быстрее, очевидно, много циклов, учитывая, что матрица расстояний, состоящая всего из 1000 2D точек, имеет миллион записей). На первый взгляд эта функция выглядела как код, который я использовал для написания, когда изучал NumPy, и я писал бы код NumPy, сначала написав код Python, а затем переводя его построчно.

Через несколько месяцев после публикации Active State результаты тестов производительности, сравнивающие три, были опубликованы и обсуждены в потоке в списке рассылки NumPy.

Функция с циклом фактически значительно превзошла два других:

from numpy import mat, zeros, newaxis

def calcDistanceMatrixFastEuclidean2(nDimPoints):
  nDimPoints = array(nDimPoints)
  n,m = nDimPoints.shape
  delta = zeros((n,n),'d')
  for d in xrange(m):
    data = nDimPoints[:,d]
    delta += (data - data[:,newaxis])**2
  return sqrt(delta)

Один из участников потока (Кейр Мирле) предложил причину, по которой это могло бы быть правдой:

Причина, по которой я подозреваю, что это будет быстрее, очевидно, много циклов, учитывая, что матрица расстояний, состоящая всего из 1000 2D точек, имеет миллион записей). На первый взгляд эта функция выглядела как код, который я использовал для написания, когда изучал NumPy, и я писал бы код NumPy, сначала написав код Python, а затем переводя его построчно.

Через несколько месяцев после публикации Active State результаты тестов производительности, сравнивающие три, были опубликованы и обсуждены в потоке в списке рассылки NumPy.

Функция с циклом фактически значительно превзошла два других:

from numpy import mat, zeros, newaxis

def calcDistanceMatrixFastEuclidean2(nDimPoints):
  nDimPoints = array(nDimPoints)
  n,m = nDimPoints.shape
  delta = zeros((n,n),'d')
  for d in xrange(m):
    data = nDimPoints[:,d]
    delta += (data - data[:,newaxis])**2
  return sqrt(delta)

Один из участников потока (Кейр Мирле) предложил причину, по которой это могло бы быть правдой:

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

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

Любые другие мысли о том, как учитывать эти результаты?

В частности, есть ли полезное правило - относительно того, когда выполнять цикл, а когда индексировать - которое можно извлечь из этого примера в качестве руководства при написании кода Numpy?

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

10
задан AndyG 20 March 2017 в 17:50
поделиться

2 ответа

TL; DR Второй приведенный выше код выполняет цикл только по количеству измерений точек (3 раза в цикле for для трехмерных точек), поэтому цикла здесь не так много. Реальное ускорение второго кода выше заключается в том, что он лучше использует возможности Numpy, чтобы избежать создания дополнительных матриц при обнаружении различий между точками. Это снижает используемую память и вычислительные затраты.

Подробное объяснение Я думаю, что функция calcDistanceMatrixFastEuclidean2 , возможно, обманывает вас своим циклом. Это только цикл по количеству размеров точек. Для 1D точек цикл выполняется только один раз, для 2D - дважды и для 3D - трижды. На самом деле это не так уж много зацикливания.

Давайте немного проанализируем код, чтобы понять, почему один быстрее другого. calcDistanceMatrixFastEuclidean Я назову fast1 , а calcDistanceMatrixFastEuclidean2 будет fast2 .

fast1 основан на способе работы Matlab, о чем свидетельствует функция repmap . Функция repmap в этом случае создает массив, состоящий только из исходных данных, повторяемых снова и снова. Однако если вы посмотрите на код функции, он очень неэффективен. Для этого он использует множество функций Numpy (3 reshape s и 2 repeat s). Функция repeat также используется для создания массива, содержащего исходные данные, причем каждый элемент данных повторяется много раз.Если наши входные данные - [1,2,3] , то мы вычитаем [1,2,3,1,2,3,1,2,3] из ] [1,1,1,2,2,2,3,3,3] . Numpy пришлось создать множество дополнительных матриц между запуском кода C Numpy, чего можно было бы избежать.

fast2 использует больше тяжелой работы Numpy, не создавая столько матриц между вызовами Numpy. fast2 проходит через каждое измерение точек, выполняет вычитание и сохраняет промежуточную сумму квадратов разностей между каждым измерением. Только в конце получается квадратный корень. Пока что это может показаться не таким эффективным, как fast1 , но fast2 избегает выполнения работы repmat с помощью индексации Numpy. Давайте посмотрим на одномерный случай для простоты. fast2 создает одномерный массив данных и вычитает его из двухмерного (N x 1) массива данных. Это создает матрицу разностей между каждой точкой и всеми другими точками без использования repmat и repeat и, таким образом, позволяет избежать создания большого количества дополнительных массивов. В этом, на мой взгляд, и заключается реальная разница в скорости. fast1 создает много дополнительных промежутков между матрицами (и они создаются с большими вычислительными затратами), чтобы найти различия между точками, в то время как fast2 лучше использует возможности Numpy, чтобы избежать этого.

Кстати, вот немного более быстрая версия fast2 :

def calcDistanceMatrixFastEuclidean3(nDimPoints):
  nDimPoints = array(nDimPoints)
  n,m = nDimPoints.shape
  data = nDimPoints[:,0]
  delta = (data - data[:,newaxis])**2
  for d in xrange(1,m):
    data = nDimPoints[:,d]
    delta += (data - data[:,newaxis])**2
  return sqrt(delta)

Разница в том, что мы больше не создаем дельту как матрицу нулей.

11
ответ дан 4 December 2019 в 00:22
поделиться

dis для развлечения:

dis.dis(calcDistanceMatrixFastEuclidean)

  2           0 LOAD_GLOBAL              0 (len)
              3 LOAD_FAST                0 (points)
              6 CALL_FUNCTION            1
              9 STORE_FAST               1 (numPoints)

  3          12 LOAD_GLOBAL              1 (sqrt)
             15 LOAD_GLOBAL              2 (sum)
             18 LOAD_GLOBAL              3 (repmat)
             21 LOAD_FAST                0 (points)
             24 LOAD_FAST                1 (numPoints)
             27 LOAD_CONST               1 (1)
             30 CALL_FUNCTION            3

  4          33 LOAD_GLOBAL              4 (repeat)
             36 LOAD_FAST                0 (points)
             39 LOAD_FAST                1 (numPoints)
             42 LOAD_CONST               2 ('axis')
             45 LOAD_CONST               3 (0)
             48 CALL_FUNCTION          258
             51 BINARY_SUBTRACT
             52 LOAD_CONST               4 (2)
             55 BINARY_POWER
             56 LOAD_CONST               2 ('axis')
             59 LOAD_CONST               1 (1)
             62 CALL_FUNCTION          257
             65 CALL_FUNCTION            1
             68 STORE_FAST               2 (distMat)

  5          71 LOAD_FAST                2 (distMat)
             74 LOAD_ATTR                5 (reshape)
             77 LOAD_FAST                1 (numPoints)
             80 LOAD_FAST                1 (numPoints)
             83 BUILD_TUPLE              2
             86 CALL_FUNCTION            1
             89 RETURN_VALUE

dis.dis(calcDistanceMatrixFastEuclidean2)

  2           0 LOAD_GLOBAL              0 (array)
              3 LOAD_FAST                0 (nDimPoints)
              6 CALL_FUNCTION            1
              9 STORE_FAST               0 (nDimPoints)

  3          12 LOAD_FAST                0 (nDimPoints)
             15 LOAD_ATTR                1 (shape)
             18 UNPACK_SEQUENCE          2
             21 STORE_FAST               1 (n)
             24 STORE_FAST               2 (m)

  4          27 LOAD_GLOBAL              2 (zeros)
             30 LOAD_FAST                1 (n)
             33 LOAD_FAST                1 (n)
             36 BUILD_TUPLE              2
             39 LOAD_CONST               1 ('d')
             42 CALL_FUNCTION            2
             45 STORE_FAST               3 (delta)

  5          48 SETUP_LOOP              76 (to 127)
             51 LOAD_GLOBAL              3 (xrange)
             54 LOAD_FAST                2 (m)
             57 CALL_FUNCTION            1
             60 GET_ITER
        >>   61 FOR_ITER                62 (to 126)
             64 STORE_FAST               4 (d)

  6          67 LOAD_FAST                0 (nDimPoints)
             70 LOAD_CONST               0 (None)
             73 LOAD_CONST               0 (None)
             76 BUILD_SLICE              2
             79 LOAD_FAST                4 (d)
             82 BUILD_TUPLE              2
             85 BINARY_SUBSCR
             86 STORE_FAST               5 (data)

  7          89 LOAD_FAST                3 (delta)
             92 LOAD_FAST                5 (data)
             95 LOAD_FAST                5 (data)
             98 LOAD_CONST               0 (None)
            101 LOAD_CONST               0 (None)
            104 BUILD_SLICE              2
            107 LOAD_GLOBAL              4 (newaxis)
            110 BUILD_TUPLE              2
            113 BINARY_SUBSCR
            114 BINARY_SUBTRACT
            115 LOAD_CONST               2 (2)
            118 BINARY_POWER
            119 INPLACE_ADD
            120 STORE_FAST               3 (delta)
            123 JUMP_ABSOLUTE           61
        >>  126 POP_BLOCK

  8     >>  127 LOAD_GLOBAL              5 (sqrt)
            130 LOAD_FAST                3 (delta)
            133 CALL_FUNCTION            1
            136 RETURN_VALUE

Я не эксперт по dis, но кажется, что вам нужно посмотреть на функции, которые вызывает первая, чтобы понять, почему они выполняются долго. В Python также есть инструмент профилировщика производительности, cProfile.

1
ответ дан 4 December 2019 в 00:22
поделиться
Другие вопросы по тегам:

Похожие вопросы: