Анализ пропускной способности в тесте копирования памяти - PullRequest
0 голосов
/ 15 января 2019

Я сравниваю следующую функцию copy (не очень!) С аргументом size (~ 1 ГБ):

void copy(unsigned char* dst, unsigned char* src, int count)
{
    for (int i = 0; i < count; ++i)
    { 
         dst[i] = src[i];
    }
}

Я собрал этот код с GCC 6.2, с -O3 -march=native -mtune-native на Xeon E5-2697 v2.

Просто чтобы вы посмотрели на сборку, сгенерированную gcc на моей машине, я вставлю сюда сборку, сгенерированную во внутреннем цикле:

movzx ecx, byte ptr [rsi+rax*1]
mov byte ptr [rdi+rax*1], cl
add rax, 0x1
cmp rdx, rax
jnz 0xffffffffffffffea

Теперь, когда мой LLC составляет ~ 25 МБ, а я копирую ~ 1 ГБ, имеет смысл, что этот код связан с памятью. perf подтверждает это большим числом остановленных циклов внешнего интерфейса:

        6914888857      cycles                    #    2,994 GHz                    
        4846745064      stalled-cycles-frontend   #   70,09% frontend cycles idle   
   <not supported>      stalled-cycles-backend   
        8025064266      instructions              #    1,16  insns per cycle        
                                                  #    0,60  stalled cycles per insn

Мой первый вопрос о 0,60 остановленных циклах на инструкцию. Это кажется очень низким числом для такого кода, который обращается к LLC / DRAM все время, поскольку данные не кэшируются. Как достигается это время ожидания LLC 30 циклов, а основной памяти около 100 циклов?

Мой второй вопрос связан; кажется, что prefetcher делает относительно хорошую работу (не удивительно, что это массив, но все же): мы используем 60% времени в LLC вместо DRAM. Тем не менее, в чем причина его провала в другой раз? Какая пропускная способность / часть uncore сделали этот предварительный сборщик не в состоянии выполнить свою задачу?

          83788617      LLC-loads                                                    [50,03%]
          50635539      LLC-load-misses           #   60,43% of all LL-cache hits    [50,04%]
          27288251      LLC-prefetches                                               [49,99%]
          24735951      LLC-prefetch-misses                                          [49,97%]

И последнее, но не менее важное: я знаю, что Intel может передавать инструкции; это также относится к таким mov с операндами памяти?

Большое спасибо!

Ответы [ 2 ]

0 голосов
/ 16 января 2019

TL; DR: в неиспользованном домене всего 5 мопов (см .: Режимы микросинтеза и адресации ). Детектор потока цикла на Ivy Bridge не может распределять мопы по границам тела цикла (см .: Снижается ли производительность при выполнении циклов, число мопов которых не кратно ширине процессора? ), поэтому для выделения требуется два цикла одна итерация. Цикл на самом деле работает на 2.3c / iter на двойной розетке Xeon E5-2680 v2 (10 ядер на сокет против ваших 12), так что это близко к лучшему, что может быть сделано с учетом узкого места внешнего интерфейса.

Предварительные выборщики работали очень хорошо, и большую часть времени цикл не связан с памятью. Копирование 1 байта за 2 цикла происходит очень медленно. (gcc сделал плохую работу и должен был дать вам цикл, который мог бы выполняться с 1 итерацией за такт. Без оптимизации по профилю, даже -O3 не позволяет -funroll-loops, но есть приемы, которые он мог бы использовать ( например, подсчет отрицательного индекса до нуля или индексирование нагрузки относительно хранилища и увеличение указателя назначения), что привело бы к уменьшению цикла до 4 моп.)

Дополнительные .3 цикла на итерацию медленнее, чем узкое место в среднем переднего конца. - это , вероятно, из-за неудачных попыток предварительной выборки (возможно, на границах страницы), или, возможно, из-за сбоев страниц и пропусков TLB в этом тест, который выполняется в статически инициализированной памяти в разделе .data.


В цикле есть две зависимости данных. Во-первых, инструкция сохранения (в частности, STD uop) зависит от результата инструкции загрузки. Во-вторых, инструкции для сохранения и загрузки зависят от add rax, 0x1. На самом деле add rax, 0x1 зависит также и от самого себя. Поскольку задержка add rax, 0x1 составляет один цикл, верхняя граница производительности цикла составляет 1 цикл на итерацию.

