стоимость перенаправления ~ 3х умножения с плавающей точкой, правда? (с демо)

Я только что обнаружил, что косвенное обращение примерно в 3 раза больше умножения с плавающей запятой!
Чего и следовало ожидать? Мой тест неверен?

Фон

После того как я прочитал Насколько косвенность указателя влияет на эффективность?, Я начинаю паниковать по поводу стоимости косвенного обращения.

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

Прежде чем преждевременно оптимизировать свой реальный код, я хочу убедиться, что он действительно стоит дорого, как я боюсь.

Я делаю трюк, чтобы найти приблизительное число (3х), как показано ниже:

Шаг 1

  • Test1 : Без косвенности -> вычислить что-то
  • Test2 : Косвенность -> вычислить что-то (то же самое)

я нашел это Test2 занимает больше времени Test1.
Ничего удивительного здесь.

Шаг 2

  • Test1 : Без косвенности -> подсчитать что-то дорогое
  • Test2 : Косвенность

Я пытаюсь изменить свой код в calculate something expensive быть дороже понемногу, чтобы сделать оба Тестовое задание стоит примерно столько же.

Результат

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

  • Test1 : Нет косвенных -> возврат float*float*... три раза
  • Test2 : Indirection -> просто вернуть float

Вот мой тестовый пример (Ideone демо): —

class C{
public: float hello;
public: float hello2s[10];
public: C(){
hello=((double) rand() / (RAND_MAX))*10;
for(int n=0;n<10;n++){
hello2s[n]= ((double) rand() / (RAND_MAX))*10;
}
}
public: float calculateCheap(){
return hello;
}
public: float calculateExpensive(){
float result=1;
result=hello2s[0]*hello2s[1]*hello2s[2]*hello2s[3]*hello2s[4];
return result;
}
};

Вот главное:

int main(){
const int numTest=10000;
C  d[numTest];
C* e[numTest];
for(int n=0;n<numTest;n++){
d[n]=C();
e[n]=new C();
}
float accu=0;
auto t1= std::chrono::system_clock::now();
for(int n=0;n<numTest;n++){
accu+=d[n].calculateExpensive();  //direct call
}
auto t2= std::chrono::system_clock::now();
for(int n=0;n<numTest;n++){
accu+=e[n]->calculateCheap();     //indirect call
}
auto t3= std::chrono::system_clock::now();
std::cout<<"direct call time ="<<(t2-t1).count()<<std::endl;
std::cout<<"indirect call time ="<<(t3-t2).count()<<std::endl;
std::cout<<"print to disable compiler cheat="<<accu<<std::endl;
}

Время прямого звонкаи Косвенное время звонка настроен так, как указано выше (через редактирование calculateExpensive).

Заключение

Стоимость косвенного обращения = 3-кратное умножение с плавающей точкой.
В моем рабочем столе (Visual Studio 2015 с -O2) это 7x.

Стоит ли ожидать, что косвенное обращение будет стоить примерно в 3 раза больше, чем умножение с плавающей точкой?
Если нет, то как мой тест не так?

(Спасибо Enzzlek за предложение улучшения, оно отредактировано.)

1

Решение

В стоимости косвенных ссылок преобладают Cache Misses. Потому что, честно говоря, Cache Misses намного дороже, чем все остальное, о чем вы говорите, все остальное заканчивается ошибкой округления.

Пропуск кэша и косвенное обращение могут быть намного дороже, чем показывает ваш тест.

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

Вы получите кучу промахов кеша, но не по одному на каждый элемент.

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

Как видно Вот, или же это изображение (не inline, у меня нет прав), количество ссылок на основную память будет доминировать почти над всем остальным в приведенном выше коде. Процессор с тактовой частотой 2 ГГц имеет время цикла 0,5 нс, а задание основной памяти составляет 100 нс или 200 циклов задержки.

Между тем, настольный процессор может выполнять более 8 операций с плавающей запятой за цикл если вы можете выполнить векторизованный код. Это потенциально в 1600 раз быстрее операции с плавающей запятой, чем одиночная ошибка кэша.

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

Обратите внимание, что не все ЦП имеют одинаковый уровень векторизации, что прилагаются определенные усилия для ускорения задержки ЦП / основной памяти, что FPU имеют различные характеристики и множество других сложностей.

2

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

Проще говоря, ваш тест очень не репрезентативен и на самом деле не измеряет точно, что вы думаете, что он делает.

Обратите внимание, что вы звоните new C() 100’000 раз. Это создаст 100 000 экземпляров C, разбросанных по всей вашей памяти, каждый из которых очень мал. Современное оборудование очень хорошо подходит для прогнозирования, если ваш доступ к памяти является регулярным. Поскольку каждое выделение, каждый вызов new происходит независимо от других, адреса памяти не будут хорошо сгруппированы, что затруднит прогнозирование. Это приводит к так называемым ошибкам кэша.

Выделение в виде массива (new C[numTest]), скорее всего, даст совершенно другие результаты, так как в этом случае адреса снова очень предсказуемы. Группировка вашей памяти как можно ближе и доступ к ней линейным, предсказуемым образом, как правило, дает гораздо лучшую производительность. Это связано с тем, что большинство кэшей и средство предварительной выборки адресов ожидают, что именно этот шаблон встречается в обычных программах.

Незначительное добавление: инициализация, как это C d[numTest] = {}; вызовет конструктор для каждого элемента

6

На ваш вопрос нет простого ответа. Это зависит от возможностей и возможностей вашего оборудования (ЦП, ОЗУ, скорости шины и т. Д.).

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

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

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

3
По вопросам рекламы [email protected]