Ответ 1
То, что я нашел до сих пор. К сожалению, на самом деле это не дает объяснения низкой производительности, а вовсе не для бимодального распространения, но это скорее набор правил, когда вы можете увидеть производительность и примечания по ее смягчению:
- Пропускная способность хранилища в L2 составляет не более одной 64-байтной строки кэша за три цикла 0 помещая верхний предел ~ 21 байт за цикл на пропускную способность магазина. С другой стороны, серия магазинов, которые промахиваются в L1 и попадают в L2, будет занимать не менее трех циклов на каждую строку кэша.
- Над этой базой существует значительное ограничение, когда магазины, попавшие в L2, чередуются с магазинами в другую строку кэша (независимо от того, попадают ли эти магазины в L1 или L2).
- Штраф, по-видимому, несколько больше для магазинов, находящихся поблизости (но все же не в одной строке кэша).
- Бимодальная производительность по крайней мере поверхностно связана с вышеупомянутым эффектом, поскольку в случае без перемежения она не появляется, хотя у меня нет дополнительных объяснений.
- Если вы убедитесь, что строка кэша уже находится в L1 перед хранилищем, путем предварительной выборки или фиктивной загрузки, медленная производительность исчезает, а производительность больше не является бимодальной.
Детали и фотографии
64-байт Stride
Исходный вопрос произвольно использовал шаг 16, но пусть начинается с, вероятно, простейшего случая: шаг 64, т.е. одна полная строка кеша. Как оказалось, различные эффекты видны с любым шагом, но 64 обеспечивает прохождение кеша L2 на каждом шаге и поэтому удаляет некоторые переменные.
Позвольте также удалить второй магазин на данный момент - так что мы просто тестируем однострочное хранилище на 64 байта на 64 КБ памяти:
top:
mov BYTE PTR [rdx],al
add rdx,0x40
sub rdi,0x1
jne top
Выполняя это в той же самой упряжке, что и выше, я получаю около 3.05 циклов/хранилище 2 хотя есть довольно небольшая вариация по сравнению с тем, что я привык видеть (- вы даже можете найти там 3.0).
Таким образом, мы знаем, что мы, вероятно, не будем делать лучше, чем это для устойчивых магазинов, исключительно для L2 1. Хотя Skylake, по-видимому, имеет пропускную способность в 64 байта между L1 и L2, в случае потока магазинов эта полоса пропускания должна использоваться как для выселений из L1, так и для загрузки новой строки в L1. 3 цикла кажутся разумными, если для каждого из них требуется один цикл (a) выселить грязную линию жертвы от L1 до L2 (b) обновить L1 с новой строкой из L2 и (c) зафиксировать хранилище в L1.
Что происходит, когда вы добавляете вторую запись в одну строку кэша (в следующий байт, хотя это и не имеет значения) в цикле? Вот так:
top:
mov BYTE PTR [rdx],al
mov BYTE PTR [rdx+0x1],al
add rdx,0x40
sub rdi,0x1
jne top
Здесь приведена гистограмма времени для 1000 прогонов тестовой жгуты для вышеуказанного цикла:
count cycles/itr
1 3.0
51 3.1
5 3.2
5 3.3
12 3.4
733 3.5
139 3.6
22 3.7
2 3.8
11 4.0
16 4.1
1 4.3
2 4.4
Таким образом, большинство раз кластеризуется около 3,5 циклов. Это означает, что это дополнительное хранилище добавило только 0,5 цикла к времени. Это может быть что-то вроде буфера хранилища, способного выгрузить два магазина в L1, если они находятся в одной строке, но это происходит примерно в половине случаев.
Учтите, что буфер хранилища содержит ряд таких хранилищ, как 1, 1, 2, 2, 3, 3
, где 1
указывает строку кэша: половина позиций имеет два последовательных значения из одной и той же строки кэша, а половина - нет. Поскольку буфер хранилища ожидает, чтобы сбрасывать хранилища, а L1 активно вытесняет и принимает линии от L2, L1 будет доступен для магазина в произвольной точке, и если он находится в позиции 1, 1
, возможно, хранит сток в одном цикле, но если он равен 1, 2
, он принимает два цикла.
Обратите внимание, что существует еще один пик около 6% результатов около 3,1, а не 3,5. Это может быть устойчивое состояние, когда мы всегда получаем удачный результат. Существует еще один пик около 3% при ~ 4.0-4.1 - "всегда неудачная" компоновка.
Позвольте проверить эту теорию, посмотрев различные смещения между первым и вторым магазинами:
top:
mov BYTE PTR [rdx + FIRST],al
mov BYTE PTR [rdx + SECOND],al
add rdx,0x40
sub rdi,0x1
jne top
Мы пробуем все значения FIRST
и SECOND
от 0 до 256 с шагом 8. Результаты с переменными значениями FIRST
на вертикальной оси и SECOND
по горизонтали:
Мы видим конкретный шаблон - белые значения являются "быстрыми" (вокруг значений 3.0-4.1, рассмотренных выше для смещения 1). Значения желтого цвета выше, до 8 циклов и красные до 10. Фиолетовые выбросы являются самыми высокими и обычно являются случаями, когда "медленный режим", описанный в OP, срабатывает (как правило, с тактовой частотой 18,0 такта/итера). Мы замечаем следующее:
-
Из рисунка белых ячеек мы видим, что мы получаем результат быстрого цикла ~ 3.5, пока второй магазин находится в одной и той же строке кеша или следующей относительно первого хранилища. Это согласуется с вышеизложенной идеей, что хранилища в одной и той же строке кэша обрабатываются более эффективно. Причина, заключающаяся в том, что наличие второго хранилища в следующей строке кеша работает, заключается в том, что шаблон заканчивается тем же, за исключением первого первого доступа:
0, 0, 1, 1, 2, 2, ...
vs0, 1, 1, 2, 2, ...
- где во втором случае это второй магазин, который первым затрагивает каждую строку кеша. Однако буфер хранилища не волнует. Как только вы попадаете в разные строки кэша, вы получаете шаблон типа0, 2, 1, 3, 2, ...
и, видимо, это отстой? -
Фиолетовые "выбросы" никогда не появляются в белых областях, поэтому, по-видимому, они ограничены сценарием, который уже медленный (и медленнее здесь происходит примерно на 2,5x медленнее: от ~ 8 до 18 циклов).
Мы можем немного уменьшить масштаб и посмотреть на еще большие смещения:
Та же самая базовая модель, хотя мы видим, что производительность улучшается (зеленая область), так как второй магазин выходит дальше (впереди или позади) первого, вплоть до того, что он снова становится хуже со смещением около ~ 1700 байт. Даже в улучшенной области мы достигаем в лучшем случае 5,8 циклов/итераций, которые все еще намного хуже, чем производительность в 1,5 раза.
Если вы добавите какую-либо команду загрузки или предварительной выборки, которая проходит впереди 3 в магазинах, исчезают как общая медленная производительность, так и выбросы медленного режима:
Вы можете перенести это обратно на исходный шаг на 16 проблем - любой тип предварительной выборки или загрузки в цикле ядра, в значительной степени нечувствительный к расстоянию (даже если он по сути), устраняет проблему, и вы получаете 2,3 цикла/итерация, близкая к наилучшему идеалу 2.0 и равная сумме двух магазинов с отдельными контурами.
Таким образом, основное правило заключается в том, что хранилища для L2 без соответствующих нагрузок намного медленнее, чем если бы вы их предварительно отбирали, если только весь поток хранилища не обращается к линиям кэша одним последовательным шаблоном. Это противоречит идее, что линейный шаблон, подобный этому, никогда не выигрывает от предварительной выборки SW.
На самом деле у меня нет пояснения, но они могут включать следующие факторы:
- Наличие других хранилищ в буферах хранилища может уменьшить concurrency запросов, идущих на L2. Неясно, когда магазины, которые пропустят в L1, распределяют буфер хранилища, но, возможно, это происходит около того, когда магазин собирается уйти в отставку, и в буфер хранения есть определенное количество "головок", чтобы L1, поэтому наличие дополнительных магазинов, которые не будут пропущены в L1, повреждает concurrency, поскольку в lookahead не отображается столько запросов, которые будут пропущены.
- Возможно, существуют конфликты для ресурсов L1 и L2, таких как порты чтения и записи, пропускная способность между кешем, которые хуже с этим шаблоном магазинов. Например, когда хранилища в разные строки чередуются, возможно, они не могут так быстро разрядиться из очереди магазина (см. Выше, где, как представляется, в некоторых сценариях более одного хранилища может стекать за цикл).
Эти комментарии доктора МакКальпина на форумах Intel также весьма интересны.
0 В основном только достижимо, когда стример L2 отключен, поскольку в противном случае дополнительный конфликт на L2 замедляет это примерно до 1 строки за 3,5 цикла.
1 Сравните это с хранилищами, где я получаю почти ровно 1,5 цикла на загрузку, для подразумеваемой полосы пропускания ~ 43 байта за цикл. Это имеет прекрасный смысл: полоса L1 ↔ L2 имеет длину 64 байта, но при условии, что L1 либо принимает линию от L2, либо обслуживает запросы нагрузки от ядра каждого цикла (но не параллельно), то у вас есть 3 цикла для двух нагрузок для разных линий L2: 2 цикла для приема строк из L2 и 1 цикл для удовлетворения двух инструкций загрузки.
2 С предварительной выборкой. Как оказалось, предварительный сборщик L2 конкурирует за доступ к кэшу L2, когда он обнаруживает потоковый доступ: хотя он всегда находит строки-кандидаты и не переходит к L3, это замедляет код и увеличивает изменчивость. Выводы, как правило, выполняются с предварительной выборкой, но все просто немного медленнее (здесь большая доля результатовс предварительной выборкой - вы видите около 3,3 цикла на нагрузку, но с большой вариабельностью).
3 Это даже не нужно делать впереди - также выполняется предварительная выборка нескольких строк: я полагаю, что prefetch/load просто быстро запускаются впереди магазинов, которые являются узкими местами, поэтому они всегда идут вперед, Таким образом, предварительная выборка является своего рода самовосстановлением и, кажется, работает практически с любой ценностью, которую вы вложили.