Это — вольный перевод моего недавнего поста на английской версии Intel Software Network. Так что те, кому Victoria Zhislina нравится больше vikky13, кто уже видел этот пост, могут сразу прочесть первый и последний абзацы, отсутствующие в оригинале.
— Всем здрасьте, мне нужен транслятор с русского языка в код программы на C++. Ну то есть, я пишу задачу, а транслятор реализует ее решение на языке С++. Где можно такой найти? Если для Cи нету, может быть, есть для других языков?
— Есть, называется начальник отдела разработки. Пишешь задачу на русском — отдаешь подчиненным и все, код готов! Хоть на Си, хоть на Дельфи, хоть на Яве. Я проверял, работает!
Говорят, что это не анекдот, а реальный вопрос на программистском форуме. Также говорят, что человек гораздо умнее машины, а значит, может ей помочь — поделиться умом. Но есть немало случаев, когда делать этого точно не стоит. Результат будет обратный ожидаемому.
Вот наглядный пример из известной open source библиотеки OpenCV:
Это — простая функция-шаблон, работающая с char, short, float и double.
Ее авторы решили помочь компилятору с SSE-векторизацией, развернув внутренний цикл по 4 и обрабатывая оставшийся хвост данных отдельно.
Думаете, современные компиляторы (под Windows) сгенеруют оптимизированный код в соответствии с замыслом авторов?
Давайте проверим, скомпилировав этот код с помощью Intel Compiler 12.0, с ключом /QxSSE2 (проверено, что использование других SSEx и AVX опций даст такой же результат)
А результат будет довольно неожиданный. Ассемблерный листинг на выходе компилятора неопровержимо показывает, что развернутый цикл НЕ векторизуется. Компилятор генерирует SSE инструкции, но только скалярные, а не векторные. Зато остаток данных — «хвост», содержащий всего 1-3 элемента данных в неразвернутом цикле, векторизуется по полной программе!
Если мы уберем разворачивание цикла:
… и снова посмотрим на ассемблер (вас я пугать им не буду), то обнаружим, что теперь цикл полностью векторизован для всех типов данных, что несомненно увеличивает производительность.
Вывод: Больше работы — меньше производительность. Меньше работы — больше. Вот бы всегда так.
Заметим, что Microsoft Compiler, Visual Studio 2010 и 2008 с ключом /arch:SSE2 НЕ векторизует вышеприведенный код ни в развернутом ни в свернутом виде. Код, им произведенный, очень похож и по виду и по производительности в обоих случаях. То есть, если для компилятора Intel развертывание цикла — вредно, то для Microsoft — просто бесполезно :).
А что если вы все же хотите сохранить развертывание цикла —оно дорого вам как память, но и векторизацию тоже хотите?
Тогда используйте прагмы компилятора Intel как показано ниже:
#pragma simd
#pragma novector
И последнее. Само по себе разворачивание циклов может положительно повлиять на производительность. Но, во-первых, возможный выигрыш от векторизации все равно превысит это положительное влияние, а, во-вторых, разворачивание можно поручить компилятору, тогда и векторизация от этого не пострадает. В числе прочего планирую затронуть эту тему на вебинаре 27 октября.
— Всем здрасьте, мне нужен транслятор с русского языка в код программы на C++. Ну то есть, я пишу задачу, а транслятор реализует ее решение на языке С++. Где можно такой найти? Если для Cи нету, может быть, есть для других языков?
— Есть, называется начальник отдела разработки. Пишешь задачу на русском — отдаешь подчиненным и все, код готов! Хоть на Си, хоть на Дельфи, хоть на Яве. Я проверял, работает!
Говорят, что это не анекдот, а реальный вопрос на программистском форуме. Также говорят, что человек гораздо умнее машины, а значит, может ей помочь — поделиться умом. Но есть немало случаев, когда делать этого точно не стоит. Результат будет обратный ожидаемому.
Вот наглядный пример из известной open source библиотеки OpenCV:
cvtScale_( const Mat& srcmat, Mat& dstmat, double _scale, double _shift )
{
Op op;
typedef typename Op::type1 WT;
typedef typename Op::rtype DT;
Size size = getContinuousSize( srcmat, dstmat, srcmat.channels() );
WT scale = saturate_cast<WT>(_scale), shift = saturate_cast<WT>(_shift);
for( int y = 0; y < size.height; y++ )
{
const T* src = (const T*)(srcmat.data + srcmat.step*y);
DT* dst = (DT*)(dstmat.data + dstmat.step*y);
int x = 0;
for(; x <= size.width - 4; x += 4 )
{
DT t0, t1;
t0 = op(src[x]*scale + shift);
t1 = op(src[x+1]*scale + shift);
dst[x] = t0; dst[x+1] = t1;
t0 = op(src[x+2]*scale + shift);
t1 = op(src[x+3]*scale + shift);
dst[x+2] = t0; dst[x+3] = t1;
}
for( ; x < size.width; x++ )
dst[x] = op(src[x]*scale + shift);
}
}
Это — простая функция-шаблон, работающая с char, short, float и double.
Ее авторы решили помочь компилятору с SSE-векторизацией, развернув внутренний цикл по 4 и обрабатывая оставшийся хвост данных отдельно.
Думаете, современные компиляторы (под Windows) сгенеруют оптимизированный код в соответствии с замыслом авторов?
Давайте проверим, скомпилировав этот код с помощью Intel Compiler 12.0, с ключом /QxSSE2 (проверено, что использование других SSEx и AVX опций даст такой же результат)
А результат будет довольно неожиданный. Ассемблерный листинг на выходе компилятора неопровержимо показывает, что развернутый цикл НЕ векторизуется. Компилятор генерирует SSE инструкции, но только скалярные, а не векторные. Зато остаток данных — «хвост», содержащий всего 1-3 элемента данных в неразвернутом цикле, векторизуется по полной программе!
Если мы уберем разворачивание цикла:
for( int y = 0; y < size.height; y++ )
{
const T* src = (const T*)(srcmat.data + srcmat.step*y);
DT* dst = (DT*)(dstmat.data + dstmat.step*y);
int x = 0;
for( ; x < size.width; x++ )
dst[x] = op(src[x]*scale + shift);
}
… и снова посмотрим на ассемблер (вас я пугать им не буду), то обнаружим, что теперь цикл полностью векторизован для всех типов данных, что несомненно увеличивает производительность.
Вывод: Больше работы — меньше производительность. Меньше работы — больше. Вот бы всегда так.
Заметим, что Microsoft Compiler, Visual Studio 2010 и 2008 с ключом /arch:SSE2 НЕ векторизует вышеприведенный код ни в развернутом ни в свернутом виде. Код, им произведенный, очень похож и по виду и по производительности в обоих случаях. То есть, если для компилятора Intel развертывание цикла — вредно, то для Microsoft — просто бесполезно :).
А что если вы все же хотите сохранить развертывание цикла —
Тогда используйте прагмы компилятора Intel как показано ниже:
#pragma simd
for(x=0; x <= size.width - 4; x += 4 )
{
DT t0, t1;
t0 = op(src[x]*scale + shift);
t1 = op(src[x+1]*scale + shift);
dst[x] = t0; dst[x+1] = t1;
t0 = op(src[x+2]*scale + shift);
t1 = op(src[x+3]*scale + shift);
dst[x+2] = t0; dst[x+3] = t1;
}
#pragma novector
for( ; x <size.width; x++ )
dst[x] = op(src[x]*scale + shift);
}
И последнее. Само по себе разворачивание циклов может положительно повлиять на производительность. Но, во-первых, возможный выигрыш от векторизации все равно превысит это положительное влияние, а, во-вторых, разворачивание можно поручить компилятору, тогда и векторизация от этого не пострадает. В числе прочего планирую затронуть эту тему на вебинаре 27 октября.