Показать сообщение отдельно
Старый 18.02.2015, 22:55   #7
Samodelkin
Мастер
 
Регистрация: 12.01.2009
Сообщений: 979
Написано 388 полезных сообщений
(для 631 пользователей)
Ответ: dword to float4 и обратно

Итак, первую версию велосипеда я сделал. Надо сказать что SIMD оптимизацию я представлял более оптимистично. Результат конечно есть, но это не разы, это где-то +30%, и это требует значительного пересмотра логики кода и структуры данных. Для сравнения DOD (Data Oriented Design) подход дал мне больше скорости чем ассемблирование (при этом он делает код более простым и понятным, в отличие от ассемблирования). Так или иначе это первая версия кодов, а у меня особого опыта ассемблирования тоже пока нет, возможно дальнейшие улучшения дадут больше результата. Ну и я думаю что поэтапное представление модернизации кодов будет интересно, чтобы наглядно увидеть какие проблемы пришлось решать.

Значит сейчас пойдет речь как раз о извлечении dword значения цвета в формате ARGB и преобразовании его в 4D вектор в формате float4( R, G, B, A ). Вот исходный материал:
texEnvironmentColor = cpTextureArray[ 0 ]->TexNearFast( { cWallTexCoordVecU[ 0 ], cWallTexCoordVecV[ 0 ] }, cpTextureArray[ 0 ]->ComputeLvl( cRayLenArray[ 0 ], cTwoTanAlpha, cFrameBufferSizeX, cWallWidth ) );
pMem->environmentColorVec0[ 0 ] = static_cast< uint8_t >( texEnvironmentColor >> 16 ) / 255.0f;
pMem->environmentColorVec0[ 1 ] = static_cast< uint8_t >( texEnvironmentColor >> 8 ) / 255.0f;
pMem->environmentColorVec0[ 2 ] = static_cast< uint8_t >( texEnvironmentColor ) / 255.0f;
Код конечно может быть перегружен, из-за того что я вырвал его из контекста, к сожалению сейчас основная цель у меня это сам проект, поэтому я просто не успеваю подготовить какие то сравнительные тесты и т. п. что по правилам следует делать в таких ситуациях. Значит первая строка это выборка из текстуры: не нужно вдаваться в подробности выборки, главное что в переменную texEnvironmentColor сохраняется dword значение цвета. Затем в трех последующих строках делается сдвиг и урезание до одного байта чтобы получить каждое значение цвета по очереди. Тут нужно заметить что альфа мне в данном случае не нужна и я не трачусь на неё, но вообще её получают тем же образом, если она есть в текстуре четвёртым каналом. Затем происходит деление каждого значения на 255.0f. Буква f указана явно (я её вообще всегда указываю) чтобы левый операнд сначала преобразовался к float (тут идёт неявное преобразование), и затем поделился на 255, таким образом мы ограничили значение цвета в диапазоне [0.0f; 1.0f] (вроде как можно применить термин нормализация, раз это вектор). Затем мы присваиваем полученные значения вектору pMem->environmentColorVec0, который уже там выровнен должным образом и всё такое.

Вот, конечно было бы правильным посмотреть на листинг ассемблера который выходит из компилятора, чтобы посмотреть насколько он тяжёлый (или нет), но по выше описанным причинам мне надо было спешить и я просто смотрел на FPS, стал он выше или нет. Ну и по моим ощущениям показанный выше кусок довольно медленный.

Этот кусок удобно разделить на две части: разбиение одного dword на четыре dword и преобразование четырёх dword в четыре float (ну как я уже сказал альфы в данном случае нет, но в целом мы работаем с 4 компонентами). В описании к SSE я нашёл полезную инструкцию cvtdq2ps -- конвертирует 4x dword в 4x float (также есть и обратная cvtps2dq если чо). Поэтому вторую часть алгоритма я заассемблировал так:
asm( "movaps   xmm6, %1   \n\t"        
     "movaps   xmm0, %2   \n\t"
     "cvtdq2ps xmm0, xmm0 \n\t"
     "divps    xmm0, xmm6 \n\t"
     "movaps   %0,   xmm0 "

     : "=m"( pMem->environmentColorVec0 )
     : "m"( pMem->power ),
       "m"( pMem->swap ) );
