Как продемонстрировать влияние ограничений кэша команд

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

volatile int checksum;
void (*funcs[MAX_FUNCS])(void);

template <unsigned t>
__attribute__ ((noinline)) static void work(void) { ++checksum; }

template <unsigned t>
static void create(void) { funcs[t - 1] = &work<t - 1>; create<t - 1>(); }

template <> void create<0>(void) {  }

int main()
{
create<MAX_FUNCS>();

for (unsigned range = 1; range <= MAX_FUNCS; range *= 2)
{
checksum = 0;
for (unsigned i = 0; i < WORKLOAD; ++i)
{
funcs[i % range]();
}
}

return 0;
}

Внешний цикл изменяет количество различных функций, вызываемых с использованием таблицы переходов. Для каждого прохода цикла время, необходимое для вызова WORKLOAD функции затем измеряются. Теперь, каковы результаты? На следующей диаграмме показано среднее время выполнения для вызова функции по отношению к используемому диапазону. Синяя линия показывает данные, измеренные на компьютере Core i7. Сравнительные измерения, обозначенные красной линией, проводились на машине Pentium 4. Тем не менее, когда дело доходит до интерпретации этих строк, я, кажется, как-то борется …

диаграмма

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

Синяя кривая ведет себя совсем по-другому. Время выполнения является постоянным для небольших диапазонов и после этого увеличивается логарифмическим. Однако для больших диапазонов кривая, похоже, снова приближается к постоянной асимптоте. Как именно можно объяснить качественные различия обеих кривых?

В настоящее время я использую GCC MinGW Win32 x86 v.4.8.1 с g++ -std=c++11 -ftemplate-depth=65536 и нет оптимизации компилятора.

Любая помощь будет оценена. Меня также интересует любая идея о том, как улучшить сам эксперимент. Заранее спасибо!

6

Решение

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

  1. Вы также измеряете время прогрева. вы не показывали, где вы разместили свои проверки времени, но если это просто внутренний цикл — тогда в первый раз, пока вы не достигнете range / 2, вы все равно будете наслаждаться прогревом предыдущей внешней итерации. Вместо этого измеряйте только высокую производительность — запустите каждую внутреннюю итерацию несколько раз (добавьте еще один цикл посередине) и возьмите метку времени только после 1-2 раундов.

  2. Вы утверждаете, что измеряли несколько уровней кэша, но ваш кэш L1 составляет всего 32 Кб, где заканчивается ваш график. Даже если предположить, что это считается с точки зрения «диапазона», каждая функция составляет ~ 21 байт (по крайней мере, в моем gcc 4.8.1), так что вы достигнете не более 256 КБ, что только в этом случае уменьшает размер вашего L2.

  3. Вы не указали модель своего процессора (у i7 на рынке есть как минимум 4 поколения: Haswell, IvyBridge, SandyBridge и Nehalem). Различия довольно велики, например, дополнительный uop-кеш со времен Sandybrige со сложными правилами и условиями хранения. Ваш базовый уровень также усложняет ситуацию, если я правильно помню, что у P4 был кэш трассировки, который также может вызывать все виды влияния на производительность. Вы должны проверить опцию, чтобы отключить их, если это возможно.

  4. Не забывайте о TLB — даже если он, вероятно, не играет здесь роли в таком жестко организованном коде, количество уникальных страниц 4k не должно превышать ITLB (128 записей), и даже до этого у вас могут начаться коллизии если ваша ОС недостаточно хорошо распространила физические кодовые страницы, чтобы избежать коллизий ITLB.

1

Другие решения

Других решений пока нет …