Профилирование 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 на порядок быстрее, но на самом деле не влияет на измерения для двойной версии. Я провел немало экспериментов и поиска и, похоже, не нашел разумного ответа на вопрос, почему.

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

Может ли кто-нибудь предложить какое-либо понимание? Что такого в вызове 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 & a_Storage ): Storage( a_Storage ){
      QueryPerformanceCounter( &PStart );
  }

  ~Timer(){
    LARGE_INTEGER PEnd;
    QueryPerformanceCounter( &PEnd );
    Storage.QuadPart += ( PEnd.QuadPart - PStart.QuadPart );
  }

  LARGE_INTEGER& Storage;
  LARGE_INTEGER PStart;
};

Обновление Итак, благодаря комментариям Джонса, я думаю, мне удалось подтвердить, что именно QueryPerformanceCounter делает плохие вещи с моим кодом simd.

Я добавил новую структуру таймера, которая напрямую использует RDTSC, и, кажется, она дает результаты, соответствующие моим ожиданиям. Результат все еще намного медленнее, чем синхронизация всего цикла, а не каждой итерации в отдельности, но я ожидаю, что это связано с тем, что получение RDTSC включает сброс конвейера инструкций (см. http://www.strchr.com/performance_measurements_with_rdtsc для получения дополнительной информации).

struct PreciseTimer{

    PreciseTimer( LARGE_INTEGER& 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_INTEGER& Storage;
};

person JBeFat    schedule 28.04.2011    source источник


Ответы (2)


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

Вы можете попробовать закомментировать фактические вызовы QPC в своем классе Timer и посмотреть, как он работает — это может помочь вам выяснить, является ли проблема созданием и уничтожением объектов Timer или вызовами QPC. Точно так же попробуйте просто вызвать QPC непосредственно в цикле вместо создания таймера и посмотрите, как это сравнивается.

person John Zwinck    schedule 28.04.2011
comment
Привет, Джон, спасибо за твой ответ. Я опробовал ваши предложения, и, как и ожидалось, именно вызовы QPC вызывают значительное падение производительности. Я до сих пор не совсем понимаю, почему это так сильно влияет на производительность. - person JBeFat; 28.04.2011
comment
Я провел еще один тест - заменив вызов QPC другим вызовом функции (определенно не встроенной), и влияние, которое он оказывает на результат, гораздо менее существенно, чем наличие QPC. Очевидно, что в вызове QueryPerformanceCounter есть что-то особенное. - person JBeFat; 28.04.2011
comment
По разным причинам QPC обычно не реализуется с RDTSC. Таким образом, накладные расходы QPC довольно высоки, и утверждение, что QPC не работает с плавающей запятой, сомнительно. - person rwong; 28.04.2011

QPC — это функция ядра, и ее вызов вызывает переключение контекста, что по своей сути гораздо более затратно и разрушительно, чем любой эквивалентный вызов функции пользовательского режима, и определенно аннулирует способность процессора выполнять обработку с нормальной скоростью. В дополнение к этому помните, что QPC/QPF являются абстракциями и требуют собственной обработки, что, вероятно, связано с использованием самого SSE.

person Puppy    schedule 28.04.2011
comment
Привет, спасибо за ваш ответ. Вы, ребята, правы, это определенно то, что делает QPC. Я действительно хотел бы понять, почему это, кажется, влияет на инструкции SIMD гораздо больше, чем стандартный код SISD. Возможно, это как-то связано с переключением между регистрами с плавающей запятой и SIMD? - person JBeFat; 29.04.2011