В xmm6 сразу помещаем вектор из 255, достаточно один раз если не будете затирать регистр. Затем конвертируем, нормализуем и сохраняем в память. Этот участок кода однозначно работает быстрее -- я проверил.

Небольшое отступление: вот кстати есть movaps для выровненных данных, есть movups для невыровненных, и есть допустим addps у которой второй аргумент может быть прочитан из памяти, но не указано выровнен он или нет. Сразу возникает вопрос как лучше?: сначала загрузить через movaps и затем складывать или сразу пользоваться addps? (Видимо нужно смотреть мануалы к процессорам -- там должно быть написано сколько циклов занимают те или иные инструкции на разных процессорах...)

С первой же частью сложнее -- сдвиги я выполнял на регистрах общего назначения (РОН) и как мне кажется получился оверхед:
asm( "mov      eax,  %3   \n\t"
     "xor      ebx,  ebx  \n\t"
     "mov      bl,   al   \n\t"
     "mov      %0,   ebx  \n\t"
     "shr      eax,  8    \n\t"
     "mov      bl,   al   \n\t"
     "mov      %1,   ebx  \n\t"
     "shr      eax,  8    \n\t"
     "mov      bl,   al   \n\t"
     "mov      %2,   ebx  "

     : "=m"( pMem->swap[ 2 ] ),
       "=m"( pMem->swap[ 1 ] ),
       "=m"( pMem->swap[ 0 ] )
     : "r"( texEnvironmentColor )
     : "eax", "ebx" );
Этот код оказался даже чуть медленней чем у компилятора, видимо из за чрезмерной чехорды с регистрами. В общем это первое что пришло в голову и конечно надо ещё раз посмотреть на список инстуркций и найти более отпимальные варианты, а также посмотреть листинг компилятора.

Вот, ну тут я ещё буду пробовать различные варианты и впоследствии выложу (если кому-то интересно, а то может все уже знают что нужно пользоваться GPU и не создавать лишнюю энтропию в мозгах окружающих), хотя какое-то более крупномасштабное исследование с тестами и замерами я всё таки оставлю на потом, после проекта, т. к. пока необходимой скорости я частично уже добился.

Ещё есть некоторые соображения насчёт вставки ассемблера в код С/С++. В GCC используется так называемый Extended Asm, крайне неудобная и запутанная вещь. НО, как мне кажется это следствие, а не причина, потому что причина в самом факте что нужно вставлять код ассемблера в код высокоуровневого языка. Компилятор не знает что внутри вставки, а программист пишущий вставку не знает как компилятор будет компилировать код. Предугадать состояние регистрового контекста до вхождения и восстановить его при выходе очень сложно. Если включается отпимизация то практически нереально. К тому же есть всякие ограничения, например на число параметров инлайн вставки. Ящитаю что выходом из этой ситуации будет раздельная компиляция -- С++ код в своём компиляторе, ассемблер в своём (fasm или что-то подобное), затем это вместе слинковывается. В данном случае логично будет оформить асмокод ввиде функции, а в соглашении вызовов есть чёткие правила о инициализации регистров, настройке стека, "сборки мусора" при возвращении из функции и т.д. что предотвратит всякие неожиданности со стороны компилятора. К тому же писание в отдельном файле будет стимулировать создавать большие куски асмокода что минимизирует оверхед на сам вызов этой функции, и кстати наличие большого количества ёмких регистров (даже в относительно старых процессорах) даёт возможность писать по нескольку страниц кода без обращения к памяти. Ну и просто приятный синтаксис с подсветкой в IDE/редакторах созданных специально для ассемблера играет важную роль, ведь от психического состояния программиста зависит конечный результат, насколько код будет качественный.
(Offline)
 
Ответить с цитированием
Эти 3 пользователя(ей) сказали Спасибо Samodelkin за это полезное сообщение:
Igor (19.02.2015), impersonalis (19.02.2015), mr.DIMAS (19.02.2015)