Использование gcc 4.4.5 (да... я знаю это старый) на x86_64. Ограничено инструкциями SSE2 (или ранее) по соображениям совместимости.
У меня есть то, что, по моему мнению, должно быть учебником для получения больших преимуществ от предварительной выборки. У меня есть массив ( "A" ) из 32-битных элементов, которые не являются (и не могут быть) в последовательном порядке. Эти 32-битные элементы представляют собой индексы в более крупный массив данных ( "D" ) данных __m128i. Для каждого элемента "A" мне нужно получить данные __m128i из соответствующего места в "D", выполнить операцию над ним и сохранить его обратно в одно и то же место в "D". Фактически каждая "запись" в D "SOME_CONST" __m128i большая. Поэтому, если значение в равно "1", индекс в D равен D [1 * SOME_CONST].
Поскольку последовательные элементы в "A" почти никогда не указывают на последовательные местоположения в "D", я склонен думать, что аппаратный префейдер будет бороться или не сможет выполнить что-либо полезное.
Тем не менее, я могу предсказать, в каких местах я буду получать доступ к следующим очень легко, просто посмотрев вперед в "А". Достаточно словесности... вот какой-то код. Операция, которую я выполняю на данных, заключается в том, чтобы взять младшие 64-бит __m128i и клонировать ее в верхние 64-бит. Сначала основной цикл, без излишеств...
// SOME_CONST is either 3 or 4, but this "operation" only needs to happen for 3
for ( i=0; i<arraySize; ++i )
{
register __m128i *dPtr = D + (A[i] * SOME_CONST);
dPtr[0] = _mm_shuffle_epi32( dPtr[0], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr[1] = _mm_shuffle_epi32( dPtr[1], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr[2] = _mm_shuffle_epi32( dPtr[2], 0 | (1<<2) | (0<<4) | (1<<6) );
// The immediate operand selects:
// Bits 0-31 = bits 0-31
// Bits 32-63 = bits 32-63
// Bits 64-95 = bits 0-31
// Bits 96-127 = bits 32-63
// If anyone is more clever than me and knows of a better way to do this in SSE2,
// bonus points. ;-)
}
Я пробовал несколько разных способов погрузить инкрементные возможности предварительной выборки, и ни один из них не привел к каким-либо ускорениям. Например, я попытался развернуть цикл, чтобы иметь шаг 2 или даже 4 элемента, но это не помогло...
// Assume the "A" array size is appropriately padded so that overruns don't
// result in SIGSEGV accessing uninitialized memory in D.
register __m128i *dPtr0, *dPtr1, *dPtr2, *dPtr3, *dPtr4, *dPtr5, *dPtr6, *dPtr7;
dPtr4 = D + (A[0] * SOME_CONST);
dPtr5 = D + (A[1] * SOME_CONST);
dPtr6 = D + (A[2] * SOME_CONST);
dPtr7 = D + (A[3] * SOME_CONST);
for ( i=0; i<arraySize; i+=4 )
{
dPtr0 = dPtr4;
dPtr1 = dPtr5;
dPtr2 = dPtr6;
dPtr3 = dPtr7;
dPtr4 = D + (A[i+4] * SOME_CONST);
_mm_prefetch( dPtr4, _MM_HINT_NTA );
_mm_prefetch( dPtr4+1, _MM_HINT_NTA ); // Is it needed? Tried both ways
dPtr5 = D + (A[i+5] * SOME_CONST);
_mm_prefetch( dPtr5, _MM_HINT_NTA );
_mm_prefetch( dPtr5+1, _MM_HINT_NTA ); // Is it needed? Tried both ways
dPtr6 = D + (A[i+6] * SOME_CONST);
_mm_prefetch( dPtr6, _MM_HINT_NTA );
_mm_prefetch( dPtr6+1, _MM_HINT_NTA ); // Is it needed? Tried both ways
dPtr7 = D + (A[i+7] * SOME_CONST);
_mm_prefetch( dPtr7, _MM_HINT_NTA );
_mm_prefetch( dPtr7+1, _MM_HINT_NTA ); // Is it needed? Tried both ways
dPtr0[0] = _mm_shuffle_epi32( dPtr0[0], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr0[1] = _mm_shuffle_epi32( dPtr0[1], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr0[2] = _mm_shuffle_epi32( dPtr0[2], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr1[0] = _mm_shuffle_epi32( dPtr1[0], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr1[1] = _mm_shuffle_epi32( dPtr1[1], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr1[2] = _mm_shuffle_epi32( dPtr1[2], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr2[0] = _mm_shuffle_epi32( dPtr2[0], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr2[1] = _mm_shuffle_epi32( dPtr2[1], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr2[2] = _mm_shuffle_epi32( dPtr2[2], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr3[0] = _mm_shuffle_epi32( dPtr3[0], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr3[1] = _mm_shuffle_epi32( dPtr3[1], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr3[2] = _mm_shuffle_epi32( dPtr3[2], 0 | (1<<2) | (0<<4) | (1<<6) );
}
Это 4-элементная версия, но я также пробовал только с двумя, если бы было слишком много данных, которые можно было бы разворачивать. Также я попытался использовать _MM_HINT_NTA и _MM_HINT_T0. Никакой заметной разницы нет.
Я также попробовал более простой вариант, который просто пытается разместить столько пространства, которое казалось бы разумным между предварительной выборкой и когда оно будет использоваться:
#define PREFETCH_DISTANCE 10
// trying 5 overnight, will see results tomorrow...
for ( i=0; i<arraySize; ++i )
{
register __m128i *dPtrFuture, *dPtr;
dPtrFuture = D + (A[i + PREFETCH_DISTANCE] * SOME_CONST);
_mm_prefetch( dPtrFuture, _MM_HINT_NTA ); // tried _MM_HINT_T0 too
_mm_prefetch( dPtrFuture + 1, _MM_HINT_NTA ); // tried _MM_HINT_T0 too
dPtr = D + (A[i] * SOME_CONST);
dPtr[0] = _mm_shuffle_epi32( dPtr[0], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr[1] = _mm_shuffle_epi32( dPtr[1], 0 | (1<<2) | (0<<4) | (1<<6) );
dPtr[2] = _mm_shuffle_epi32( dPtr[2], 0 | (1<<2) | (0<<4) | (1<<6) );
}
Первоначально я ожидаю, что этот код остановится, но как только он получит "PREFETCH_DISTANCE" в цикле, я надеялся, что он увидит достаточно хороший прирост скорости. Большинство из этих вариантов вызывают не более 0,2 с разницы во времени выполнения в течение миллионов итераций, которые занимают общее время процессора 4 м: 30 секунд на этой конкретной машине (которая является каменным бездействием, кроме меня). Различия кажутся неотличимыми от "шума" в данных.
Правильно ли я утверждаю, что предварительная выборка должна помочь мне здесь? Если да, то что я делаю неправильно?
Приветствуются все полезные и интересные мысли.
EDIT:
Я создал надуманный пример, который действительно рандомизирует данные в A. Я играл с размерами буфера от 64 МБ до 6400 МБ. Я обнаружил, что получаю огромную выгоду от разворачивания цикла и предварительного вычисления адресов следующих 4 элементов, поскольку я выполняю свою операцию на текущем 4. Но мои баги во время выполнения в 10 раз, если я пытаюсь выполнить предварительную выборку любой из адресов, которые я предварительно вычислил. Я действительно почесываю голову на этом. Мой автономный надуманный код:
#include <xmmintrin.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#define QUEUE_ELEMENTS 1048576
#define DATA_ELEMENT_SIZE 4 * sizeof( __m128i )
#define DATA_ELEMENTS QUEUE_ELEMENTS
#define QUEUE_ITERATIONS 100000
#define LOOP_UNROLL_4
#define LOOP_UNROLL_2
#ifdef LOOP_UNROLL_4
#define UNROLL_CONST 4
#else
#ifdef LOOP_UNROLL_2
#define UNROLL_CONST 2
#else
#define UNROLL_CONST 1
#endif
#endif
int main( void )
{
unsigned long long randTemp;
unsigned long i, outerLoop;
unsigned long *workQueue;
__m128i *data, *dataOrig;
clock_t timeStamp;
workQueue = malloc( QUEUE_ELEMENTS * sizeof( unsigned long ) );
dataOrig = malloc( (DATA_ELEMENTS * DATA_ELEMENT_SIZE) + 2 );
if ( (unsigned long long) dataOrig & 0xf )
{
data = (__m128i *) (((unsigned long long) dataOrig & ~0xf) + 0x10);
// force 16-byte (128-bit) alignment
} else data = dataOrig;
// Not initializing data, because its contents isn't important.
for ( i=0; i<QUEUE_ELEMENTS; ++i )
{
randTemp = (unsigned long long)rand() *
(unsigned long long) QUEUE_ELEMENTS / (unsigned long long) RAND_MAX;
workQueue[i] = (unsigned long) randTemp;
}
printf( "Starting work...\n" );
// Actual work happening below... start counting.
timeStamp = clock();
for ( outerLoop = 0; outerLoop < QUEUE_ITERATIONS; ++outerLoop )
{
register __m128i *dataPtr0, *dataPtr1, *dataPtr2, *dataPtr3;
register __m128i *dataPtr4, *dataPtr5, *dataPtr6, *dataPtr7;
#ifdef LOOP_UNROLL_2
dataPtr4 = data + (workQueue[0] * DATA_ELEMENT_SIZE);
dataPtr5 = data + (workQueue[1] * DATA_ELEMENT_SIZE);
#endif
#ifdef LOOP_UNROLL_4
dataPtr6 = data + (workQueue[2] * DATA_ELEMENT_SIZE);
dataPtr7 = data + (workQueue[3] * DATA_ELEMENT_SIZE);
#endif
for ( i=0; i<QUEUE_ELEMENTS; i+=UNROLL_CONST )
{
#ifdef LOOP_UNROLL_2
dataPtr0 = dataPtr4;
dataPtr4 = data + (workQueue[i+4] * DATA_ELEMENT_SIZE);
// _mm_prefetch( dataPtr4, _MM_HINT_T0 );
dataPtr1 = dataPtr5;
dataPtr5 = data + (workQueue[i+5] * DATA_ELEMENT_SIZE);
// _mm_prefetch( dataPtr5, _MM_HINT_T0 );
#endif
#ifdef LOOP_UNROLL_4
dataPtr2 = dataPtr6;
dataPtr6 = data + (workQueue[i+6] * DATA_ELEMENT_SIZE);
// _mm_prefetch( dataPtr6, _MM_HINT_T0 );
dataPtr3 = dataPtr7;
dataPtr7 = data + (workQueue[i+7] * DATA_ELEMENT_SIZE);
// _mm_prefetch( dataPtr7, _MM_HINT_T0 );
#endif
#if !defined( LOOP_UNROLL_2 ) && !defined( LOOP_UNROLL_4 )
dataPtr0 = data + (workQueue[i] * DATA_ELEMENT_SIZE);
#endif
_mm_shuffle_epi32( dataPtr0[0], 0 | (1<<2) | (0<<4) | (1<<6) );
_mm_shuffle_epi32( dataPtr0[1], 0 | (1<<2) | (0<<4) | (1<<6) );
_mm_shuffle_epi32( dataPtr0[2], 0 | (1<<2) | (0<<4) | (1<<6) );
// Per original code, no need to perform operation on dataPtrx[3]
#ifdef LOOP_UNROLL_2
_mm_shuffle_epi32( dataPtr1[0], 0 | (1<<2) | (0<<4) | (1<<6) );
_mm_shuffle_epi32( dataPtr1[1], 0 | (1<<2) | (0<<4) | (1<<6) );
_mm_shuffle_epi32( dataPtr1[2], 0 | (1<<2) | (0<<4) | (1<<6) );
#endif
#ifdef LOOP_UNROLL_4
_mm_shuffle_epi32( dataPtr2[0], 0 | (1<<2) | (0<<4) | (1<<6) );
_mm_shuffle_epi32( dataPtr2[1], 0 | (1<<2) | (0<<4) | (1<<6) );
_mm_shuffle_epi32( dataPtr2[2], 0 | (1<<2) | (0<<4) | (1<<6) );
_mm_shuffle_epi32( dataPtr3[0], 0 | (1<<2) | (0<<4) | (1<<6) );
_mm_shuffle_epi32( dataPtr3[1], 0 | (1<<2) | (0<<4) | (1<<6) );
_mm_shuffle_epi32( dataPtr3[2], 0 | (1<<2) | (0<<4) | (1<<6) );
#endif
}
if ( (outerLoop % 1000) == 0 ) { putchar( '.' ); fflush( stdout ); }
}
timeStamp = clock() - timeStamp;
printf( "\nRun was %lu seconds.\n", timeStamp / CLOCKS_PER_SEC );
free( dataOrig );
free( workQueue );
return 0;
}
- Мое время выполнения с LOOP_UNROLL_2 составляет 40 секунд.
- Мое время выполнения с LOOP_UNROLL_4 составляет 20 секунд.
- Мое время без каких-либо разворачиваний составляет 80 секунд.
- Когда я включаю prefetches, время выполнения так велико, что я просто убиваю процесс, а не жду его.
Я даже написал 8-кратный развернутый цикл, и он по-прежнему отлично масштабируется до 10 секунд времени исполнения. Я поражен тем, что он не насытился, потому что в этот момент у меня наверняка закончились регистры, в которых было бы 16 уникальных указателей. Так что я могу узнать из этого? Что мой внутренний код цикла настолько туго, что он сильно омрачен накладными расходами в самой конструкции цикла? Есть ли ошибки в этом коде, которые я не вижу? Все сборки были с gcc -O2
.