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);
Оно работает. Однако, хотя количество итераций в циклах тестирования - nn - увеличивается, выигрыш в производительности simd-copy уменьшается:
nn = 10: усиление SSE =3, усиление AVX = 6;
nn = 100: усиление SSE = 0,75, усиление AVX = 1,5;
nn = 1000: усиление SSE = 0,55, усиление AVX = 1,1;
Кто-нибудь может объяснить, в чем причина упомянутого эффекта снижения производительности и целесообразно ли векторизацию операции копирования вручную?
5 ответов
Проблема в том, что ваш тест плохо справляется с переносом некоторых факторов в оборудование, которые затрудняют сравнительный анализ. Чтобы проверить это, я сделал свой собственный контрольный пример. Что-то вроде этого:
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% ноутбуков.
Это очень интересный вопрос, но я считаю, что ни один из ответов пока не является правильным, потому что сам вопрос настолько вводит в заблуждение.
Название должно быть изменено на "Как достичь теоретической пропускной способности памяти ввода-вывода?"
Независимо от того, какой набор команд используется, ЦП намного быстрее, чем ОЗУ, что чистая копия памяти блока ограничена на 100%. И это объясняет, почему существует небольшая разница между производительностью SSE и AVX.
Для небольших буферов, горячих в кеше L1D, AVX может копировать значительно быстрее, чем SSE, на процессорах типа Haswell, где при загрузке / хранении 256b действительно используется путь данных 256b к кэшу L1D вместо разделения на две операции 128b.
По иронии судьбы, древняя инструкция X86 Stosq работает намного лучше, чем SSE и AVX с точки зрения копирования памяти!
В этой статье объясняется, как по-настоящему насыщать пропускную способность памяти, и в ней есть богатые ссылки для дальнейшего изучения.
См. Также расширенный REP MOVSB для memcpy здесь на SO, где в ответе @BeeOnRope обсуждаются хранилища NT (и хранилища без RFO, сделанные rep stosb/stosq
) по сравнению с обычными хранилищами, и то, как пропускная способность одноядерной памяти часто ограничивается максимальным параллелизмом / задержкой, а не самим контроллером памяти.
Написание быстрых 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 Code Code Analyzer (IACA). Это инструмент статического анализа, который может сказать вам такие вещи, как "у вас есть конвейерные ларьки".
Я думаю, это потому, что измерения не точны для коротких операций.
При измерении производительности на процессоре Intel
Отключите "Turbo Boost" и "SpeedStep". Вы можете сделать это в системе BIOS.
Измените приоритет процесса / потока на высокий или в реальном времени. Это сохранит вашу нить в рабочем состоянии.
Установите Process CPU Mask только на одно ядро. Маскировка ЦП с более высоким приоритетом минимизирует переключение контекста.
используйте встроенную функцию __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;
}
Я думаю, что ваша главная проблема / узкое место это ваша _mm_malloc
,
Я настоятельно рекомендую использовать std::vector
в качестве основной структуры данных, если вы беспокоитесь о локальности в C++.
intrinsics - это не просто "библиотека", они больше похожи на встроенную функцию, предоставляемую вам от вашего компилятора, вы должны быть знакомы с внутренними документами / документами вашего компилятора перед использованием этих функций.
Также обратите внимание, что тот факт, что AVX
новее чем SSE
не делает AVX
быстрее, что бы вы ни планировали использовать, количество циклов, выполняемых функцией, вероятно, более важно, чем аргумент "avx vs sse", например, посмотрите этот ответ.
Попробуйте с POD int array[]
или std::vector
,