В настоящее время я пытаюсь лучше понять проблемы с памятью и кешем. Я где-то читал, что локальность памяти важнее для чтения, чем для записи, потому что в первом случае процессор должен фактически ожидать данные, тогда как во втором случае он может просто отправить их и забыть о них.
Имея это в виду, я сделал следующий быстрый и грязный тест: я написал скрипт, который создает массив из N случайных чисел с плавающей точкой и перестановку, то есть массив, содержащий числа от 0 до N-1 в случайном порядке. Затем он многократно либо (1) считывает массив данных линейно и записывает его обратно в новый массив в шаблоне произвольного доступа, заданного перестановкой, либо (2) считывает массив данных в переставленном порядке и линейно записывает его в новый массив.
К моему удивлению (2) показалось постоянно быстрее, чем (1). Однако были проблемы с моим скриптом
- Сценарий написан на python/numpy. Это довольно высокоуровневый язык, поэтому не ясно, насколько хорошо реализовано чтение/запись.
- Я, вероятно, не сбалансировал два случая должным образом.
Кроме того, некоторые из ответов/комментариев ниже предполагают, что мое исходное ожидание неверно и что в зависимости от деталей кэша процессора любой случай может быть быстрее.
Мой вопрос:
- Какой (если есть) из двух должен быть быстрее?
- Каковы релевантные концепции кеша здесь; как они влияют на результат
Приятное объяснение для начинающих будет оценено. Любой вспомогательный код должен быть в C/cython/numpy/numba или python.
По выбору:
- Объясните, почему абсолютные длительности нелинейны по размеру задачи (см. Сроки ниже).
- Объясните поведение моих явно неадекватных экспериментов с питоном.
Для справки, моей платформой является Linux-4.12.14-lp150.11-default-x86_64-with-glibc2.3.4
. Версия Python 3.6.5.
Вот код, который я написал:
import numpy as np
from timeit import timeit
def setup():
global a, b, c
a = np.random.permutation(N)
b = np.random.random(N)
c = np.empty_like(b)
def fwd():
c = b[a]
def inv():
c[a] = b
N = 10_000
setup()
timeit(fwd, number=100_000)
# 1.4942631321027875
timeit(inv, number=100_000)
# 2.531870319042355
N = 100_000
setup()
timeit(fwd, number=10_000)
# 2.4054739447310567
timeit(inv, number=10_000)
# 3.2365565397776663
N = 1_000_000
setup()
timeit(fwd, number=1_000)
# 11.131387163884938
timeit(inv, number=1_000)
# 14.19817715883255
Как отметили @Trilarion и @Yann Vernier, мои фрагменты не сбалансированы должным образом, поэтому я заменил их на
def fwd():
c[d] = b[a]
b[d] = c[a]
def inv():
c[a] = b[d]
b[a] = c[d]
где d = np.arange(N)
(я перетасовываю все в обе стороны, мы надеемся уменьшить эффекты пробного кэширования). Я также заменил timeit
на repeat
и уменьшил количество повторений в 10 раз.
Тогда я получаю
[0.6757169323973358, 0.6705542299896479, 0.6702114241197705] #fwd
[0.8183442652225494, 0.8382121799513698, 0.8173762648366392] #inv
[1.0969422250054777, 1.0725746559910476, 1.0892365919426084] #fwd
[1.0284497970715165, 1.025063106790185, 1.0247828317806125] #inv
[3.073981977067888, 3.077839042060077, 3.072118630632758] #fwd
[3.2967213969677687, 3.2996009718626738, 3.2817375687882304] #inv
Таким образом, кажется, что разница все еще существует, но она намного тоньше и теперь может пойти в любую сторону в зависимости от размера проблемы.