Распределяет ли программная предварительная загрузка буфер заполнения строки (LFB)?

Я понял, что закон Литтла ограничивает скорость передачи данных с заданной задержкой и с определенным уровнем параллелизма. Если вы хотите перевести что-то быстрее, вам нужны либо более крупные переводы, больше переводов "в полете" или меньшая задержка. В случае чтения из ОЗУ параллелизм ограничен числом буферов заполнения строки.

Буфер заполнения строки выделяется, когда нагрузка пропускает кэш L1. Современные чипы Intel (Nehalem, Sandy Bridge, Ivy Bridge, Haswell) имеют 10 LFB на ядро ​​и, таким образом, ограничены 10 невыполненными ошибками кэша на ядро. Если задержка ОЗУ составляет 70 нс (правдоподобно), а каждая передача составляет 128 байт (строка кэша 64B плюс его аппаратно предварительно выбранный сдвоенный), это ограничивает пропускную способность на ядро: 10 * 128B / 75 нс = ~16 ГБ / с. Такие тесты, как однопоточный поток, подтверждают, что это достаточно точно.

Очевидным способом уменьшения задержки будет предварительная выборка нужных данных с помощью инструкций x64, таких как PREFETCHT0, PREFETCHT1, PREFETCHT2 или PREFETCHNTA, чтобы их не приходилось считывать из ОЗУ. Но я не смог ничего ускорить, используя их. Кажется, проблема в том, что сами инструкции __mm_prefetch() потребляют LFB, поэтому они также подчиняются тем же ограничениям. Аппаратные предварительные выборки не затрагивают LFB, но также не пересекают границы страницы.

Но я нигде не могу найти ничего подобного. Самое близкое, что я нашел, - это 15-летняя статья, в которой говорится, что при предварительной выборке на Pentium III используются буферы линейного заполнения. Я волнуюсь, что вещи могли измениться с тех пор. И так как я думаю, что LFB связаны с кешем L1, я не уверен, почему предварительная выборка к L2 или L3 потребляет их. И все же скорости, которые я измеряю, соответствуют этому случаю.

Итак: есть ли способ инициировать выборку из нового места в памяти, не используя один из этих 10 линейных буферов заполнения, таким образом, достигая более высокой пропускной способности, обходя закон Литтла?

2 ответа

Основываясь на моем тестировании, все типы инструкций предварительной выборки используют буферы заполнения строки на последних основных процессорах Intel.

В частности, я добавил несколько тестов load & prefetch в uarch-bench, которые используют большие скачки по буферам разных размеров. Вот типичные результаты на моем Skylake i7-6700HQ:

                     Benchmark   Cycles    Nanos
  16-KiB parallel        loads     0.50     0.19
  16-KiB parallel   prefetcht0     0.50     0.19
  16-KiB parallel   prefetcht1     1.15     0.44
  16-KiB parallel   prefetcht2     1.24     0.48
  16-KiB parallel prefetchtnta     0.50     0.19

  32-KiB parallel        loads     0.50     0.19
  32-KiB parallel   prefetcht0     0.50     0.19
  32-KiB parallel   prefetcht1     1.28     0.49
  32-KiB parallel   prefetcht2     1.28     0.49
  32-KiB parallel prefetchtnta     0.50     0.19

 128-KiB parallel        loads     1.00     0.39
 128-KiB parallel   prefetcht0     2.00     0.77
 128-KiB parallel   prefetcht1     1.31     0.50
 128-KiB parallel   prefetcht2     1.31     0.50
 128-KiB parallel prefetchtnta     4.10     1.58

 256-KiB parallel        loads     1.00     0.39
 256-KiB parallel   prefetcht0     2.00     0.77
 256-KiB parallel   prefetcht1     1.31     0.50
 256-KiB parallel   prefetcht2     1.31     0.50
 256-KiB parallel prefetchtnta     4.10     1.58

 512-KiB parallel        loads     4.09     1.58
 512-KiB parallel   prefetcht0     4.12     1.59
 512-KiB parallel   prefetcht1     3.80     1.46
 512-KiB parallel   prefetcht2     3.80     1.46
 512-KiB parallel prefetchtnta     4.10     1.58

2048-KiB parallel        loads     4.09     1.58
2048-KiB parallel   prefetcht0     4.12     1.59
2048-KiB parallel   prefetcht1     3.80     1.46
2048-KiB parallel   prefetcht2     3.80     1.46
2048-KiB parallel prefetchtnta    16.54     6.38

Ключевым моментом, который стоит отметить, является то, что ни один из методов предварительной выборки не работает намного быстрее, чем загрузка при любом размере буфера. Если какая-либо инструкция предварительной выборки не использует LFB, мы ожидаем, что она будет очень быстрой для эталонного теста, который вписывается в уровень кеша, к которому она выполняет предварительную выборку. Напримерprefetcht1вводит строки в L2, поэтому для теста 128-КиБ можно ожидать, что он будет быстрее, чем вариант загрузки, если он не использует LFB.

Более убедительно, мы можем изучитьl1d_pend_miss.fb_full счетчик, описание которого:

