#c #c #sse #simd
#c #c #sse #simd
Вопрос:
ОБНОВЛЕНО — Проверьте ниже
Постараемся сделать это как можно короче. С удовольствием добавлю любые дополнительные подробности, если потребуется.
У меня есть некоторый sse-код для нормализации вектора. Я использую QueryPerformanceCounter() (завернутый во вспомогательную структуру) для измерения производительности.
Если я измеряю так
for( int j = 0; j < NUM_VECTORS; j )
{
Timer t(norm_sse);
NormaliseSSE( vectors_sse j);
}
Результаты, которые я получаю, часто медленнее, чем просто выполнение стандартной нормализации с 4 удвоениями, представляющими вектор (тестирование в той же конфигурации).
for( int j = 0; j < NUM_VECTORS; j )
{
Timer t(norm_dbl);
NormaliseDBL( vectors_dbl j);
}
Однако, синхронизируя только весь цикл, подобный этому
{
Timer t(norm_sse);
for( int j = 0; j < NUM_VECTORS; j ){
NormaliseSSE( vectors_sse j );
}
}
показывает, что код SSE на порядок быстрее, но на самом деле не влияет на измерения для двойной версии.
Я провел немало экспериментов и поисков и, похоже, не могу найти разумного ответа относительно того, почему.
Например, я знаю, что при приведении результатов к float могут быть штрафы, но здесь ничего подобного не происходит.
Кто-нибудь может предложить какое-либо понимание? Что такого в вызове QueryPerformanceCounter между каждой нормализацией, что так сильно замедляет работу SIMD-кода?
Спасибо за чтение 🙂
Более подробная информация ниже:
- Оба метода нормализации встроены (проверены при разборке)
- Запуск в выпуске
- 32-разрядная компиляция
Простая векторная структура
_declspec(align(16)) struct FVECTOR{
typedef float REAL;
union{
struct { REAL x, y, z, w; };
__m128 Vec;
};
};
Код для нормализации SSE:
__m128 Vec = _v->Vec;
__m128 sqr = _mm_mul_ps( Vec, Vec ); // Vec * Vec
__m128 yxwz = _mm_shuffle_ps( sqr, sqr , 0x4e );
__m128 addOne = _mm_add_ps( sqr, yxwz );
__m128 swapPairs = _mm_shuffle_ps( addOne, addOne , 0x11 );
__m128 addTwo = _mm_add_ps( addOne, swapPairs );
__m128 invSqrOne = _mm_rsqrt_ps( addTwo );
_v->Vec = _mm_mul_ps( invSqrOne, Vec );
Код для нормализации двойных
double len_recip = 1./sqrt(v->x*v->x v->y*v->y v->z*v->z);
v->x *= len_recip;
v->y *= len_recip;
v->z *= len_recip;
Вспомогательная структура
struct Timer{
Timer( LARGE_INTEGER amp; a_Storage ): Storage( a_Storage ){
QueryPerformanceCounter( amp;PStart );
}
~Timer(){
LARGE_INTEGER PEnd;
QueryPerformanceCounter( amp;PEnd );
Storage.QuadPart = ( PEnd.QuadPart - PStart.QuadPart );
}
LARGE_INTEGERamp; Storage;
LARGE_INTEGER PStart;
};
Обновить
Итак, благодаря комментариям Джона, я думаю, мне удалось подтвердить, что именно QueryPerformanceCounter плохо влияет на мой simd-код.
Я добавил новую структуру timer, которая напрямую использует RDTSC, и, похоже, она дает результаты, соответствующие тому, что я ожидал. Результат по-прежнему намного медленнее, чем синхронизация всего цикла, а не каждой итерации в отдельности, но я ожидаю, что это связано с тем, что получение RDTSC включает очистку конвейера команд (Проверьте http://www.strchr.com/performance_measurements_with_rdtsc для получения дополнительной информации).
struct PreciseTimer{
PreciseTimer( LARGE_INTEGERamp; a_Storage ) : Storage(a_Storage){
StartVal.QuadPart = GetRDTSC();
}
~PreciseTimer(){
Storage.QuadPart = ( GetRDTSC() - StartVal.QuadPart );
}
unsigned __int64 inline GetRDTSC() {
unsigned int lo, hi;
__asm {
; Flush the pipeline
xor eax, eax
CPUID
; Get RDTSC counter in edx:eax
RDTSC
mov DWORD PTR [hi], edx
mov DWORD PTR [lo], eax
}
return (unsigned __int64)(hi << 32 | lo);
}
LARGE_INTEGER StartVal;
LARGE_INTEGERamp; Storage;
};
Ответ №1:
Когда цикл выполняется только кодом SSE, процессор должен быть в состоянии поддерживать свои конвейеры заполненными и выполнять огромное количество SIMD-инструкций в единицу времени. Когда вы добавляете код таймера в цикл, теперь между каждой из простых в оптимизации операций появляется целая куча инструкций, отличных от SIMD, возможно, менее предсказуемых. Вероятно, что вызов QueryPerformanceCounter либо достаточно дорог, чтобы сделать часть манипулирования данными незначительной, либо природа выполняемого им кода наносит ущерб способности процессора продолжать выполнять инструкции с максимальной скоростью (возможно, из-за вытеснения кэша или ветвей, которые не являются хорошо прогнозируемыми).
Вы могли бы попробовать закомментировать фактические вызовы QPC в вашем классе Timer и посмотреть, как это работает — это может помочь вам определить, является ли проблема созданием и уничтожением объектов Timer или вызовами QPC. Аналогично, попробуйте просто вызвать QPC непосредственно в цикле вместо создания таймера и посмотрите, как это сравнивается.
Комментарии:
1. Эй, Джон, спасибо за твой ответ. Я опробовал ваши предложения, и, как и ожидалось, это определенно вызовы QPC, которые вызывают значительное падение производительности. Я все еще не совсем понимаю, почему это оказывает такое огромное влияние на производительность.
2. Я провел еще один тест — заменил вызов QPC вызовом другой функции (определенно не встроенной), и влияние, которое это оказывает на результат, гораздо менее значительно, чем наличие QPC там. Так что, очевидно, есть что-то особенное в вызове QueryPerformanceCounter.
3. По разным причинам QPC обычно не реализуется с помощью RDTSC. Таким образом, накладные расходы на QPC довольно высоки, и утверждение о том, что «QPC не выполняет операции с плавающей запятой», сомнительно.
Ответ №2:
QPC — это функция ядра, и ее вызов вызывает переключение контекста, что по своей сути намного дороже и разрушительнее, чем любой эквивалентный вызов функции пользовательского режима, и определенно сведет на нет способность процессора обрабатывать данные с нормальной скоростью. В дополнение к этому помните, что QPC / QPF являются абстракциями и требуют собственной обработки, которая, вероятно, предполагает использование самого SSE.
Комментарии:
1. Привет, спасибо за ваш ответ. Вы, ребята, правы, это определенно то, что делает QPC. Я действительно хотел бы понять, почему это, кажется, влияет на инструкции SIMD намного сильнее, чем стандартный код SISD. Возможно, это как-то связано с заменой между регистрами float и SIMD?