SSE-copy, AVX-copy и std :: copy производительность

Я пытался улучшить производительность операции копирования через SSE и AVX:

    #include <immintrin.h>

const int sz = 1024;
float *mas = (float *)_mm_malloc(sz*sizeof(float), 16);
float *tar = (float *)_mm_malloc(sz*sizeof(float), 16);
float a=0;
std::generate(mas, mas+sz, [&](){return ++a;});

const int nn = 1000;//Number of iteration in tester loops
std::chrono::time_point<std::chrono::system_clock> start1, end1, start2, end2, start3, end3;

//std::copy testing
start1 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
std::copy(mas, mas+sz, tar);
end1 = std::chrono::system_clock::now();
float elapsed1 = std::chrono::duration_cast<std::chrono::microseconds>(end1-start1).count();

//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=4, _tar+=4)
{
__m128 buffer = _mm_load_ps(_mas);
_mm_store_ps(_tar, buffer);
}
}
end2 = std::chrono::system_clock::now();
float elapsed2 = std::chrono::duration_cast<std::chrono::microseconds>(end2-start2).count();

//AVX-copy testing
start3 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=8, _tar+=8)
{
__m256 buffer = _mm256_load_ps(_mas);
_mm256_store_ps(_tar, buffer);
}
}
end3 = std::chrono::system_clock::now();
float elapsed3 = std::chrono::duration_cast<std::chrono::microseconds>(end3-start3).count();

std::cout<<"serial - "<<elapsed1<<", SSE - "<<elapsed2<<", AVX - "<<elapsed3<<"\nSSE gain: "<<elapsed1/elapsed2<<"\nAVX gain: "<<elapsed1/elapsed3;

_mm_free(mas);
_mm_free(tar);

Оно работает. Однако, хотя количество итераций в циклах тестера — пп — увеличивается, прирост производительности simd-копии уменьшается:

nn = 10: усиление SSE = 3, усиление AVX = 6;

nn = 100: усиление SSE = 0,75, усиление AVX = 1,5;

nn = 1000: усиление SSE = 0,55, усиление AVX = 1,1;

Кто-нибудь может объяснить, в чем причина упомянутого эффекта снижения производительности и целесообразно ли векторизацию операции копирования вручную?

16

Решение

Проблема в том, что ваш тест плохо справляется с переносом некоторых факторов в оборудование, которые затрудняют сравнительный анализ. Чтобы проверить это, я сделал свой собственный контрольный пример. Что-то вроде этого:

for blah blah:
sleep(500ms)
std::copy
sse
axv

выход:

SSE: 1.11753x faster than std::copy
AVX: 1.81342x faster than std::copy

Так что в этом случае AVX намного быстрее, чем std::copy, Что произойдет, когда я перейду на тестовый набор к …

for blah blah:
sleep(500ms)
sse
axv
std::copy

Обратите внимание, что абсолютно ничего не изменилось, кроме порядка тестов.

SSE: 0.797673x faster than std::copy
AVX: 0.809399x faster than std::copy

Ого! как это возможно? Процессору требуется некоторое время, чтобы разогнаться до полной скорости, поэтому последующие тесты имеют преимущество. На этот вопрос теперь есть 3 ответа, включая «принятый» ответ. Но только тот с самым низким количеством голосов был на правильном пути.

Это одна из причин, по которым тестирование сложно, и вы никогда не должны доверять чьим-либо микро-тестам, если они не включили подробную информацию о своих настройках. Это не просто код, который может пойти не так. Функции энергосбережения и странные драйверы могут полностью испортить ваш тест. Однажды я измерил разницу в производительности в 7 раз, переключив биос, который предлагают менее 1% ноутбуков.

22

Другие решения

Это очень интересный вопрос, но Я считаю, что ни один из ответов пока не является правильным, потому что сам вопрос настолько вводит в заблуждение.

Название должно быть изменено на «Как достичь теоретической пропускной способности памяти ввода / вывода?»

Независимо от того, какой набор инструкций используется, ЦП намного быстрее, чем ОЗУ, что чистая копия памяти блока ограничена на 100%. И это объясняет, почему существует небольшая разница между производительностью SSE и AVX.

Для небольших буферов, горячих в кеше L1D, AVX может копировать значительно быстрее, чем SSE, на таких процессорах, как Haswell, где 256-битные загрузки / хранилища действительно используют 256-канальный путь данных к L1D-кэшу вместо разделения на две 128-битные операции.

Как ни странно, древняя инструкция X86 представитель Stosq работает намного лучше, чем SSE и AVX с точки зрения копирования памяти!

Статья здесь объясняет, как по-настоящему насыщать пропускную способность памяти, а также содержит подробные ссылки для дальнейшего изучения.

