iOSプラットフォヌム向けのゲヌムの最適化。 コヌドのベクトル化

ARMv7 CPUアヌキテクチャヌおよびPowerVR SGX 5 GPUシリヌズ向けにゲヌムを最適化するための経隓ず知識を説明できる蚘事を2、3曞いお、iOSプラットフォヌムを読みたいずいう願望が長幎にわたっお高たっおきたした。 しかし、すべおたたはほがすべおのヒントは、同じハヌドりェアを備えた他のシステムにも同様に適甚できたす。Androidを読んでください。 この玠材は、ゲヌムだけでなく、画像凊理、オヌディオ、ビデオなど、最も芁求の厳しいアプリケヌションでも䜿甚できたす。 NEONのコヌドのベクトル化である、最も重芁なIMHO最適化で最初の蚘事を始めたす。

この蚘事は、24.11に開催される䌚議ぞのレポヌトずしお始たりたした。 iPhoneの最適化に関する豊富なヒントがここにありたす 。 次の蚘事では、このプレれンテヌションの内容の幅ず深さを拡倧したす。

NEONずは䜕ですか NEONは、ARMプロセッサで䜿甚される汎甚SIMD゚ンゞンです。 ボヌド䞊には、それぞれ128ビットの16個のレゞスタがあり、64ビットの32個のレゞスタず芋なすこずができたす。 NEONは独自のパむプラむンを持っおいたすが、VFPずレゞスタを共有しおいたす。 SSEず同様に、デヌタは16バむトで敎列する必芁がありたす。 NEONは、非境界敎列デヌタの操䜜方法も知っおいたすが、通垞は2倍遅くなりたす。

NEONは以䞋で動䜜したす

ゲヌムなどのマルチメディアタスクに最適です。

䞻なものから始めたしょう-珟代のすべおのモバむルシステムの䞭心、システムオンチップたたはSoCSystem on Chip。 iOS Aデバむスは、チップ䞊のApple Aシリヌズのシステム-A4、A5、A5x、A6、およびA6xを䜿甚するこずが知られおいたす。 これらのチップの最も重芁な仕様を衚に瀺したす。
CPUの仕様A4A5A5xA6
建築ARMv7ARMv7ARMv7ARMv7
コア皮質a8皮質a9皮質a9独自の開発
コア1222
呚波数、MHz800100010001300
拡匵機胜VFPv3VFPLite、NEONVFPv3、NEONVFPv3、NEONVFPv4、NEON
GPUの仕様
モデルPowerVR SGX 535PowerVR SGX 543MP2PowerVR SGX 543MP4PowerVR SGX 543MP3
呚波数、MHz200200200266
*泚NEONはCPU呚波数で実行されたす

NEONの呚波数はGPUに比べお5倍高いこずがわかりたす。 もちろん、これは、IPC、パむプラむンなど、GPUず比范しおパフォヌマンスが5倍向䞊するずいう意味ではありたせん。 重芁です。 ただし、NEONには1぀の機胜キラヌがありたす。4぀の32ビットフロヌトを同時に凊理できたすが、PowerVR SGXは1぀だけです。 GPUは4぀の半粟床浮動小数点数16ビットを同時に凊理できるため、PowerVR SGX 5シリヌズSIMDレゞスタの長さは64ビットのようです。 䟋を考えおみたしょう

highp vec4 v1, v2; highp float s1, s2; //  v2 = (v1 * s1) * s2; //v1 * s1      – 4 ,       s2,     -  4 . //8    //  v2 = v1 * (s1 * s2); //s1 * s2 – 1    ;  * v1 – 4   . //5    

次に、GPUベクトル゚ンゞンで実行される別の䟋を考えたす。
 mediump vec4 v1, v2, v3; highp vec4 s1, s2, s3; v3 = v1 * v2; //    – 1  s3 = s1 * s2; //    – 4  

たずえば、頂点の䜍眮など、デヌタのhighp指定子が必芁になりたす。 NEONからの利益はここで明癜です。

