Почему вычитание NumPy медленнее на одной большой матрице $ M $, чем при делении $ M $ на меньшие матрицы и последующем вычитании? - PullRequest
1 голос
/ 12 июля 2019

Я работаю над некоторым кодом, в котором у меня есть несколько матриц, и я хочу вычесть вектор $ v $ из каждой строки каждой матрицы (а затем сделать другие вещи с результатом). Поскольку я использую NumPy и хочу максимально «векторизовать», я решил ускорить время выполнения, сохранив все матрицы в виде одной большой («объединенной») матрицы и вычтя из нее $ v $. Проблема в том, что мой код работает медленнее после этой предполагаемой оптимизации. Фактически, в некоторых сценариях разбиение матриц и вычитание по отдельности происходит значительно быстрее (см. Пример кода ниже).

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

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

Я сделал следующее MWE, чтобы продемонстрировать проблему.

Импорт NumPy и таймера:

import numpy as np
from timeit import default_timer as timer

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

n = 100  # width of matrix
m = 500  # height of matrix
k = 100  # number of matrices
M = 100  # upper bound on entries
reps = 100  # repetitions for timings

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

list_of_matrices = [np.random.randint(0, M+1, size=(m,n)) for _ in range(k)]
large_matrix = np.row_stack(list_of_matrices)
vector = np.random.randint(0, M+1, size=n)

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

def list_compute(list_of_matrices, vector):
    for j in range(k):
        np.subtract(list_of_matrices[j], vector)

def array_compute(bidlists, vector):
    np.subtract(large_matrix, vector_matrix, out=pre_allocated)

pre_allocated = np.empty(shape=large_matrix.shape)
vector_matrix = np.broadcast_to(vector, shape=large_matrix.shape)
def faster_array_compute(large_matrix, vector_matrix, out_matrix):
    np.subtract(large_matrix, vector_matrix, out=out_matrix)

Я тестирую три функции, запустив

start = timer()
for _ in range(reps):
    list_compute(list_of_matrices, vector)
print timer() - start

start = timer()
for _ in range(reps):
    array_compute(large_matrix, vector)
print timer() - start

start = timer()
for _ in range(reps):
    faster_array_compute(large_matrix, vector_matrix, pre_allocated)
print timer() - start

Для вышеуказанных параметров я получаю время

0.539432048798
1.12959504128
1.10976290703

Наивно, я бы ожидал, что подход с большой матрицей будет более быстрым или, по крайней мере, конкурентоспособным по сравнению с подходом с несколькими матрицами. Я надеюсь, что кто-то может дать мне некоторое представление о том, почему это не так и как я могу ускорить мой код!

1 Ответ

2 голосов
/ 12 июля 2019

Тип переменной pre_allocated - float8.Входные матрицы являются целыми.У вас есть неявное преобразование.Попробуйте изменить предварительное распределение следующим образом:

pre_allocated = np.empty_like(large_matrix)

До изменения время выполнения на моей машине было:

0.6756095182868318
1.2262537249271794
1.250292605883855

После изменения:

0.6776479894965846
0.6468182835551346
0.6538956945388001

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

Похоже, что нет никакого усиления из-за предварительного распределения.

Обратите внимание, что распределение очень быстрое, поскольку оно резервирует только адресное пространство.,ОЗУ потребляется только на событие доступа на самом деле.Размер буфера составляет 20 МБ, поэтому он больше кэша L3 на процессоре.Время выполнения будет зависеть от ошибок страниц и повторного заполнения кешей.Более того, в первом случае память перераспределяется сразу после освобождения.Ресурс, вероятно, будет «горячим» для распределителя памяти.Поэтому вы не можете напрямую сравнивать решение А. с другими.

Измените строку «действие» в первом случае, чтобы сохранить фактический результат:

        np.subtract(list_of_matrices[j], vector, out=pre_allocated[m*j:m*(j+1)])

Тогда выигрыш от векторизованных операций станет более заметным:

0.8738251849091547
0.678185239557866
0.6830777283598941
...