Количество раз, когда запросу требовалась запись FB (Fill Buffer), но для нее не было доступной записи. Запрос включает в себя кэшируемые / не кэшируемые требования, которые являютсяинструкциями загрузки, сохранения илипредварительной выборки ПО.

В описании уже указывается, что предварительным выборкам SW требуются записи LFB, и тестирование подтвердило это: для всех типов предварительной выборки этот показатель был очень высоким для любого теста, где параллелизм был ограничивающим фактором. Например, для 512-КиБprefetcht1тестовое задание:

 Performance counter stats for './uarch-bench --test-name 512-KiB parallel   prefetcht1':

        38,345,242      branches                                                    
     1,074,657,384      cycles                                                      
       284,646,019      mem_inst_retired.all_loads                                   
     1,677,347,358      l1d_pend_miss.fb_full                  

fb_full значение больше, чем количество циклов, это означает, что LFB был заполнен почти все время (это может быть больше, чем количество циклов, поскольку до двух нагрузок может потребоваться LFB на цикл). Эта рабочая нагрузка - чистые предварительные выборки, поэтому заполнять LFB нечего, кроме предварительной выборки.

Результаты этого теста также отражают заявленное поведение в разделе руководства, цитируемом Лиором:

Есть случаи, когда PREFETCH не будет выполнять предварительную выборку данных. Они включают:

  • ...
  • Если в подсистеме памяти не хватает буферов запросов между кэшем первого уровня и кэшем второго уровня.

Очевидно, что это не так: запросы на предварительную выборку не сбрасываются при заполнении LFB, а останавливаются как обычная загрузка до тех пор, пока ресурсы не станут доступны (это не является необоснованным поведением: если вы запрашивали программную предварительную выборку, вы, вероятно, захотите чтобы получить его, возможно, даже если это означает, что он застопорился).

Мы также отмечаем следующие интересные поведения:

  • Кажется, есть небольшая разница между prefetcht1 а также prefetcht2 так как они сообщают о разной производительности для теста 16 КиБ (разница варьируется, но постоянно отличается), но если вы повторите тест, вы увидите, что это скорее всего вариация от прогона к прогоне, поскольку эти конкретные значения несколько нестабильный (большинство других значений очень стабильны).
  • Для испытаний L2 мы можем выдержать 1 нагрузку за цикл, но только одну prefetcht0 упреждающий. Это немного странно, потому что prefetcht0 должна быть очень похожа на нагрузку (и может выдавать 2 за цикл в случаях L1).
  • Несмотря на то, что L2 имеет ~12 циклов задержки, мы можем полностью скрыть LFB задержки только с 10 LFB: мы получаем 1,0 цикла на нагрузку (ограниченную пропускной способностью L2), а не 12 / 10 == 1.2 циклов на нагрузку, которые мы ожидаем (в лучшем случае), если LFB был ограничивающим фактом (и очень низкие значения для fb_full подтверждает это). Вероятно, это связано с тем, что задержка в 12 циклов - это полная задержка загрузки до использования на всем ядре выполнения, которая включает в себя также несколько циклов дополнительной задержки (например, задержка L1 составляет 4-5 циклов), поэтому фактическое время, затраченное на LFB составляет менее 10 циклов.
  • Для тестов L3 мы видим значения 3,8-4,1 циклов, очень близкие к ожидаемым 42/10 = 4,2 циклам на основе задержки загрузки в L3. Таким образом, мы определенно ограничены 10 LFB, когда мы попадаем в L3. Вотprefetcht1а такжеprefetcht2 постоянно на 0,3 цикла быстрее, чем нагрузки или prefetcht0, Учитывая 10 LFB, это равняется 3 циклам меньше занятости, более или менее объясненной остановкой предварительной выборки на L2 вместо того, чтобы идти полностью к L1.
  • prefetchtntaкак правило, имеет гораздо более низкую пропускную способность, чем другие за пределами L1. Это, вероятно, означает, чтоprefetchtntaфактически делает то, что должен, и, кажется, приносит линии в L1, а не в L2, и только "слабо" в L3. Таким образом, для тестов, содержащих L2, он имеет пропускную способность, ограниченную параллелизмом, как если бы он попадал в кэш L3, а для случая 2048 КБ (1/3 от размера кэша L3) он имеет производительность попадания в основную память.prefetchnta ограничивает загрязнение кэша L3 (примерно одним способом на набор), поэтому мы, похоже, получаем выселения.

Может ли быть иначе?

Вот более старый ответ, который я написал перед тестированием, рассуждая о том, как он может работать:

В целом, я ожидал бы, что любая предварительная выборка, которая приводит к тому, что данные, заканчивающиеся в L1, потребляют буфер заполнения строки, так как я считаю, что единственный путь между L1 и остальной частью иерархии памяти - это LFB1. Так что предварительные выборки SW и HW, которые нацелены на L1, вероятно, используют LFB.

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

Это оставляет программную предварительную выборку, предназначенную для L2 (или выше), такую ​​какprefetcht1а такжеprefetcht22 В отличие от запросов, сгенерированных L2, они запускаются в ядре, поэтому им нужен какой-то способ выхода из ядра, и это может быть через LFB. Из руководства по оптимизации Intel есть следующие интересные цитаты (выделено мое):