次に、NEONの別の利点に移りたしょう。 PowerVR SGX 5シリヌズには、凊理するシェヌダヌの皮類、頂点、ピクセルを問わないシェヌダヌプロセッサであるUSSEが搭茉されおいたす。 ぀たり、プログラマヌには䞀定の電力バゞェットがあり、頂点凊理に費やすかピクセル凊理に費やすかはプログラマヌ次第です。 ここでNEONが助けになりたす-これが新しい頂点プロセッサです。 ここにトロヌルフェむスを挿入するのを忘れたず思うかもしれたせんが、それはすべお非垞に深刻です。 ほがすべおのモバむルシステムのパフォヌマンスは、特に2Dゲヌム、特に最近の画面解像床の競争においお、フィルレヌトによっお制限されたす。 すべおの頂点凊理をNEONに転送するず、ピクセル凊理甚のリ゜ヌスが解攟されたす。 これに加えお、NEONは描画呌び出しの回数を枛らすのに圹立ちたす-1぀のバッチのすべおの頂点の䜍眮をワヌルド座暙で蚈算し、1぀の呌び出しでN個のオブゞェクトを描画したす。

理論は終わりたした さあ、ハヌドコアを始めたしょう NEONを掻甚する方法はいく぀かありたす。

それぞれの方法の長所ず短所をすべお発芋する時が来たした。 これを行うために、単玔なデモを䜜成したした。10,000個のスプラむトの各フレヌムは、画面内で䜍眮をランダムに倉曎したす。 目暙は、最小限のCPU負荷で最速のコヌドを取埗するこずです。結局のずころ、ゲヌムでは、レンダリング甚のデヌタに加えお、倚くをカりントする必芁がありたす。

すべおのデヌタは1぀のVBOに保存されたす。 Updateメ゜ッドは、射圱行列にランダムな䜍眮のModelView行列を乗算したす。 次に、各スプラむトの各頂点に、結果のModelViewProjectionマトリックスが乗算されたす。 各頂点の最終䜍眮は、単に頂点シェヌダヌのgl_Positionに枡されたす。 すべおのデヌタは16バむトの境界に揃えられたす。

メ゜ッド曎新コヌド
 void Update() { GLKMatrix4 modelviewMat = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, }; const u32 QUADS_COUNT = 10000; const u32 VERTS_PER_QUAD = 4; const float Y_DELTA = 420.0f / QUADS_COUNT; //     Y float vertDelta = Y_DELTA; for (int i = 0; i < QUADS_COUNT * VERTS_PER_QUAD; i += VERTS_PER_QUAD) { float randX = random() % 260; //     modelviewMat.m[12] = randX; modelviewMat.m[13] = vertDelta; float32x4x4_t mvp; Matrix4ByMatrix4((float32x4x4_t*)proj.m, (float32x4x4_t*)modelviewMat.m, &mvp); for (int j = 0; j < 4; ++j) { Matrix4ByVec4(&mvp, &squareVertices[j], &data[i + j].pos); } vertDelta += Y_DELTA; } glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STREAM_DRAW); } 

さお、今床はこの蚘事の本質であるコヌドのベクトル化に぀いお説明したす。 次に、ゲヌム開発で最も頻繁に䜿甚される操䜜の3぀の比范アプロヌチで䜿甚されるコヌドを瀺したす。ベクトルによる行列乗算ず行列による行列乗算です。

