Распределяет ли программная предварительная загрузка буфер заполнения строки (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
а такжеprefetcht2
2 В отличие от запросов, сгенерированных 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 для каждая предварительная выборка, так что вы просто немного продвигаете свою нагрузку - если ваши манипуляции с данными быстрее, чем доступ к памяти, вы в конечном итоге догоните свою предварительную выборку ПО. Так что это работает, только если вы можете заранее забрать все, что вам нужно, достаточно хорошо и сохранить его там.