2015-12-25 1 views
3

Я запускаю код C++ для того, чтобы выполнить бенчмаркинг в наборе данных. Проблема в том, что часто я получаю время для первого запуска, и эти цифры массово меняются (т.е. от 28 секунд до 10 секунд), если я снова запустил тот же код. Я предполагаю, что это происходит из-за автоматического кэширования процессора. Есть ли способ сбросить кеш или каким-то образом предотвратить эти колебания?Промывка кеша для предотвращения флуктуаций бенчмаркинга

+1

Вы говорите о кэш-памяти процессора или кэш-памяти ОС? Дополнительные 18 секунд кажутся слишком длинными, чтобы просто заполнить кеш процессора из ОЗУ, + ошибки страниц и пропуски TLB. Даже разбросанные случайные чтения должны иметь возможность заполнять все строки кэша большого кеша L3 большого Xeon в * хорошо * до 1 секунды. –

+2

@PeterCordes По моим расчетам, если вы сделали разбросанные чтения, которые были последовательно зависимы и принимали 100 нс каждый, даже игнорируя TLB и другие затраты, вы бы искали только строки с кешем в размере ~ 2,8 МБ за 18 секунд, так что это кажется область выполнимости. – BeeOnRope

ответ

3

Не тот, который работает «за все, везде». У большинства процессоров есть специальные инструкции по очистке кеша, но они часто являются привилегированными инструкциями, поэтому их нужно делать изнутри ядра ОС, а не из кода пользовательского режима. И, конечно, это совершенно разные инструкции для каждой архитектуры процессора.

Все текущие процессоры x86 имеют инструкцию clflush, которая сбрасывает одну строку кеша, но для этого вам нужно иметь адрес данных (или кода), которые вы хотите сбросить. Это прекрасно для небольших и простых структур данных, не так хорошо, если у вас есть бинарное дерево, которое повсюду. И, конечно, совсем не портативный.

В большинстве сред считывание и запись большого блока альтернативных данных, например. что-то вроде:

// Global variables. 
const size_t bigger_than_cachesize = 10 * 1024 * 1024; 
long *p = new long[bigger_than_cachesize]; 
... 
// When you want to "flush" cache. 
for(int i = 0; i < bigger_than_cachesize; i++) 
{ 
    p[i] = rand(); 
} 

Использование rand будет гораздо медленнее, чем заполнение с чем-то постоянным/известным. Но компилятор не может оптимизировать вызов, что означает, что он (почти) гарантирует, что код останется.

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

других идеями

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

Кроме того, обычно измеряется производительность «горячих прогонов» эталона, а не первый «холодный прогон», где кеши пусты. Это делает, конечно, зависит от того, что вы на самом деле пытаетесь достичь ...

1

Вот мой основной подход:

  1. Выделяет область памяти 2x размера LLC, если вы можете определить размер LLC динамически (или вы знаете это статически), или если вы этого не сделаете, несколько разумных кратных наибольшему размеру LLC на интересующей платформе .
  2. memset область памяти к некоторому ненулевому значению: 1 будет делать все отлично.
  3. «Раковина» указателя где-нибудь, чтобы компилятор не мог оптимизировать материал выше или ниже (запись в volatile работает почти в 100% случаев).
  4. Прочитано из случайных индексов в регионе, пока вы не коснетесь каждой строки кэша в среднем 10 раз или около того (скопируйте прочитанные значения в сумму, которую вы погрузите аналогично (3)).