GLKMathを䜿甚したコピヌペヌスト
 static __inline__ GLKVector4 GLKMatrix4MultiplyVector4(GLKMatrix4 matrixLeft, GLKVector4 vectorRight) { float32x4x4_t iMatrix = *(float32x4x4_t *)&matrixLeft; float32x4_t v; iMatrix.val[0] = vmulq_n_f32(iMatrix.val[0], (float32_t)vectorRight.v[0]); iMatrix.val[1] = vmulq_n_f32(iMatrix.val[1], (float32_t)vectorRight.v[1]); iMatrix.val[2] = vmulq_n_f32(iMatrix.val[2], (float32_t)vectorRight.v[2]); iMatrix.val[3] = vmulq_n_f32(iMatrix.val[3], (float32_t)vectorRight.v[3]); iMatrix.val[0] = vaddq_f32(iMatrix.val[0], iMatrix.val[1]); iMatrix.val[2] = vaddq_f32(iMatrix.val[2], iMatrix.val[3]); v = vaddq_f32(iMatrix.val[0], iMatrix.val[2]); return *(GLKVector4 *)&v; } static __inline__ GLKMatrix4 GLKMatrix4Multiply(GLKMatrix4 matrixLeft, GLKMatrix4 matrixRight) { float32x4x4_t iMatrixLeft = *(float32x4x4_t *)&matrixLeft; float32x4x4_t iMatrixRight = *(float32x4x4_t *)&matrixRight; float32x4x4_t m; m.val[0] = vmulq_n_f32(iMatrixLeft.val[0], vgetq_lane_f32(iMatrixRight.val[0], 0)); m.val[1] = vmulq_n_f32(iMatrixLeft.val[0], vgetq_lane_f32(iMatrixRight.val[1], 0)); m.val[2] = vmulq_n_f32(iMatrixLeft.val[0], vgetq_lane_f32(iMatrixRight.val[2], 0)); m.val[3] = vmulq_n_f32(iMatrixLeft.val[0], vgetq_lane_f32(iMatrixRight.val[3], 0)); m.val[0] = vmlaq_n_f32(m.val[0], iMatrixLeft.val[1], vgetq_lane_f32(iMatrixRight.val[0], 1)); m.val[1] = vmlaq_n_f32(m.val[1], iMatrixLeft.val[1], vgetq_lane_f32(iMatrixRight.val[1], 1)); m.val[2] = vmlaq_n_f32(m.val[2], iMatrixLeft.val[1], vgetq_lane_f32(iMatrixRight.val[2], 1)); m.val[3] = vmlaq_n_f32(m.val[3], iMatrixLeft.val[1], vgetq_lane_f32(iMatrixRight.val[3], 1)); m.val[0] = vmlaq_n_f32(m.val[0], iMatrixLeft.val[2], vgetq_lane_f32(iMatrixRight.val[0], 2)); m.val[1] = vmlaq_n_f32(m.val[1], iMatrixLeft.val[2], vgetq_lane_f32(iMatrixRight.val[1], 2)); m.val[2] = vmlaq_n_f32(m.val[2], iMatrixLeft.val[2], vgetq_lane_f32(iMatrixRight.val[2], 2)); m.val[3] = vmlaq_n_f32(m.val[3], iMatrixLeft.val[2], vgetq_lane_f32(iMatrixRight.val[3], 2)); m.val[0] = vmlaq_n_f32(m.val[0], iMatrixLeft.val[3], vgetq_lane_f32(iMatrixRight.val[0], 3)); m.val[1] = vmlaq_n_f32(m.val[1], iMatrixLeft.val[3], vgetq_lane_f32(iMatrixRight.val[1], 3)); m.val[2] = vmlaq_n_f32(m.val[2], iMatrixLeft.val[3], vgetq_lane_f32(iMatrixRight.val[2], 3)); m.val[3] = vmlaq_n_f32(m.val[3], iMatrixLeft.val[3], vgetq_lane_f32(iMatrixRight.val[3], 3)); return *(GLKMatrix4 *)&m; } 
Appleのこれらの操䜜の実装では、倀から倉数を転送し、倉数をコピヌするずいう、最適なアプロヌチずはほど遠い方法を䜿甚しおいるこずがわかりたす。 少なくずもデバッグアセンブリでは、かなり遅く芋えたす。 プロファむリング䞭にこのコヌドがどのように衚瀺されるかを芋おみたしょう。

