Профилирование SIMD-кода - PullRequest
       9

Профилирование SIMD-кода

8 голосов
/ 28 апреля 2011

ОБНОВЛЕНО - проверка ниже

Будет сохранять это как можно более коротким.Рад добавить больше деталей, если требуется.

У меня есть некоторый код 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.

Iдобавлена ​​новая структура таймера, которая напрямую использует 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;
};

Ответы [ 2 ]

13 голосов
/ 28 апреля 2011

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

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

2 голосов
/ 28 апреля 2011

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

...