Смотрите также Улучшенный REP MOVSB ​​для memcpy здесь, на SO, где в ответе @ BeeOnRope обсуждаются магазины NT (и магазины без RFO, сделанные rep stosb/stosq) по сравнению с обычными хранилищами, и то, как пропускная способность одноядерной памяти часто ограничивается максимальным параллелизмом / задержкой, а не самим контроллером памяти.

5

Я думаю, это потому, что измерения не точны для коротких операций.

При измерении производительности на процессоре Intel

  1. Отключите «Turbo Boost» и «SpeedStep». Вы можете сделать это в системе BIOS.

  2. Измените приоритет процесса / потока на высокий или в реальном времени. Это сохранит вашу нить в рабочем состоянии.

  3. Установите Process CPU Mask только на одно ядро. Маскировка ЦП с более высоким приоритетом минимизирует переключение контекста.

  4. используйте встроенную функцию __rdtsc (). Серия Intel Core возвращает внутренний тактовый счетчик процессора с помощью __rdtsc (). Вы получите 3400000000 отсчетов в секунду от 3,4 ГГц процессора. И __rdtsc () сбрасывает все запланированные операции в CPU, чтобы он мог более точно измерять время.

Это мой стартовый код для тестирования SSE / AVX.

    int GetMSB(DWORD_PTR dwordPtr)
{
if(dwordPtr)
{
int result = 1;
#if defined(_WIN64)
if(dwordPtr & 0xFFFFFFFF00000000) { result += 32; dwordPtr &= 0xFFFFFFFF00000000; }
if(dwordPtr & 0xFFFF0000FFFF0000) { result += 16; dwordPtr &= 0xFFFF0000FFFF0000; }
if(dwordPtr & 0xFF00FF00FF00FF00) { result += 8;  dwordPtr &= 0xFF00FF00FF00FF00; }
if(dwordPtr & 0xF0F0F0F0F0F0F0F0) { result += 4;  dwordPtr &= 0xF0F0F0F0F0F0F0F0; }
if(dwordPtr & 0xCCCCCCCCCCCCCCCC) { result += 2;  dwordPtr &= 0xCCCCCCCCCCCCCCCC; }
if(dwordPtr & 0xAAAAAAAAAAAAAAAA) { result += 1; }
#else
if(dwordPtr & 0xFFFF0000) { result += 16; dwordPtr &= 0xFFFF0000; }
if(dwordPtr & 0xFF00FF00) { result += 8;  dwordPtr &= 0xFF00FF00; }
if(dwordPtr & 0xF0F0F0F0) { result += 4;  dwordPtr &= 0xF0F0F0F0; }
if(dwordPtr & 0xCCCCCCCC) { result += 2;  dwordPtr &= 0xCCCCCCCC; }
if(dwordPtr & 0xAAAAAAAA) { result += 1; }
#endif
return result;
}
else
{
return 0;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
// Set Core Affinity
DWORD_PTR processMask, systemMask;
GetProcessAffinityMask(GetCurrentProcess(), &processMask, &systemMask);
SetProcessAffinityMask(GetCurrentProcess(), 1 << (GetMSB(processMask) - 1) );

// Set Process Priority. you can use REALTIME_PRIORITY_CLASS.
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);

DWORD64 start, end;
start = __rdtsc();
// your code here.
end = __rdtsc();
printf("%I64d\n", end - start);
return 0;
}
3

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

Вы можете проверить эту идею, вручную развернув одну ступеньку:

//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=8, _tar+=8)
{
__m128 buffer1 = _mm_load_ps(_mas);
__m128 buffer2 = _mm_load_ps(_mas+4);
_mm_store_ps(_tar, buffer1);
_mm_store_ps(_tar+4, buffer2);
}
}

Обычно при использовании встроенных функций я разбираю вывод и убеждаюсь, что ничего сумасшедшего не происходит (вы можете попробовать это, чтобы убедиться, что / как исходный цикл был развернут). Для более сложных циклов правильным инструментом является Анализатор кода архитектуры Intel (IACA). Это инструмент статического анализа, который может сказать вам такие вещи, как «у вас есть трубопроводные киоски».

3

Я думаю, что ваша главная проблема / узкое место это ваша _mm_malloc.

Я настоятельно рекомендую использовать std::vector в качестве основной структуры данных, если вы беспокоитесь о локальности в C ++.

встроенные функции не совсем «библиотека», они больше похожи на встроенная Функция, предоставляемая вам от вашего компилятора, вы должны быть знакомы с внутренними документами / документами вашего компилятора перед использованием этой функции.

Также обратите внимание, что тот факт, что AVX новее чем SSE не делает AVX быстрее, что бы вы ни планировали использовать, количество циклов, выполняемых функцией, вероятно, более важно, чем аргумент «avx vs sse», например, см. этот ответ.

Попробуйте с POD int array[] или std::vector,

1