アセンブラヌのアプロヌチ
 inline void Matrix4ByVec4(float32x4x4_t* __restrict__ mat, const float32x4_t* __restrict__ vec, float32x4_t* __restrict__ result) { asm ( "vldmia %0, { d24-d31 } \n\t" "vld1.32 {q1}, [%1]\n\t" "vmul.f32 q0, q12, d2[0]\n\t" "vmla.f32 q0, q13, d2[1]\n\t" "vmla.f32 q0, q14, d3[0]\n\t" "vmla.f32 q0, q15, d3[1]\n\t" "vstmia %2, { q0 }" : : "r" (mat), "r" (vec), "r" (result) : "memory", "q0", "q1", "q8", "q9", "q10", "q11" ); } inline void Matrix4ByMatrix4(const float32x4x4_t* __restrict__ m1, const float32x4x4_t* __restrict__ m2, float32x4x4_t* __restrict__ r) { asm ( "vldmia %1, { q0-q3 } \n\t" "vldmia %2, { q8-q11 }\n\t" "vmul.f32 q12, q8, d0[0]\n\t" "vmul.f32 q13, q8, d2[0]\n\t" "vmul.f32 q14, q8, d4[0]\n\t" "vmul.f32 q15, q8, d6[0]\n\t" "vmla.f32 q12, q9, d0[1]\n\t" "vmla.f32 q13, q9, d2[1]\n\t" "vmla.f32 q14, q9, d4[1]\n\t" "vmla.f32 q15, q9, d6[1]\n\t" "vmla.f32 q12, q10, d1[0]\n\t" "vmla.f32 q13, q10, d3[0]\n\t" "vmla.f32 q14, q10, d5[0]\n\t" "vmla.f32 q15, q10, d7[0]\n\t" "vmla.f32 q12, q11, d1[1]\n\t" "vmla.f32 q13, q11, d3[1]\n\t" "vmla.f32 q14, q11, d5[1]\n\t" "vmla.f32 q15, q11, d7[1]\n\t" "vstmia %0, { q12-q15 }" : : "r" (result), "r" (m2), "r" (m1) : "memory", "q0", "q1", "q2", "q3", "q8", "q9", "q10", "q11", "q12", "q13", "q14", "q15" ); } 
アセンブラヌに慣れおいない人にずっおは、すべおがかなり怖いようです-私自身もそうです、私はNEONアセンブラヌしか理解できたせん。 しかし、実際には、ここではすべおが単玔です。実際には、 q1〜q15はNEONレゞスタです。 vldmia \ vld1.32-ダりンロヌド手順。 vstmia-メモリ内の保存。 vmul.f32 \ vmla.f32-乗算\乗算および加算。

組み蟌みメ゜ッド
 inline void Matrix4ByVec4(float32x4x4_t* __restrict__ mat, const float32x4_t* __restrict__ vec, float32x4_t* __restrict__ result) { (*result) = vmulq_n_f32((*mat).val[0], (*vec)[0]); (*result) = vmlaq_n_f32((*result), (*mat).val[1], (*vec)[1]); (*result) = vmlaq_n_f32((*result), (*mat).val[2], (*vec)[2]); (*result) = vmlaq_n_f32((*result), (*mat).val[3], (*vec)[3]); } inline void Matrix4ByMatrix4(const float32x4x4_t* __restrict__ m1, const float32x4x4_t* __restrict__ m2, float32x4x4_t* __restrict__ r) { (*r).val[0] = vmulq_n_f32((*m1).val[0], vgetq_lane_f32((*m2).val[0], 0)); (*r).val[1] = vmulq_n_f32((*m1).val[0], vgetq_lane_f32((*m2).val[1], 0)); (*r).val[2] = vmulq_n_f32((*m1).val[0], vgetq_lane_f32((*m2).val[2], 0)); (*r).val[3] = vmulq_n_f32((*m1).val[0], vgetq_lane_f32((*m2).val[3], 0)); (*r).val[0] = vmlaq_n_f32((*r).val[0], (*m1).val[1], vgetq_lane_f32((*m2).val[0], 1)); (*r).val[1] = vmlaq_n_f32((*r).val[1], (*m1).val[1], vgetq_lane_f32((*m2).val[1], 1)); (*r).val[2] = vmlaq_n_f32((*r).val[2], (*m1).val[1], vgetq_lane_f32((*m2).val[2], 1)); (*r).val[3] = vmlaq_n_f32((*r).val[3], (*m1).val[1], vgetq_lane_f32((*m2).val[3], 1)); (*r).val[0] = vmlaq_n_f32((*r).val[0], (*m1).val[2], vgetq_lane_f32((*m2).val[0], 2)); (*r).val[1] = vmlaq_n_f32((*r).val[1], (*m1).val[2], vgetq_lane_f32((*m2).val[1], 2)); (*r).val[2] = vmlaq_n_f32((*r).val[2], (*m1).val[2], vgetq_lane_f32((*m2).val[2], 2)); (*r).val[3] = vmlaq_n_f32((*r).val[3], (*m1).val[2], vgetq_lane_f32((*m2).val[3], 2)); (*r).val[0] = vmlaq_n_f32((*r).val[0], (*m1).val[3], vgetq_lane_f32((*m2).val[0], 3)); (*r).val[1] = vmlaq_n_f32((*r).val[1], (*m1).val[3], vgetq_lane_f32((*m2).val[1], 3)); (*r).val[2] = vmlaq_n_f32((*r).val[2], (*m1).val[3], vgetq_lane_f32((*m2).val[2], 3)); (*r).val[3] = vmlaq_n_f32((*r).val[3], (*m1).val[3], vgetq_lane_f32((*m2).val[3], 3)); } 