Вот некоторые замечания о том, почему это вообще работает, и почему меньшее число может не работать - детали являются x86-ориентированными, но подобные проблемы будут применяться и ко многим другим архитектурам.

  • Вы абсолютно хотите записи на выделенную память (шаг 2), прежде чем начать свой основной только для чтения промывочной цикла, так как в противном случае вы можете просто повторно чтение из той же маленькой zero-mapped page возвращенного ОС в удовлетворяют вашему распределению памяти.
  • Вы хотите использовать регион, значительно больший размера LLC, поскольку уровни внешнего кэша обычно физически адресуются, но вы можете распределять и получать доступ к виртуальным адресам. Если вы просто выделите регион с размером LLC, вы, как правило, не получите полного покрытия всех способов каждого набора кеша: некоторые наборы будут чрезмерно представлены (и поэтому будут полностью очищены), в то время как другие наборы будут недопредставлены и поэтому не все существующие значения даже могут быть сброшены путем доступа к этой области памяти. Перераспределение 2x делает очень вероятным, что почти все наборы имеют достаточное представление.
  • Вы хотите, чтобы оптимизатор не делал умных вещей, например, отмечая, что память никогда не ускользает от функции и не устраняет все ваши чтения и записи.
  • Вы хотите по очереди повторять вокруг области памяти, а не просто прокладывать через нее линейно: некоторые проекты, такие как LLC в недавнем Intel, обнаруживают, когда присутствует «потоковый» шаблон, и переключаются с LRU на MRU, поскольку LRU является о наименее возможной политике замены для такой нагрузки. Эффект заключается в том, что независимо от того, сколько раз вы обмениваетесь памятью, некоторые «старые» строки до ваших усилий могут оставаться в кеше. Случайный доступ к памяти поражает это поведение.
  • Вы хотите получить доступ к большему количеству ресурсов только для объема (a) по той же причине, которую вы выделяете больше, чем размер LLC (виртуальный доступ или физическое кэширование) и (b), поскольку произвольный доступ требует большего доступа, прежде чем у вас будет высокий вероятность попадания в каждый набор достаточно времени (c) кэши, как правило, только псевдо-LRU, поэтому вам нужно больше, чем количество запросов, которые вы ожидаете при точном LRU, чтобы очистить каждую строку.

Даже это не является надежным. Другие аппаратные оптимизации или поведение кэширования, не рассмотренные выше, могут привести к сбою этого подхода. Вам может быть очень не повезло с распределением страниц, предоставляемым ОС, и не сможет достичь всех страниц (вы можете в значительной степени смягчить это, используя страницы 2 МБ). Я настоятельно рекомендую проверить, является ли ваша техника флеша адекватной: один из подходов состоит в том, чтобы измерить количество промахов кэш-памяти с использованием счетчиков производительности процессора при запуске вашего теста и посмотреть, имеет ли число значение на основе известного заданного размера .

Обратите внимание, что это оставляет все уровни кеша с линиями в E (эксклюзивном) или, возможно, в S (совместно используемом) состоянии, а не в M (измененном) состоянии. Это означает, что эти строки не нужно высылать на другие уровни кэша, когда они заменяются доступом в вашем тесте: их можно просто отбросить. Подход, описанный в other answer, оставит большинство/все строки в состоянии M, так что изначально у вас будет 1 линия трафика выселения для каждой линии, к которой вы обращаетесь в своем тесте. Вы можете достичь такого же поведения с моим рецептом выше, изменив шаг 4, чтобы писать, а не читать.

В этом отношении ни один из подходов здесь не является по своей сути «лучше», чем другой: в реальном мире уровни кэша будут иметь сочетание модифицированных и немодифицированных строк, в то время как эти подходы оставляют кеш на двух крайних значениях континуум. В принципе, вы можете сравниться как с состояниями all-M, так и с no-M и посмотреть, имеет ли это значение много: если это так, вы можете попытаться оценить, какое состояние реального кэша будет обычно реплицироваться.


Помните, что размеры LLC растут почти каждое поколение процессора (в основном потому, что ядро ​​отсчеты все больше), так что вы хотите, чтобы оставить место для роста, если это должно быть будущее доказательство.

Я просто выбрасываю это, как будто это «легко», но на самом деле может быть очень сложно в зависимости от вашей точной проблемы.

 Смежные вопросы

  • Нет связанных вопросов^_^