Доброго времени суток всем! В ходе работы над нейронными сетями встал вопрос об оптимизации алгоритма обучения однойслойного персептрона. В частности, наибольшее время занимают две функции - это умножение матрицы nR x nC на вектор nC и сложение двух матриц такой же размерности. Важно использовать тип данных double(ошибка копиться за 100-200 эпох существенная). Необходимо реализовать функции на SSE2. LjДо этого все было в классах и алгоритм школьный- так что заморочек не было Попытался написать алгоритм умножения, который пришел в голову на Си: Код (Text): const int nR = 10; const int nC = 25; _MM_ALIGN16 double *inVector, *outVector, *lentMatrix; //--------------------------------------------------------------------------- void InitializiVectorAndMatrix(int nRow, int nCol, double *ma, double *vec) { int i, j; for(i = 0; i < nRow; i++) for(j = 0; j < nCol; j++) { ma[i*nCol + j] = (double) (rand()%1000)/1000.0; vec[j] = (double) (rand()%1000)/1000.0; } } //--------------------------------------------------------------------------- void MatrixMulVectorAdvanced(int nRow, int nCol, double *ma, double *vecIn, double *vecOut) { int i, j; double x11, x12, x21, x22, x31, x32, x41, x42, y11, y12, y21, y22, y31, y32; int intNumCol_2 = 2*(nCol/2); int intNumCol_6 = 6*(nCol/6); int shift = 0; int nshift = 0; i = 0; do{ x41 = 0.0; x42 = 0.0; j = 0; shift = i*nCol; while(j < intNumCol_6){ nshift = shift + j; x11 = ma[nshift + 0]; x12 = ma[nshift + 1]; x21 = ma[nshift + 2]; x22 = ma[nshift + 3]; x31 = ma[nshift + 4]; x32 = ma[nshift + 5]; y11 = vecIn[j + 0]; y12 = vecIn[j + 1]; y21 = vecIn[j + 2]; y22 = vecIn[j + 3]; y31 = vecIn[j + 4]; y32 = vecIn[j + 5]; x41 += x11*y11; x42 += x12*y12; x41 += x21*y21; x42 += x22*y22; x41 += x31*y31; x42 += x32*y32; j+= 6; } while(j < intNumCol_2){ nshift = shift + j; x11 = ma[nshift + 0]; x12 = ma[nshift + 1]; y11 = vecIn[j + 0]; y12 = vecIn[j + 1]; x41 += x11*y11; x42 += x12*y12; j += 2; } x41 += x42; while(j < nCol){ x11 = ma[shift + j]; y11 = vecIn[j]; x41 += x11*y11; j++; } vecOut[i] = x41; }while(++i < nRow); } А потом что-то написал на assemblere...Куча ошибок и совершенно неоптимизарованно, но только час занимаюсь ассемблером. Не могли бы вы мне помочь разобраться с этим кодом? Код (Text): void MatrixMulVectorAdvancedSSE2(int nRow, int nCol, double *ma, double *vecIn, double *vecOut) { int intNumCol_2 = 2*(nCol/2); int intNumCol_6 = 6*(nCol/6); __asm{ pushad xor eax, eax ;//Counter for rows in matrix xor ebx, ebx ;//Counter for cols in matrix and rows in vector mov esi, ma ;//Load pointer to source matrix mov edi, vecIn ;//Load pointer to source vector Loop_By_Rows: ;//Cycle for rows of matrix cmp eax, nRow jnl End_Loop_By_Rows xorpd xmm6, xmm6 xor ebx, ebx mov edx, intNumCol_6 Loop_By_Col_Step_6: cmp ebx, edx jnl Loop_By_Col_Step_2 mov ecx, nCol imul ecx, eax add ecx, ebx movupd xmm0, [esi + ecx + 0h] movupd xmm1, [esi + ecx + 10h] movupd xmm2, [esi + ecx + 20h] movupd xmm3, [edi + ecx + 0] movupd xmm4, [edi + ecx + 10h] movupd xmm5, [edi + ecx + 20h] mulpd xmm0, xmm3 mulpd xmm1, xmm4 mulpd xmm2, xmm5 addpd xmm6, xmm0 addpd xmm6, xmm1 addpd xmm6, xmm2 add ebx, 6 jmp Loop_By_Col_Step_6 Loop_By_Col_Step_2: mov edx, intNumCol_2 cmp ebx, edx jnl End_Loop_By_Col_Step_2 mov ecx, nCol imul ecx, eax add ecx, ebx movupd xmm0, [esi + ecx + 0] movupd xmm3, [edi + ecx + 0] mulpd xmm0, xmm3 addpd xmm6, xmm0 add ebx, 2 jmp Loop_By_Col_Step_2 End_Loop_By_Col_Step_2: movupd xmm1, xmm6 shufpd xmm1, xmm1, 3h addsd xmm6, xmm1 Loop_By_Col_Step_1: mov edx, nCol cmp ebx, edx jnl End_Loop_By_Col_Step_1 mov ecx, nCol imul ecx, eax add ecx, ebx movsd xmm0, [esi + ecx + 0] movsd xmm3, [edi + ecx + 0] mulpd xmm0, xmm3 addpd xmm6, xmm0 inc ebx jmp Loop_By_Col_Step_1 End_Loop_By_Col_Step_1: mov edx, vecOut mov ecx, eax imul ecx, 8 movsd [edx + ecx], xmm6 inc eax jmp Loop_By_Rows End_Loop_By_Rows: popad } }
У меня траффика не хватает скачать Win C++ компилер Дизассемблировал то, что получается в MSVS компиляторе - но там и не пахнет SSE2. С другой стороны, осталось немного - просто передвижение по массиву не работает в асмвском коде корректно - что-то я упускаю из вида. Так как все равно писать dll-йку для Buildera, то надо доразбираться с кодом так...Уже крыша едет от ассемблера
Skevalt Во-во Разворот основного цикла на 6 даже при правильной реализации является избыточным, а в приведенном сишном варианте выглядит просто глупо - спрашивается куда долго и упорно грузятся 12 переменных, если регистров всего 8 ?!! Суть разворота подобных циклов заключается не столько в уменьшении накладных расходов на управление циклом (т.к.целочисленные операци могут выполняться параллельно с fpu), сколько в скрытии латентности зависимых операций fadd\addpd за счет параллельного накопления нескольких частичных сумм. Поэтому вариант SSE с точки зрения оптимизации по скорости тоже реализован не правильно, т.к. он копит только одну сумму, а нужно копить как минимум две, как в сишном варианте. И потом использование невыравненной загрузки movupd вместо выравненной movapd может свести на нет все преимущества SSE перед fpu Вариант с накоплением 2-х сумм Код (Text): push esi push edi push ebx mov esi,ma mov ebx,vecOut mov eax,nCol mov edx,eax and edx,3 xor eax,edx shl eax,3 neg eax mov esp,nRow ;!!! используется esp, в Си можно заменить на локальную переменную Loop_By_Rows: mov edi,vecIn sub edi,eax mov ecx,eax xorps xmm0,xmm0 xorps xmm1,xmm1 @@: movapd xmm2,[esi] ;на P4 movupd до 50% хуже movapd movapd xmm3,[esi+16] mulpd xmm2,[edi+ecx] mulpd xmm3,[edi+ecx+16] addpd xmm0,xmm2 addpd xmm1,xmm3 add esi,32 add ecx,32 jnz @B test edx,2 jz @F movapd xmm2,[esi] mulpd xmm2,[edi] add esi,16 add edi,16 addpd xmm0,xmm2 @@: test edx,1 jz @F movsd xmm3,[esi] mulsd xmm3,[edi] add esi,8 addsd xmm1,xmm3 @@: addpd xmm0,xmm1 movhlps xmm1,xmm0 addsd xmm1,xmm0 movq [ebx],xmm1 ;или movsd, movlpd - без разницы add ebx,8 sub esp,1 ;!!! jnz Loop_By_Rows lea esp,[ebp-4*3] ;!!! pop ebx pop edi pop esi ;ret
Тут немного другое, когда я писал на С, то подразумевал x11 и х12 страшую и младшую части регистра xmm, таким образом, я гружу всего в три регистра 6 чисел из матрицы, а в другие три регистра 6 чисел из вектора - вот откуда загрузка "12" переменных Получается, что за раз сначала перемножается 6 пар чисел, затем, в следующем цикле перемножаются по 2 пары чисел, и потом уже идет работа с одиночными парами. Развернул так, чтоб поэффективней использовать сразу все регистры(на мой взгляд) - ведь минимум происходит параллельное сложение двух чисел в старшей и младшей частях регистра. А в сишном тоже одна сумма копиться А это я откровенно ступил - долго искал, как бы выравнивать данные в MSVS (в Builder просто делал указатель с адресом кратным 16) чтобы использовать MOVAPD и в конечном итоге забыл поменять в самом коде Спасибо большое за отзывы и код - буду разбираться
Угу, но компилятор не понял твоих замыслов и затолкал все переменные в стек )) Кроме регистров нужно еще эффективно использовать порты запуска и исполнительные блоки. При длинной серии мувов блок load\AGU пыхтит от натуги, а вычислительные блоки отдыхают, хотя при чередовании операций они могли бы работать параллельно с загрузкой В твоей интерпретации может и одна, но фактически x41 и x42 это две суммы Только учти, что при нечетном nCol половина строк все равно не будет выравнена на 16 и использование movdqa приведет к ошибке - нужны доп.извращения на случай нечетного nCol
asmfan Которое до сих пор ни на одном проце эффективно не реализовано Разве что подождать аэмдэшной барселоны - они там чем-то подобным пугали...