Поскольку хранилище (STD) зависит от нагрузки, оно не может быть отправлено с RS до тех пор, пока загрузка не завершится, что занимает не менее 4 циклов (в случае попадания L1). Кроме того, есть только один порт, который может принимать STOP-мопы, но до двух нагрузок можно завершить за цикл на Ivy Bridge (особенно в случае, если две нагрузки относятся к линиям, которые находятся в кэше L1, и не возникает конфликт банков), приводя к дополнительному раздору. Тем не менее, RESOURCE_STALLS.ANY показывает, что фактическое RS никогда не заполняется. IDQ_UOPS_NOT_DELIVERED.CORE подсчитывает количество неиспользованных слотов выдачи. Это равно 36% всех слотов. Событие LSD.CYCLES_ACTIVE показывает, что ЛСД используется для доставки мопов большую часть времени. Однако LSD.CYCLES_4_UOPS / LSD.CYCLES_ACTIVE = ~ 50% показывает, что примерно в 50% циклов на RS поступает менее 4 моп. RS не будет заполнен из-за неоптимальной пропускной способности распределения.

Счет stalled-cycles-frontend соответствует UOPS_ISSUED.STALL_CYCLES, который подсчитывает количество остановок распределения, связанных как с внешними, так и с задними частями. Я не понимаю, как UOPS_ISSUED.STALL_CYCLES связано с количеством циклов и других событий.

Количество LLC-loads включает в себя:

  • Вся нагрузка по требованию запрашивает к L3 независимо от того, попадет или нет запрос в L3 и, в случае пропуска, независимо от источника данных. Это также включает запросы загрузки по требованию от аппаратного средства просмотра страниц. Мне не ясно, учитываются ли запросы на загрузку от средства предварительной загрузки следующей страницы.
  • Все запросы на чтение данных аппаратной предварительной выборки, сгенерированные предварительной выборкой L2, где целевая линия должна быть помещена в L3 (то есть в L3 или в оба в L3 и L2, но не только в L2). Аппаратные запросы чтения данных устройства предварительной выборки L2, где линия должна быть размещена только в L2, не включены. Обратите внимание, что запросы предварительных сборщиков L1 поступают в L2 и влияют на них и могут вызывать предварительные выборщики L2, то есть они не пропускают L2.

LLC-load-misses является подмножеством LLC-loads и включает только те события, которые пропущены в L3. Оба подсчитаны на ядро.

Существует важное различие между подсчетом запросов (гранулярность строки кэша) и подсчетом инструкций загрузки или загрузок мопов (с использованием MEM_LOAD_UOPS_RETIRED.*). Кэши L1 и L2 кэшируют запросы загрузки сквоша в одну и ту же строку кэша, поэтому многочисленные пропуски в L1 могут привести к одному запросу к L3.

Оптимальная производительность может быть достигнута, если все хранилища и нагрузки попадут в кэш L1. Поскольку размер используемого вами буфера составляет 1 ГБ, цикл может вызвать максимум 1 ГБ / 64 = ~ 17 М запросов загрузки L3. Тем не менее, ваше LLC-loads измерение, 83M, намного больше, возможно, из-за кода, отличного от цикла, который вы показали в вопросе. Другая возможная причина заключается в том, что вы забыли использовать суффикс :u для подсчета только событий пользовательского режима.

Мои измерения как на IvB, так и на HSW показывают, что LLC-loads:u ничтожно мал по сравнению с 17M. Однако большинство нагрузок L3 являются пропущенными (т. Е. LLC-loads:u = ~ LLC-loads-misses:u). CYCLE_ACTIVITY.STALLS_LDM_PENDING показывает, что общее влияние нагрузок на производительность незначительно. Кроме того, мои измерения показывают, что цикл работает на 2,3c / iter на IvB (против 1,5c / iter на HSW), что говорит о том, что одна нагрузка выдается каждые 2 цикла. Я думаю, что неоптимальная пропускная способность распределения является главной причиной этого. Обратите внимание, что условия псевдонимов 4K (LD_BLOCKS_PARTIAL.ADDRESS_ALIAS) практически отсутствуют. Все это означает, что средства предварительной выборки довольно хорошо справились с задачей скрытия задержки доступа к памяти для большинства нагрузок.


Счетчики на IvB, которые можно использовать для оценки производительности аппаратных средств предварительной выборки:

Ваш процессор имеет два устройства предварительной выборки данных L1 и два устройства предварительной выборки данных L2 (один из них может выполнять предварительную выборку как в L2, так и / или в L3). Устройство предварительной выборки может быть неэффективным по следующим причинам:

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

