Продолжение статьи: часть 1 , часть 3 , часть 4
Ээффективность использования строк кэша
Пункты 1.1 и 1.2 подчеркивают одну из самых основных проблем: использование только части адресного пространства в строке кэша.А это не только увеличивает трафик на шине, но и снижает эффективность цепей процессора.
Если во время нахождения в кеше используется только часть строки кеша, то приложение значительно уменьшает размер своего кеша.
В пункте 1.1 говорится о практике компиляторов упорядочивать элементы структур в памяти в соответствии с их размером.
Итак, если первым элементом структуры является «char», за которым следует 4-байтовый «int», то компилятор добавит между этими элементами еще 3 байта, чтобы расположить их в 4-байтовой сетке.
Пункт 1.2 вроде бы говорит сам за себя, но это один из важнейших источников высокой загруженности автобуса.
Есть много способов обнаружить эту проблему.
В приложениях с преобладанием циклов эффективность можно грубо оценить, подсчитав, сколько раз выполняются основные циклы и сколько байтов данных они используют на каждой итерации, посредством статического анализа ассемблерного листинга.
Результатом является максимальное количество байтов, используемых в цикле.
Измерив количество строк кэша, проходящих по шине за циклы, и умножив это число на 64, мы получим трафик на шине, инициируемый в циклах.
Теперь, если разделить количество байтов, потребляемых за цикл (из анализа листинга) на общее количество байтов, переданных по шине, мы получим приблизительную оценку эффективности использования шины.
Чтобы узнать, сколько раз выполняется цикл, усредните количество событий INST_RETIRED.ANY_P по командам цикла.
Это и будет количество исполнений базового блока.
Само событие неравномерно распределено по базовому блоку; однако общее число будет верным для «достаточно большой» области.
Итоговое значение для приложения верно, но «длинные» команды покажут гораздо большее количество этого события, чем команды непосредственно перед ними или после них внутри того же блока.
В цикле с доминирующим потоком, проходящим через несколько базовых блоков, событие должно усредняться по всем его базовым блокам, предполагая, что на основе статистического анализа ассемблерного листинга можно продемонстрировать, что все они на самом деле выполняются одинаковое количество раз.
Используя собранные данные, это можно сделать в VTune Analyser. Просто выделите все команды в цикле, чтобы отобразить их сумму.
Подсчитайте количество команд в выделенной области.
Умножьте сумму INST_RETIRED.ANY_P на значение после выборки (SAV) и разделите на количество команд. Это будет общее количество итераций цикла.
Этот метод также позволяет определить среднее количество итераций внутреннего цикла; необходимо количество выполнений внутреннего цикла разделить на количество выполнений базовых блоков до или после цикла.
Результат крайне неточный, если количество итераций внутреннего цикла намного больше 100, но очень точен для небольших циклов.
Однако, хотя этот метод не точен для большого количества итераций, в целом полезно знать, что итераций много.
Простой контроль загрузки и выгрузки в память поможет оценить общее количество байтов, используемых за итерацию.
Конечно, существует риск завышения этой оценки из-за одинаковых указателей, особенно при учете множества циклов.
Общее количество используемых строк кэша измеряется событием BUS_TRANS_BURST.SELF. Скоро будет доступна еще одна статья, в которой более подробно рассматривается анализ доступа к данным.
В случаях, когда в часто выполняемых циклах фактически используется лишь небольшая часть данных, передаваемых по шине, существует несколько способов улучшить использование шины и, следовательно, производительность одного потока или нескольких параллельных потоков.
Наиболее очевидное решение — разделить данные в соответствии с их фактическим использованием, организовав их в параллельные массивы или связанные списки структур.
В общем, только часть данных в каждой структуре используется повторно, тогда как большая часть используется лишь изредка.
В большом приложении полная реорганизация формата данных может быть чрезвычайно сложной.
В этом случае часто используемые данные можно скопировать в удобный упакованный массив структур, содержащий только часто используемые элементы.
Это позволит часто выполняемым программным циклам генерировать минимальный трафик шины при выполнении.
Более агрессивная оптимизация предполагает «развертывание» формата данных на 2 в случае чисел с плавающей запятой двойной точности или на 4, если все данные имеют формат int или одинарной точности с плавающей запятой (float).
Это откроет путь к чрезвычайно эффективному использованию набора инструкций Intel Streaming SIMD Extension 3 (SSE3), поскольку инструкции по упаковке данных в этом случае не потребуются.
Возможно, было бы даже неплохо добавить несколько фиктивных элементов в конец массива, чтобы размер массива был четным в 2 или 4 раза.
Возможно, лучше использовать структуры массива, а не структуры массива, если доминирующим шаблоном доступа является последовательный доступ к списку массивов, поскольку это гарантирует использование всех байтов строк кэша.
Разгрузка прошлого Кёша
Компиляторы избегают генерации инструкций по выгрузке потока после KЭsha в тех случаях, когда количество итераций цикла назначения данных неизвестно.Например, в простой функции ТРИАД ниже значение len неизвестно во время трансляции.
Большинство компиляторов будут использовать обычные инструкции по выгрузке или упакованные инструкции по выгрузке SSE для массива «a».double TRIAD(int len, double a1, double * a, double *b, double*c, double*d){ int i; for(i=0; i<len; i++) a[i] = b[i] + a1*d[i]; return; }
Это вызовет запрос владения строкой кэша, что удваивает пропускную способность, необходимую для этого массива.
Если массив не нужен сразу или слишком велик, чтобы целиком поместиться в КЭШе, то трафик можно сократить вдвое.
В приведенном выше примере, если количество итераций «len», умноженное на 8, больше размера кэша последнего уровня (LLC), деленного на 3, то первая строка кэша для массива «a» будет выгружена из кэша.
строка перед концом цикла.
Чтобы компилятор Intel генерировал оптимальный код для большого количества итераций, нужно выровнять 16-байтовые массивы, а затем модифицировать цикл, добавив непосредственно перед ним две директивы pragma. #pragm vector aligned
#pragma vector nontemporal
for(i=0; i<len; i++)
a[i] = b[i] + a1*d[i];
Чрезмерная предварительная выборка строк кэша также очень важна.
Плохо запрограммированные предварительные выборки программного обеспечения могут увеличить трафик на шине.
Убедитесь, что вы не выполняете предварительную выборку строк кэша, которые не будут использоваться позже.
Интервал предварительной выборки также должен быть достаточно большим, чтобы фактически загрузить строку в кеш перед ее использованием.
Инструкции загрузки, которые ссылаются на строки кэша, которые все еще предварительно выбираются, увеличивают счетчик событий LOAD_HIT_PRE. Так что обнаружить это довольно легко.
Процессоры Intel Core 2 имеют 4 аппаратных блока предварительной выборки.
Два для работы с кэшем второго уровня (L2) аналогично тому, как это было ранее в процессорах Pentium 4. Еще два дополнительных аппаратных блока предварительной выборки работают с кэшем данных первого уровня (L1): это блок предварительной выборки потока и блок предварительной выборки, который ищет шаблоны шагов, связанные с конкретными адресами команд (IP).
Обычно система включает в себя два устройства предварительной выборки: устройство предварительной выборки IP-адреса L2 и устройство предварительной выборки IP-адреса L1D, но не устройство предварительной выборки потока L1D. В целях анализа может быть полезно отключить эти блоки.
Обычно это можно сделать в настройках BIOS, в подразделе «Параметры процессора».
Искать следует по ключевым словам «prefetch» и «adjency» — обычно эти пункты необходимо изменить.
Вы можете использовать события производительности, чтобы определить, какие аппаратные средства предварительной выборки включены.
Событие L2_LD.SELF.PREFETCH рассматривает строки кэша, загруженные в L2 KЭSH, как блок предварительной выборки L2. Возникновение события L1D_PREFETCH.REQUESTS указывает на то, что блоки предварительной выборки данных L1 включены.
Чтобы определить количество неиспользуемых строк кэша, запрошенных аппаратным устройством предварительной выборки, запустите программу 2 раза с включенными и отключенными устройствами предварительной выборки.
Измерьте общее количество строк кэша, отправленных с событием BUS_TRANS_BURST.SELF. Если между этими двумя значениями существует значительная разница, используйте VTune Analyser, чтобы точно определить, где она встречается в исходном коде.
Частые причины этого:
- вложенные циклы с небольшим количеством итераций и большими шагами во внешних циклах;
- вложенные циклы с противоположными направлениями движения по данным (внешний цикл идет вперед с большим шагом, внутренний цикл идет назад с маленьким шагом).
Во втором случае блок предварительной выборки может попытаться загрузить уже использованные строки, то есть это будет абсолютно бесполезно.
Эта ситуация очень распространена в итерационных решателях и в большинстве случаев ее можно исправить, просто изменив направление внутреннего цикла и некоторые необходимые указатели.
В обоих случаях многие строки кэша, скорее всего, будут заняты инструкциями загрузки и выгрузки.
Это можно проверить с помощью события MEM_LOAD_RETIRED.L2_LINE_MISS, которое учитывает только загрузки, и события L2_LD.SELF.DEMAND, которое учитывает как загрузки, так и выгрузки в строке кэша, включая также запросы, сделанные механизмами предварительной выборки L1. Фрагментирование данных является стандартным решением при нехватке пропускной способности и обычно предлагается без каких-либо намеков на то, как его реализовать на самом деле.
Легче сказать, чем сделать.
Однако между декомпозицией и фрагментированием данных существует некоторая зависимость, и в некоторых случаях этим можно воспользоваться.
Декомпозиция данных может быть выполнена таким образом, чтобы существовало два уровня иерархии.
Внешний уровень предполагает разделение «одного набора данных на ядро» и одновременно на один процесс или поток, в зависимости от применяемой декомпозиции процесса подсчета, например, openmp, явная потоковая обработка или MPI. «Набор данных на ядро» может быть далее разделен в соответствии с той же стратегией на «виртуальные узлы», размер которых определяется размером последнего уровня KЭSha. Эту стратегию следует учитывать на ранних этапах разработки алгоритма, поскольку она может значительно повысить масштабируемость.
Чрезмерная загрузка дисковой подсистемы
Чрезмерная нагрузка на общий диск программы означает, что время, которое программа тратит на ожидание доступа к дисковой подсистеме, с учетом количества используемых ядер начинает доминировать над временем выполнения самого приложения.Это легко увидеть в представлении модулей в анализаторе производительности VTune, просто сравнив, сколько времени выполнения затрачивается драйвером диска при увеличении количества ядер.
Для хорошо масштабируемого приложения (фиксированный или линейно увеличивающийся объем работы) эта часть должна быть постоянной.
Конечно, на это стоит обратить внимание только в том случае, если (увеличенное) время доступа к диску существенно.
Различия в поведении означают, что несколько компонентов мешают друг другу при доступе к диску.
Это может произойти, например, из-за различных операций, вызывающих ошибочные движения головок диска, что мешает последовательному доступу, который является наиболее эффективным.
В случаях, когда доступ к диску не фиксирован, почти всегда лучше вовремя организовать его порядок, оставив оставшиеся ядра занятыми другими делами.
Таким образом, разбросав фазы доступа к диску для отдельных потоков или процессов, можно избежать чрезмерной нагрузки на дисковую подсистему.
Теги: #масштабируемость #Intel #vtune #счетчики производительности #счетчики производительности #Высокая производительность
-
Об Ассортименте
19 Oct, 24 -
Вим Крокет
19 Oct, 24