Как правило, программная предварительная выборка в L2 будет показывать больше преимуществ, чем предварительная выборка L1. Программная предварительная выборка в L1 будет потреблять критические аппаратные ресурсы (заполнить буфер), пока заполнение строки кэша не завершится.Программная предварительная выборка в L2 не удерживает эти ресурсы, и она с меньшей вероятностью окажет негативное влияние на производительность. Если вы используете программные предварительные выборки L1, то лучше всего, если программная предварительная выборка обслуживается попаданиями в кэш-память L2, поэтому продолжительность удержания аппаратных ресурсов сводится к минимуму.

Казалось бы, это указывает на то, что программные предварительные выборки не используют LFB, но эта цитата относится только к архитектуре Knights Landing, и я не могу найти подобный язык ни для одной из более распространенных архитектур. Похоже, что дизайн кэша Knights Landing значительно отличается (или цитата неверна).


На самом деле, я думаю, что даже не временные хранилища используют LFB для выхода из ядра выполнения, но время их заполнения короткое, потому что, как только они попадают в L2, они могут войти в супер-очередь (фактически не входя в L2).), а затем освободить их связанный LFB.

2 Я думаю, что оба они нацелены на L2 на недавнем Intel, но это также неясно - возможно, t2 намек на самом деле нацелен на ООО на некоторых уархах?

Прежде всего, небольшое исправление - прочитайте руководство по оптимизации, и вы заметите, что некоторые средства предварительной выборки HW принадлежат кэш-памяти L2 и, как таковые, не ограничены количеством буферов заполнения, а скорее аналогом L2.

"Пространственный предварительный выборщик" (подразумеваемая строка colocated-64B, завершающаяся блоками 128B) является одним из них, поэтому теоретически, если вы выберете каждую вторую строку, вы сможете получить более высокую пропускную способность (некоторые предварительные сборщики DCU могут попытаться "Заполните пробелы для вас", но теоретически они должны иметь более низкий приоритет, чтобы это могло работать).

Тем не менее, "король" prefetcher другой парень, "L2 стример". Раздел 2.1.5.4 гласит:

Streamer: этот модуль предварительной выборки отслеживает запросы на чтение из кэша L1 для возрастания и убывания последовательностей адресов. Контролируемые запросы на чтение включают запросы Lache DCache, инициированные операциями загрузки и сохранения и аппаратными устройствами предварительной выборки, а также запросы ICache L1 для выборки кода. Когда обнаружен прямой или обратный поток запросов, ожидаемые строки кэша предварительно выбираются. Предварительно выбранные строки кэша должны быть на одной странице 4K

Важная часть -

Стример может выдавать два запроса на предварительную выборку при каждом поиске L2. Стример может работать на 20 строк впереди запроса на загрузку

Это соотношение 2:1 означает, что для потока обращений, который распознается этим средством предварительной выборки, он всегда будет опережать ваши обращения. Это правда, что вы не увидите эти строки в вашем L1 автоматически, но это означает, что, если все работает хорошо, вы всегда должны получать задержку попадания L2 для них (как только поток предварительной выборки имел достаточно времени, чтобы запустить и уменьшить L3/ память). задержки). У вас может быть только 10 LFB, но, как вы отметили в своих вычислениях - чем меньше задержка доступа, тем быстрее вы можете заменить их, тем выше пропускная способность. Это по существу отрыв L1 <-- mem задержка в параллельные потоки L1 <-- L2 а также L2 <-- mem,

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

Есть случаи, когда PREFETCH не будет выполнять предварительную выборку данных. Они включают:

  • PREFETCH вызывает промах DTLB (Lookaside Buffer для трансляции данных). Это относится к процессорам Pentium 4 с сигнатурой CPUID, соответствующей семейству 15, модели 0, 1 или 2. PREFETCH разрешает пропуски DTLB и извлекает данные на процессорах Pentium 4 с сигнатурой CPUID, соответствующей семейству 15, модели 3.
  • Доступ к указанному адресу, который вызывает ошибку / исключение.
  • Если в подсистеме памяти не хватает буферов запросов между кэшем первого уровня и кэшем второго уровня.

...

Поэтому я полагаю, что вы правы, а предварительные выборки SW - это не способ искусственно увеличить количество невыполненных запросов. Тем не менее, то же самое объяснение применимо и здесь: если вы знаете, как использовать предварительную выборку SW, чтобы получить доступ к своим линиям достаточно заранее, вы можете уменьшить некоторую задержку доступа и увеличить эффективную BW. Это, однако, не будет работать для длинных потоков по двум причинам: 1) емкость вашего кэша ограничена (даже если предварительная выборка временная, как, например, t0), и 2) вам все равно нужно заплатить полную задержку L1->mem для каждая предварительная выборка, так что вы просто немного продвигаете свою нагрузку - если ваши манипуляции с данными быстрее, чем доступ к памяти, вы в конечном итоге догоните свою предварительную выборку ПО. Так что это работает, только если вы можете заранее забрать все, что вам нужно, достаточно хорошо и сохранить его там.

Другие вопросы по тегам