Количество пропущенных запросов на L1, L2 и L3 является хорошим показателем того, насколько хорошо работают сборщики. Все пропуски L3 (как считается LLC-load-misses) также обязательно являются пропусками L2, поэтому количество пропусков L2 больше, чем LLC-load-misses. Также все пропуски по требованию L2 - это обязательно пропуски по L1.

На Ivy Bridge вы можете использовать события производительности LOAD_HIT_PRE.HW_PF и CYCLE_ACTIVITY.CYCLES_* (в дополнение к событиям промаха), чтобы узнать больше о том, как выполнялись сборщики предварительной выборки, и оценить их влияние на производительность. Важно измерить CYCLE_ACTIVITY.CYCLES_* событий, потому что даже если количество пропусков было на первый взгляд высоким, это не обязательно означает, что пропуски являются основной причиной снижения производительности.

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


Код, который я использовал, следующий:

BITS 64
DEFAULT REL

section .data
bufdest:    times COUNT db 1 
bufsrc:     times COUNT db 1

section .text
global _start
_start:
    lea rdi, [bufdest]
    lea rsi, [bufsrc]

    mov rdx, COUNT
    mov rax, 0

.loop:
    movzx ecx, byte [rsi+rax*1]
    mov byte [rdi+rax*1], cl
    add rax, 1
    cmp rdx, rax
    jnz .loop

    xor edi,edi
    mov eax,231
    syscall
0 голосов
/ 15 января 2019

Мой первый вопрос о 0,60 остановленных циклах на инструкцию. Это кажется очень низким числом для такого кода, который обращается к LLC / DRAM все время, поскольку данные не кэшируются. Как достигается это время ожидания LLC 30 циклов, а основной памяти около 100 циклов?

Мой второй вопрос связан; кажется, что prefetcher делает относительно хорошую работу (не удивительно, что это массив, но все же): мы используем 60% времени в LLC вместо DRAM. Тем не менее, в чем причина его провала в другой раз? Какая пропускная способность / часть uncore сделали этот предварительный сборщик не в состоянии выполнить свою задачу?

С предварительными сборщиками. В частности, в зависимости от того, какой ЦП это, может быть «средство предварительной выборки TLB», извлекающее трансляции виртуальной памяти, плюс средство предварительной выборки строки кэша, которое извлекает данные из ОЗУ в L3, плюс средство предварительной выборки L1 или L2, извлекающее данные из L3.

Обратите внимание, что кэши (например, L3) работают с физическими адресами, аппаратный предварительный выборщик работает с обнаружением и предварительной выборкой последовательных обращений к физическим адресам, а из-за управления / разбиения по страницам виртуальной памяти физический доступ почти никогда не бывает последовательным на границах страниц. По этой причине средство предварительной выборки прекращает предварительную выборку на границах страницы и, вероятно, выполняет три «не предварительно выбранных» доступа, чтобы начать предварительную выборку со следующей страницы.

Также обратите внимание, что если бы ОЗУ было медленнее (или код был быстрее), то средство предварительной выборки не сможет идти в ногу, и вы остановитесь. Для современных многоядерных машин оперативная память часто достаточно быстра, чтобы соответствовать одному ЦП, но не справляется со всеми процессорами. Это означает, что за пределами «контролируемых условий тестирования» (например, когда пользователь запускает 50 процессов одновременно и все ЦП работают с ОЗУ), ваш эталонный тест будет полностью неверным. Есть также такие вещи, как IRQ, переключатели задач и сбои страниц, которые могут / будут мешать (особенно когда компьютер загружен).

И последнее, но не менее важное: я знаю, что Intel может передавать инструкции; это также относится к таким мовам с операндами памяти?

Да; но обычный mov с памятью (например, mov byte ptr [rdi+rax*1], cl) также будет ограничен правилами упорядочения памяти «заказано с перезаписью из хранилища».

Обратите внимание, что есть много способов ускорить копирование, в том числе использование невременных хранилищ (для преднамеренного нарушения / обхода правил упорядочения памяти), используя rep movs (который специально оптимизирован для работы с целыми строками кэша, где это возможно) использование гораздо больших фрагментов (например, копирование AVX2 по 32 байта за раз), самостоятельная предварительная выборка (особенно на границах страниц) и очистка кэша (чтобы кеши по-прежнему содержали полезные сведения после выполнения копирования).

Однако гораздо лучше поступить наоборот - намеренно делать большие копии очень медленно, так что программист замечает, что они отстой и "вынужден" пытаться найти способ избежать копирования. Это может стоить 0 циклов, чтобы избежать копирования 20 МБ, что значительно быстрее, чем «наименее худшая» альтернатива.

...