Быстрое копирование видеопамяти. Параллелизация копирования

Dmitry Serkin (Intel) (24 пост(а)) 06.07.2009 12:48

Долго не решался опубликовать данную заметку.  Слишком много подводных камней и предположений с которыми еще предстоит разобраться. Но думаю все же некоторым будет интересно.

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

На каждый исполнительный юнит приходится всего по 4 буфера. Процессор использует их постоянно, загружая и выгружая данные во все уровни кэша и DRAM память. Поэтому если попытаться разделить регион копируемой памяти на несколько частей и каждую из частей копировать в отдельном потоке, то  эффект не заставит себя ждать. В итоге, наращивая количество потоков, производительность упрется в пропускную способность подсистемы памяти.

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

Из графика видно, что на 4-5 потоках синтетический тест достигает своей пиковой производительности.

На тестовой машине установлена  3х-канальная DDR3 память с пиковой нагрузкой в 32 Гб/c. Вряд ли мы ее превысили. С большей степенью вероятности, причина спада производительности на большем количестве потоков заключается в том, что наш Core i7 содержит всего 4 физических ядра. И уже при больше, чем на 4 потоках, они начинают делить между собой ресурсы процессора, вызывая тем самым нежелательные кэш эффекты.

Хочется обсудить способ определения пропускной способности памяти. Мы написали маленькую программу для определения пропускной способности памяти на чтение и на запись. В цикле происходит «холостое» чтение из памяти или запись в память.

enum
{
BLOCKSIZE                   = 1024 * 1024 * 256,
ITER                        = 128,
ALIGN                       = 64
};

typedef unsigned char byte;

int main(int argc, char **argv)
{
byte *mem, *pBuf = new byte [BLOCKSIZE + ALIGN];
LARGE_INTEGER start, stop, freq;
__int64 temp;
int timePerBlock;
int mbPerSec;

SetThreadAffinityMask(GetCurrentThread(), 2);

mem = (byte *) ((((size_t) pBuf) + ALIGN - 1) & -ALIGN);
memset(mem, 0, BLOCKSIZE);

// start timing
QueryPerformanceCounter(&start);

for (int i = 0; i < ITER; i++)
{

__asm
{
mov ecx, BLOCKSIZE
mov esi, dword ptr [mem]
pxor xmm0, xmm0

jmp NEXT

ALIGN 10h
RESTART_LOOP:
#if 0
// read speed test
movdqa xmm0, oword ptr [esi + 000h]
movdqa xmm1, oword ptr [esi + 010h]
movdqa xmm2, oword ptr [esi + 020h]
movdqa xmm3, oword ptr [esi + 030h]
movdqa xmm4, oword ptr [esi + 040h]
movdqa xmm5, oword ptr [esi + 050h]
movdqa xmm6, oword ptr [esi + 060h]
movdqa xmm7, oword ptr [esi + 070h]
movdqa xmm0, oword ptr [esi + 080h]
movdqa xmm1, oword ptr [esi + 090h]
movdqa xmm2, oword ptr [esi + 0a0h]
movdqa xmm3, oword ptr [esi + 0b0h]
movdqa xmm4, oword ptr [esi + 0c0h]
movdqa xmm5, oword ptr [esi + 0d0h]
movdqa xmm6, oword ptr [esi + 0e0h]
movdqa xmm7, oword ptr [esi + 0f0h]
#else
// write speed test
movntdq oword ptr [esi + 000h], xmm0
movntdq oword ptr [esi + 010h], xmm0
movntdq oword ptr [esi + 020h], xmm0
movntdq oword ptr [esi + 030h], xmm0
movntdq oword ptr [esi + 040h], xmm0
movntdq oword ptr [esi + 050h], xmm0
movntdq oword ptr [esi + 060h], xmm0
movntdq oword ptr [esi + 070h], xmm0
movntdq oword ptr [esi + 080h], xmm0
movntdq oword ptr [esi + 090h], xmm0
movntdq oword ptr [esi + 0a0h], xmm0
movntdq oword ptr [esi + 0b0h], xmm0
movntdq oword ptr [esi + 0c0h], xmm0
movntdq oword ptr [esi + 0d0h], xmm0
movntdq oword ptr [esi + 0e0h], xmm0
movntdq oword ptr [esi + 0f0h], xmm0
#endif

add esi, 100h
sub ecx, 100h

NEXT:
cmp ecx, 0
jne RESTART_LOOP
}

}

// finish timing
QueryPerformanceCounter(&stop);

QueryPerformanceFrequency(&freq);

timePerBlock = (int) ((stop.QuadPart - start.QuadPart) * 1000 * 10 / (freq.QuadPart * ITER));
printf("msec : %d.%d\n", timePerBlock / 10, timePerBlock % 10);

temp = ITER * (BLOCKSIZE / (1024 * 1024));
temp *= freq.QuadPart;
temp /= (stop.QuadPart - start.QuadPart);
mbPerSec = (int) temp;
printf("mb/s: %d\n", mbPerSec);

delete [] pBuf;

return 0;
}

Таким образом можно вычислить фактическую пропускную способность памяти. Мне хочется верить в это ?. На тестовой конфигурации получаем 7 gb/s на запись и 11 gb/s на чтение.

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

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

Категории: Intel Software Network, Графика, Параллельное программирование, Разработка софта
Метки: ,

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

Комментарии (0)

Обратная ссылка (0)


Оставить комментарий  

Для получения технической помощи посетите сайт службы поддержки.
Имя (обязательно)*

Электронная почта (обязательно; не будет отображено на этой странице)*

Ваш URL-адрес (необязательно)


Комментарий*