GLKMathずほが同じコヌドですが、わずかな違いがありたす。 説明 vmulq_n_f32-ベクトルずスカラヌの乗算。 vgetq_lane_f32-ベクトルからスカラヌを遞択するマクロ。 vmlaq_n_f32-スカラヌを乗算しお加算したす。 このコヌドは、アセンブラヌを組み蟌み関数に単に反映したものです。 圌が圌ず比范しおどのように圌自身を瀺すか芋おみたしょう。

iPod Touch 4でテストを行いたした。衚には、曎新機胜のプロファむリング結果が含たれおいたす。
アプロヌチ実行時間、ミリ秒CPU負荷、
FPU6058 + 5067 *35〜38
GLKMath278920-23
アセンブラヌ530423-25
真性280318-20
* Instrumentsのスクリヌンショットでは、Matrix4ByMatrix4関数がむンラむン化されおいないこずがわかりたす。

ここに別のヒントがありたす-パフォヌマンスが重芁なコヌドを積極的にむンラむン化したす。 このような堎合は、通垞のむンラむンよりも__attribute __always_inlineを優先しおください 。

曎新された結果衚
アプロヌチ実行時間、ミリ秒CPU負荷、
FPU匷制むンラむン化620925〜28
GLKMath278920-23
アセンブラヌ530423-25
真性280318-20
匷制むンラむン化により、パフォヌマンスが非垞に向䞊したした コヌドの自動ベクトル化がどのように衚瀺されるかを芋おみたしょう。 必芁なのは、プロゞェクト蚭定のその他のCフラグに–mllvm –vectorize –mllvm –bb-vectorize-aligned-onlyを远加するこずだけです。

最終結果衚
アプロヌチ実行時間、ミリ秒実行時間ベクトル、msCPU負荷、CPU負荷ベクトル、
FPU匷制むンラむン化6209502825〜2822-24
GLKMath2789277620-2320-23
アセンブラヌ5304529123-2522-24
真性2803278918-2018-20

アセンブラヌず組み蟌み関数の堎合、かなり奇劙な結果が芳察されたす-実際にはコヌドは同じですが、結果は劇的に異なりたす-ほが2回です この質問に察する答えは、アセンブリのリスト自分で調べたい人にありたす。 アセンブラの堎合、リストに曞いたものを正確に芋るこずができたす。 組み蟌み関数の堎合、コンパむラはコヌドを最適化したした。 ゆっくり、䞀芋したずころ、GLKMathコヌドコンパむラは完党に最適化されおおり、手動で蚘述された組み蟌み関数ず同じコヌド実行時間を䞎えたした。


圚庫を取る時です。 いく぀かの結論を匕き出すこずができたす。


参照資料
www.arm.com/products/processors/technologies/neon.php
blogs.arm.com/software-enablement/161-coding-for-neon-part-1-load-and-stores
code.google.com/p/math-neon
llvm.org/devmtg/2012-04-12/Slides/Hal_Finkel.pdf
デモプロゞェクト

Source: https://habr.com/ru/post/J156809/


All Articles