ARM NEON скининг

    Что это?


    Что такое ARM NEON? – ARM® NEON™ это SIMD движок … – другими словами это расширенный набор инструкций наподобие x86 CPU SSE/SSE2 но для процессоров с ARM архитектурой.

    Зачем?


    Всё и так было хорошо пока я не добавил поддержку FSAA. После этого фпс просел ниже чем 15.
    После оптимизации у меня опять было около 25 FPS. Но в памяти засела одна функция которая потребляла 10% времени на кадр в которой я уже не знал что можно оптимизировать.

    Благодаря одному моему другу, который время от времени задавал вопрос типа «А не хочешь ли ты задействовать NEON в своем движке» я таки решился (с его поддержкой) переписать эту функцию на NEON.

    Оригинальный код на C для скининга (Matrix palette skinnig).



    Структуры:

    // ready to use with glSubData for vertex buffer
    struct PN
    {
      Math::Vec3f p;
      Math::Vec3f n;
    };
    


    Трансформация одного веса:

    forceinline
    void transformPointNormal4x3Weight_NoW(const Matrix44f& mat,const Vec3f& inV, const Vec3f& inN, BaseRenderScene::PN& outPN)
    {
      outPN.p.vec[0] = (inV.vec[0]*mat.mat[0][0] + inV.vec[1]*mat.mat[1][0] + inV.vec[2]*mat.mat[2][0] + mat.mat[3][0]);
      outPN.n.vec[0] = (inN.vec[0]*mat.mat[0][0] + inN.vec[1]*mat.mat[1][0] + inN.vec[2]*mat.mat[2][0]);
    
      outPN.p.vec[1] = (inV.vec[0]*mat.mat[0][1] + inV.vec[1]*mat.mat[1][1] + inV.vec[2]*mat.mat[2][1] + mat.mat[3][1]);
      outPN.n.vec[1] = (inN.vec[0]*mat.mat[0][1] + inN.vec[1]*mat.mat[1][1] + inN.vec[2]*mat.mat[2][1]);
    
      outPN.p.vec[2] = (inV.vec[0]*mat.mat[0][2] + inV.vec[1]*mat.mat[1][2] + inV.vec[2]*mat.mat[2][2] + mat.mat[3][2]);
      outPN.n.vec[2] = (inN.vec[0]*mat.mat[0][2] + inN.vec[1]*mat.mat[1][2] + inN.vec[2]*mat.mat[2][2]);
    }
    
    forceinline
    void transformPointNormal4x3Weight(const Matrix44f& mat,const Vec3f& inV, const Vec3f& inN, BaseRenderScene::PN& outPN,float w )
    {
      outPN.p.vec[0] = (inV.vec[0]*mat.mat[0][0] + inV.vec[1]*mat.mat[1][0] + inV.vec[2]*mat.mat[2][0] + mat.mat[3][0])*w;
      outPN.n.vec[0] = (inN.vec[0]*mat.mat[0][0] + inN.vec[1]*mat.mat[1][0] + inN.vec[2]*mat.mat[2][0])*w;
    
      outPN.p.vec[1] = (inV.vec[0]*mat.mat[0][1] + inV.vec[1]*mat.mat[1][1] + inV.vec[2]*mat.mat[2][1] + mat.mat[3][1])*w;
      outPN.n.vec[1] = (inN.vec[0]*mat.mat[0][1] + inN.vec[1]*mat.mat[1][1] + inN.vec[2]*mat.mat[2][1])*w;
    
      outPN.p.vec[2] = (inV.vec[0]*mat.mat[0][2] + inV.vec[1]*mat.mat[1][2] + inV.vec[2]*mat.mat[2][2] + mat.mat[3][2])*w;
      outPN.n.vec[2] = (inN.vec[0]*mat.mat[0][2] + inN.vec[1]*mat.mat[1][2] + inN.vec[2]*mat.mat[2][2])*w;
    }
    
    forceinline
    void transformPointNormal4x3AddWeighted(const Matrix44f& mat,const Vec3f& inV, const Vec3f& inN, BaseRenderScene::PN& outPN,float w )
    {
      outPN.p.vec[0] += (inV.vec[0]*mat.mat[0][0] + inV.vec[1]*mat.mat[1][0] + inV.vec[2]*mat.mat[2][0] + mat.mat[3][0])*w;
      outPN.n.vec[0] += (inN.vec[0]*mat.mat[0][0] + inN.vec[1]*mat.mat[1][0] + inN.vec[2]*mat.mat[2][0])*w;
    
      outPN.p.vec[1] += (inV.vec[0]*mat.mat[0][1] + inV.vec[1]*mat.mat[1][1] + inV.vec[2]*mat.mat[2][1] + mat.mat[3][1])*w;
      outPN.n.vec[1] += (inN.vec[0]*mat.mat[0][1] + inN.vec[1]*mat.mat[1][1] + inN.vec[2]*mat.mat[2][1])*w;
    
      outPN.p.vec[2] += (inV.vec[0]*mat.mat[0][2] + inV.vec[1]*mat.mat[1][2] + inV.vec[2]*mat.mat[2][2] + mat.mat[3][2])*w;
      outPN.n.vec[2] += (inN.vec[0]*mat.mat[0][2] + inN.vec[1]*mat.mat[1][2] + inN.vec[2]*mat.mat[2][2])*w;
    }
    


    Трансформация одного вертекса:

    const Vec3f& vx = pVerticies[v];
    const Vec3f& vxN = pNormals[v];
    
    float w = pVertexWeight[v].vec[0];
    int boneIndex = pVertexBones[v].vec[0];
    const Matrix44f& boneTM = pBoneTMList[boneIndex];
    if( wCount==1 )
    {
      transformPointNormal4x3Weight_NoW(boneTM,vx,vxN,skinTempPN[v]);
    }
    else
    {
      // 1st vertex without add
      transformPointNormal4x3Weight_N(boneTM,vx,vxN,skinTempPN[v],w);
      for(size_t i=1;i<wCount;i++)
      {
       // other verticies
       w = pVertexWeight[v].vec[i];
       boneIndex = pVertexBones[v].vec[i];
       const Matrix44f& boneTM = pBoneTMList[boneIndex];
       transformPointNormal4x3AddWeighted_N(boneTM,vx,vxN,skinTempPN[v],w);
      }
    }
    

    Знающий человек сразу заметит, что я храню количество не нулевых весов для каждого вертекса. В моем случае около 30% вершин были с одним весом, что позволило выиграть немножко времени.

    ASM with NEON(xCode style)


    Код ниже это asm/С код оптимизированный с использованием ARM NEON.
    Несколько ньюансов:
    • Для ARM NEON все входящие данный должны быть выровнены на 16 байт. Из за этого требования я все свои входящие позиции и нормали расширил на Vec4f.
    • Исходящие данные все еще могут быть выровнены по 4 байта. Это позволило мне заливать результат напрямую в вертекс буфер без лишних телодвижений. Для варианта с выровненными данными по 16 байт пришлось бы падить данные лишними 4+4 байтами и гонять их в вертекс буффер (а это происходит на каждом кадре).



    #if defined(__ARM_NEON__)
    #define USE_NEON
    #endif
    
    #if defined(USE_NEON)
    
    
    #ifdef __thumb__
    #error "This file should be compiled in ARM mode only."
    // Note in Xcode, right click file, Get Info->Build, Other compiler flags = "-marm"
    #endif
    
    #define OP  "q0"
    
    #define OPS0  "s0"
    #define OPS1  "s1"
    #define OPS2  "s2"
    
    #define ON  "q1"
    
    #define ONS0  "s4"
    #define ONS1  "s5"
    #define ONS2  "s6"
    
    
    #define IP  "q2"
    #define IN  "q3"
    
    #define IPX "d4[0]"
    #define IPY "d4[1]"
    #define IPZ "d5[0]"
    #define IPW "d5[1]"
    
    #define INX "d6[0]"
    #define INY "d6[1]"
    #define INZ "d7[0]"
    #define INW "d7[1]"
    
    #define WQ "q4"
    #define W0D "d8[0]"
    #define W1D "d8[1]"
    #define W2D "d9[0]"
    #define W3D "d9[1]"
    
    #define QM0 q8
    #define QM1 q9
    #define QM2 q10
    #define QM3 q11
    
    #define QT  "q14"
    
    
    // outP = mt.row0*pos + mt.row1*pos + mt.row2*pos + mt.row3*pos
    #define mat_pos(_RES) \
    "vmul.f32 " _RES ", q8, " IPX "\n\t" \
    "vmla.f32 " _RES ", q9, " IPY "\n\t" \
    "vmla.f32 " _RES ", q10, " IPZ "\n\t" \
    "vmla.f32 " _RES ", q11, " IPW "\n\t"
    
    #define mat_pos_w_set(_RES,_QT,_WD) \
    mat_pos(_QT) \
    "vmul.f32 " _RES ", " _QT ", " _WD "\n\t"
    
    #define mat_pos_w_add(_RES,_QT,_WD) \
    mat_pos(_QT) \
    "vmla.f32 " _RES ", " _QT ", " _WD "\n\t"
    
    
    // outN = mt.row0*nor + mt.row1*nor + mt.row2*nor
    #define mat_nor(_RES) \
    "vmul.f32 " _RES ", q8, " INX "\n\t" \
    "vmla.f32 " _RES ", q9, " INY "\n\t" \
    "vmla.f32 " _RES ", q10, " INZ "\n\t"
    
    #define mat_nor_w_set(_RES,_QT,_WD) \
    mat_nor(_QT) \
    "vmul.f32 " _RES ", " _QT ", " _WD "\n\t"
    
    #define mat_nor_w_add(_RES,_QT,_WD) \
    mat_nor(_QT) \
    "vmla.f32 " _RES ", " _QT ", " _WD "\n\t"
    
    #define STORE3_P3N3(_R) \
    "fsts "OPS0",[" _R "] \n\t" \
    "fsts "OPS1",[" _R ",#4] \n\t" \
    "fsts "OPS2",[" _R ",#8] \n\t" \
    "fsts "ONS0",[" _R ",#12] \n\t" \
    "fsts "ONS1",[" _R ",#16] \n\t" \
    "fsts "ONS2",[" _R ",#20] \n\t"
    
    
    #define mat_load(_R) \
    "vldmia " _R ", { q8-q11 } \n\t"
    
    __attribute__((always_inline))
    void clalcSkin1(
                    const Matrix44f* mat0,
                    const Vec4f* posnorm,
                    Vec3f* outPN)
    {
      //
      asm volatile
      (
       // q4-q7 need to be preserved
       "vldmia %1, { " IP " - " IN " } \n\t"        // pos norm
       // OP p temp
       // ON n temp
       //
       // mat0
       mat_load("%0")
       mat_pos(OP)
       mat_nor(ON)
       STORE3_P3N3("%2")
       : // no output
       : "r" (mat0), "r" (posnorm), "r" (outPN)
       : "memory", IP, IN, WQ, QT, OP, ON, "q8", "q9", "q10", "q11" //clobber
       );
    }
    
    __attribute__((always_inline))
    void clalcSkin2(
                    const Matrix44f* mat0,
                    const Matrix44f* mat1,
                    const Vec4f* posnorm,
                    const Vec4f* weight,
                    Vec3f* outPN)
    {
      //
      asm volatile
      (
       // q4-q7 need to be preserved
       "vmov q15," WQ "\n\t"
       //
       "vldmia %2, { " IP " - " IN " } \n\t"        // pos norm
       "vldmia %3, { " WQ " } \n\t"     // weights
       // QT intermediate temp
       // OP p temp
       // ON n temp
       //
       // mat0
       mat_load("%0")
       mat_pos_w_set(OP,QT,W0D)
       mat_nor_w_set(ON,QT,W0D)
       // mat 1
       mat_load("%1")
       mat_pos_w_add(OP,QT,W1D)
       mat_nor_w_add(ON,QT,W1D)
       // output pos3f,norm3f
       STORE3_P3N3("%4")
       // restore q4 (WQ)
       "vmov " WQ ", q15 \n\t"
       : // no output
       : "r" (mat0), "r" (mat1), "r" (posnorm), "r" (weight), "r" (outPN)
       : "memory", IP, IN, WQ, QT , OP, ON, "q8", "q9", "q10", "q11", "q15" //clobber
       );
    }
    
    __attribute__((always_inline))
    void clalcSkin3(
                    const Matrix44f* mat0,
                    const Matrix44f* mat1,
                    const Matrix44f* mat2,
                    const Vec4f* posnorm,
                    const Vec4f* weight,
                    Vec3f* outPN)
    {
      //
      asm volatile
      (
       // q4-q7 need to be preserved
       "vmov q15," WQ "\n\t"
       //
       "vldmia %3, { " IP " - " IN " } \n\t"        // pos norm
       "vldmia %4, { " WQ " } \n\t"     // weights
       // QT intermediate temp
       // OP p temp
       // ON n temp
       //
       // mat0
       mat_load("%0")
       mat_pos_w_set(OP,QT,W0D)
       mat_nor_w_set(ON,QT,W0D)
       // mat 1
       mat_load("%1")
       mat_pos_w_add(OP,QT,W1D)
       mat_nor_w_add(ON,QT,W1D)
       // mat 2
       mat_load("%2")
       mat_pos_w_add(OP,QT,W2D)
       mat_nor_w_add(ON,QT,W2D)
       // output pos,normal
       STORE3_P3N3("%5")
       
       // restore q4 (WQ)
       "vmov " WQ ", q15 \n\t"
       
       : // no output
       : "r" (mat0), "r" (mat1), "r" (mat2),"r" (posnorm), "r" (weight), "r" (outPN)
       : "memory", IP, IN, WQ, QT, OP, ON, "q8", "q9", "q10", "q11", "q15" //clobber
       );
    }
    __attribute__((always_inline))
    void clalcSkin4(
                    const Matrix44f* mat0,
                    const Matrix44f* mat1,
                    const Matrix44f* mat2,
                    const Matrix44f* mat3,
                    const Vec4f* posnorm,
                    const Vec4f* weight,
                    Vec3f* outPN)
    {
      //
      asm volatile
      (
       // q4-q7 need to be preserved
       "vmov q15," WQ "\n\t"
       //
       "vldmia %4, { " IP " - " IN " } \n\t"        // pos norm
       "vldmia %5, { " WQ " } \n\t"     // weights
       // QT intermediate temp
       // OP p temp
       // ON n temp
       //
       // mat0
       mat_load("%0")
       mat_pos_w_set(OP,QT,W0D)
       mat_nor_w_set(ON,QT,W0D)
       // mat 1
       mat_load("%1")
       mat_pos_w_add(OP,QT,W1D)
       mat_nor_w_add(ON,QT,W1D)
       // mat 2
       mat_load("%2")
       mat_pos_w_add(OP,QT,W2D)
       mat_nor_w_add(ON,QT,W2D)
       // mat 3
       mat_load("%3")
       mat_pos_w_add(OP,QT,W3D)
       mat_nor_w_add(ON,QT,W3D)
       // output pos,normal
       STORE3_P3N3("%6")
       
       // restore q4 (WQ)
       "vmov " WQ ", q15\n\t"
       
       : // no output
       : "r" (mat0), "r" (mat1), "r" (mat2), "r" (mat3), "r" (posnorm), "r" (weight), "r" (outPN)
       : "memory", IP, IN, WQ, QT, OP, ON, "q8", "q9", "q10", "q11", "q15" //clobber
       );
    }
    
    


    Результаты


    Я не делал никаких синтетических тестов — все проверял на рабочем проекте.

    502ms(c++) против 307ms(arm neon) на ~10 секундном интервале для iPhone 4 (на 39% быстрее чем на С).

    Вопросы-Ответы


    Попытаюсь ответить сразу на несколько вопросов?

    Q: А почему не описано как работает и что такое ARM NEON?
    A: Пересказывать спеки смысла нет.

    Q: А почему не использовать шейдера?
    A: OpenGL 1.1

    Q: А почему не использовать OpenGL 2.0+?
    A: Только после портации на Windows Phone 8 (там нет FF и как раз в этот момент я допишу «шейдерность» в движок и потом уже и на GL 2.0).

    Q: А почему не использовать GL_OES_matrix_palette для FF?
    A: Надо бить модель на группы по 11 (для iphone) матриц и на это нет времени — возможно в будущем.

    Q: А где можно узнать больше и желательно с примерами?
    A: Я советую посмотреть Тут. Осторожно там LGPL.

    Q: А сколько это заняло?
    A: Неделю — именно это и есть причина написания статьи (если кому то сэкономит время я буду счастлив).

    Q: Я ничего не понял, а можно по подробнее?
    A: Можно (зависит от комментариев), но я старался написать очень понятный код.

    PS. Ошибки в личку.
    Поделиться публикацией

    Похожие публикации

    Комментарии 19

    • НЛО прилетело и опубликовало эту надпись здесь
        0
        Смотрел но не нашел.
        Можно конечно составить из нескольких вызовов простых функций но будет явно медленнее. Даже на интринсиках будет медленнее.
          0
          > Даже на интринсиках будет медленнее.

          Пробовали? Баг зафайлили?
            0
              0
              Там использовался GCC, а это совершенно другая история.
                0
                Вполне может быть — но потратить время на два варианта я себе позволить не могу. После прочтения статьи решил не рисковать и написал на asm.
          • НЛО прилетело и опубликовало эту надпись здесь
          0
          Не пробовали автоматическую векторизацию в clang? (Или её ещё нет в Apple clang?..)
            0
            Ее там нет.
              0
              В clang есть векторизация базовых блоков, что вместе с loop unrolling даёт нужный эффект. А вот есть ли она в Apple clang — это другой вопрос.
                +1
                Хмммм. Странно, что Neon есть, а компилятор для него не осилили. А если пару модулей прогнать gcc (в т.ч. до ассемблерного, а не объектного кода), а остальное родным компилятором? По-крайней мере, это могло бы занять заметно меньше недели.
                  0
                  Идея интересная — попробую.
                    0
                    Попробовал — вот исходный код для тестов
                    Вот результат — как то не уверен — совсем по другому

                    Вижуал студия 2008 для x86 смогла векторизировать — но про качество ничего не скажу

                    Командная строка:
                    arm-none-eabi-gcc.exe -O3 -S -mfloat-abi=softfp -ftree-vectorizer-verbose=4 -mfpu=neon -funsafe-math-optimizations -ftree-vectorize -c BaseRenderScene_Math2.cpp

                      +1
                      Latest clang:
                      pastebin.com/U2eEhcjY

                      Только первая функция (достаточно чтобы увидеть результат). Изменения исходного кода — добавил квалификатор restrict:
                      void transformPointNormal4x3Weight_NoW(const Matrix44f * __restrict__ pmat, const Vec4f * __restrict__ pVerticiesNormals, PN *__restrict__ poutPN)
                      {
                        const Matrix44f& mat = *pmat;
                        PN &outPN = *poutPN;
                      


                      Команды:
                      ./Release/bin/clang -cc1 -triple armv7-apple-darwin9 -target-abi aapcs  -emit-llvm -O3 /tmp/aaa.cc
                      ./Release/bin/opt -O3 -tbaa -vectorize -bb-vectorize -bb-vectorize-req-chain-depth=3 /tmp/aaa.ll -S | ./Release/bin/llc -O3
                      
                        0
                        Для меня разница очевидна между ручным и автоматическим вариантов

                          0
                          Я и не обещал идеального результата. Но ведь просто подобрать нужные опции — это не неделя работы.
                            +1
                            Согласен
                              0
                              К стати, store кусок в автоматическом варианте выглядит более оптимальным
                            0
                            Мой вариант первой функции получается таким

                            	vldmia r1, { q2 - q3 } 
                            	vldmia r0, { q8-q11 } 
                            	vmul.f32 q0, q8, d4[0]
                            	vmla.f32 q0, q9, d4[1]
                            	vmla.f32 q0, q10, d5[0]
                            	vmla.f32 q0, q11, d5[1]
                            	vmul.f32 q1, q8, d6[0]
                            	vmla.f32 q1, q9, d6[1]
                            	vmla.f32 q1, q10, d7[0]
                            	fsts s0,[r2] 
                            	fsts s1,[r2,#4] 
                            	fsts s2,[r2,#8] 
                            	fsts s4,[r2,#12] 
                            	fsts s5,[r2,#16] 
                            	fsts s6,[r2,#20] 
                            

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое