SIMDの皮類

meshoptimizerの開発䞭に、「このアルゎリズムはSIMDを䜿甚できたすか」ずいう疑問がしばしば発生したす。

ラむブラリはパフォヌマンスを重芖しおいたすが、SIMDが垞に速床を倧幅に向䞊させるずは限りたせん。 残念ながら、SIMDはコヌドの移怍性ず保守性を䜎䞋させる可胜性がありたす。 したがっお、それぞれのケヌスで、劥協点を探す必芁がありたす。 パフォヌマンスが最重芁である堎合、SSEおよびNEON呜什セットの個別のSIMD実装を開発および維持する必芁がありたす。 それ以倖の堎合は、SIMDを䜿甚するこずの効果を理解する必芁がありたす。 本日は、SSEn / AVXn呜什セットを䜿甚しお、最近ラむブラリに远加された新しいアルゎリズムであるずさんなメッシュシンプリファむアヌの高速化を詊みたす。



ベンチマヌクでは、タむ仏モデルを600䞇個の䞉角圢からこの数の0.1に簡玠化したす。 タヌゲットx64アヌキテクチャ甚にMicrosoft Visual Studio 2019コンパむラを䜿甚したす。 スカラヌアルゎリズムは、単䞀のIntel Core i7-8700Kスレッド玄4.4 GHzで玄210ミリ秒でこのような合理化を実行できたす。 これは、1秒あたり玄2850䞇の䞉角圢に盞圓したす。 実際にはこれで十分かもしれたせんが、機噚の最倧胜力を探求したいず思いたした。

堎合によっおは、グリッドを断片に分割するこずで手順を䞊列化できたすが、このためには境界を維持するために接続性の远加分析を行う必芁があるため、ここでは玔粋なSIMD最適化に制限したす。

䞃次元


最適化の可胜性を理解するために、むンテルVTuneを䜿甚しおプロファむリングを実行したす。 手順を100回実行しお、十分なデヌタがあるこずを確認したす。



ここでは、各機胜の実行時間を修正し、ボトルネックを芋぀けるために、マむクロアヌキテクチャモヌドをオンにしたした。 合理化は䞀連の関数を䜿甚しお実行され、各関数には䞀定のサむクル数が必芁であるこずがわかりたす。 関数のリストは時間で゜ヌトされたす。 ここでは、アルゎリズムを理解しやすくするために、実行順に䞊んでいたす。


computeVertexIdsおよびcountTrianglesは数回実行されたすアルゎリズムは、頂点をマヌゞするためのメッシュサむズを決定し、加速バむナリ怜玢を実行しおタヌゲットの䞉角圢の数この堎合は6000を達成し、各メッシュサむズが各反埩で生成する䞉角圢の数を蚈算したす。 他の機胜は䞀床起動されたす。 このファむルでは、5回の怜玢パスでタヌゲットメッシュサむズが40 3になりたす。

VTuneは、リ゜ヌスを最も倚く消費する関数が2次関数を蚈算する関数であるこずを矩務的に報告したす。これには、21秒間の合蚈実行時間のほが半分がかかりたす。 これは、SIMDを最適化するための最初の目暙です。

SIMDピヌスバむピヌス


fillCellQuadricsの゜ヌスコヌドを芋お、蚈算察象を正確に理解しおみたしょう。

 static void fillCellQuadrics(Quadric* cell_quadrics, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const unsigned int* vertex_cells) { for (size_t i = 0; i < index_count; i += 3) { unsigned int i0 = indices[i + 0]; unsigned int i1 = indices[i + 1]; unsigned int i2 = indices[i + 2]; unsigned int c0 = vertex_cells[i0]; unsigned int c1 = vertex_cells[i1]; unsigned int c2 = vertex_cells[i2]; bool single_cell = (c0 == c1) & (c0 == c2); float weight = single_cell ? 3.f : 1.f; Quadric Q; quadricFromTriangle(Q, vertex_positions[i0], vertex_positions[i1], vertex_positions[i2], weight); if (single_cell) { quadricAdd(cell_quadrics[c0], Q); } else { quadricAdd(cell_quadrics[c0], Q); quadricAdd(cell_quadrics[c1], Q); quadricAdd(cell_quadrics[c2], Q); } } } 

この関数は、すべおの䞉角圢を反埩凊理し、各䞉角圢の2次関数を蚈算しお、各セルの2次関数に远加したす。 Quadric-10の浮動小数点数ずしお衚される4×4察称行列

 struct Quadric { float a00; float a10, a11; float a20, a21, a22; float b0, b1, b2, c; }; 

二次関数の蚈算には、䞉角圢の平面方皋匏を解き、二次行列を䜜成し、䞉角圢の面積を䜿甚しお重み付けする必芁がありたす。

 static void quadricFromPlane(Quadric& Q, float a, float b, float c, float d) { Q.a00 = a * a; Q.a10 = b * a; Q.a11 = b * b; Q.a20 = c * a; Q.a21 = c * b; Q.a22 = c * c; Q.b0 = d * a; Q.b1 = d * b; Q.b2 = d * c; Qc = d * d; } static void quadricFromTriangle(Quadric& Q, const Vector3& p0, const Vector3& p1, const Vector3& p2, float weight) { Vector3 p10 = {p1.x - p0.x, p1.y - p0.y, p1.z - p0.z}; Vector3 p20 = {p2.x - p0.x, p2.y - p0.y, p2.z - p0.z}; Vector3 normal = { p10.y * p20.z - p10.z * p20.y, p10.z * p20.x - p10.x * p20.z, p10.x * p20.y - p10.y * p20.x }; float area = normalize(normal); float distance = normal.x*p0.x + normal.y*p0.y + normal.z*p0.z; quadricFromPlane(Q, normal.x, normal.y, normal.z, -distance); quadricMul(Q, area * weight); } 

浮動小数点挔算が倚数あるように芋えるため、SIMDを䜿甚しお䞊列化できたす。 最初に、各ベクトルを4幅のSIMDベクトルずしお衚し、たた、 Quadric構造を10ではなく12の浮動小数点数に倉曎しお、3぀のSIMDレゞスタに正確に適合しサむズを倧きくしおもパフォヌマンスに圱響しない、フィヌルドの順序を倉曎しお蚈算を行いたすquadricFromPlaneはより均䞀になりたした。

 struct Quadric { float a00, a11, a22; float pad0; float a10, a21, a20; float pad1; float b0, b1, b2, c; }; 

ここで、䞀郚の蚈算、特にスカラヌ積は、SSEの以前のバヌゞョンずあたり䞀貫性がありたせん。 幞いなこずに、スカラヌ積の呜什がSSE4.1に登堎したした。これは非垞に䟿利です。

 static void fillCellQuadrics(Quadric* cell_quadrics, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const unsigned int* vertex_cells) { const int yzx = _MM_SHUFFLE(3, 0, 2, 1); const int zxy = _MM_SHUFFLE(3, 1, 0, 2); const int dp_xyz = 0x7f; for (size_t i = 0; i < index_count; i += 3) { unsigned int i0 = indices[i + 0]; unsigned int i1 = indices[i + 1]; unsigned int i2 = indices[i + 2]; unsigned int c0 = vertex_cells[i0]; unsigned int c1 = vertex_cells[i1]; unsigned int c2 = vertex_cells[i2]; bool single_cell = (c0 == c1) & (c0 == c2); __m128 p0 = _mm_loadu_ps(&vertex_positions[i0].x); __m128 p1 = _mm_loadu_ps(&vertex_positions[i1].x); __m128 p2 = _mm_loadu_ps(&vertex_positions[i2].x); __m128 p10 = _mm_sub_ps(p1, p0); __m128 p20 = _mm_sub_ps(p2, p0); __m128 normal = _mm_sub_ps( _mm_mul_ps( _mm_shuffle_ps(p10, p10, yzx), _mm_shuffle_ps(p20, p20, zxy)), _mm_mul_ps( _mm_shuffle_ps(p10, p10, zxy), _mm_shuffle_ps(p20, p20, yzx))); __m128 areasq = _mm_dp_ps(normal, normal, dp_xyz); // SSE4.1 __m128 area = _mm_sqrt_ps(areasq); // masks the result of the division when area==0 // scalar version does this in normalize() normal = _mm_and_ps( _mm_div_ps(normal, area), _mm_cmpneq_ps(area, _mm_setzero_ps())); __m128 distance = _mm_dp_ps(normal, p0, dp_xyz); // SSE4.1 __m128 negdistance = _mm_sub_ps(_mm_setzero_ps(), distance); __m128 normalnegdist = _mm_blend_ps(normal, negdistance, 8); __m128 Qx = _mm_mul_ps(normal, normal); __m128 Qy = _mm_mul_ps( _mm_shuffle_ps(normal, normal, _MM_SHUFFLE(3, 2, 2, 1)), _mm_shuffle_ps(normal, normal, _MM_SHUFFLE(3, 0, 1, 0))); __m128 Qz = _mm_mul_ps(negdistance, normalnegdist); if (single_cell) { area = _mm_mul_ps(area, _mm_set1_ps(3.f)); Qx = _mm_mul_ps(Qx, area); Qy = _mm_mul_ps(Qy, area); Qz = _mm_mul_ps(Qz, area); Quadric& q0 = cell_quadrics[c0]; __m128 q0x = _mm_loadu_ps(&q0.a00); __m128 q0y = _mm_loadu_ps(&q0.a10); __m128 q0z = _mm_loadu_ps(&q0.b0); _mm_storeu_ps(&q0.a00, _mm_add_ps(q0x, Qx)); _mm_storeu_ps(&q0.a10, _mm_add_ps(q0y, Qy)); _mm_storeu_ps(&q0.b0, _mm_add_ps(q0z, Qz)); } else { // omitted for brevity, repeats the if() body // three times for c0/c1/c2 } } } 

このコヌドには特に興味深いものはありたせん。 アラむンされおいないロヌド/ストア呜什を豊富に䜿甚しおいたす。 Vector3の入力はアラむメントできたすが、アラむメントされおいない読み取りに察しお目立ったペナルティはないようです。 関数の前半ではベクトルが䜿甚されおいないこずに泚意しおください-ベクトルは3぀のコンポヌネントを持ち、堎合によっおは1぀だけですareasq / area / distanceの蚈算を参照。䞀方、プロセッサは4぀の操䜜を䞊行しお実行したす。 いずれにせよ、䞊列化がどのように圹立぀かを芋おみたしょう。



fillCellQuadricsの100回の開始が9.8秒ではなく5.3秒で実行されるようになり、各操䜜で玄45ミリ秒節玄されたす-悪くはありたせんが、それほど印象的ではありたせん。 倚くの呜什では、4぀のコンポヌネントの代わりに3぀のコンポヌネントを䜿甚し、正確な乗算を䜿甚しおいるため、かなりの遅延が発生したす。 以前にSIMDの指瀺を曞いたこずがあれば、スカラヌ積を正しく行う方法を知っおいたす。

これを行うには、䞀床に4぀のベクトルを実行する必芁がありたす。 1぀のSIMDレゞスタに1぀のフルベクトルを栌玍する代わりに、3぀のレゞスタを䜿甚したす。1぀はx 4぀のコンポヌネントを栌玍し、もう1぀はを栌玍し、3番目のz栌玍したす。 ここでは、䞀床に4぀のベクトルが必芁です。぀たり、4぀の䞉角圢を同時に凊理したす。

動的むンデックスを䜿甚した配列が倚数ありたす。 通垞、 x / y / zコンポヌネントの準備された配列にデヌタを転送するのに圹立ちたすたたは、むしろ、入力の8぀の頂点のそれぞれに察しお、たずえば、 float x[8], y[8], z[8]などの小さなSIMDレゞスタが通垞䜿甚されたすデヌタこれはAoSoA配列構造の配列ず呌ばれ、キャッシュの局所性ずSIMDレゞスタヌぞのロヌドの容易さのバランスが取れおいたすが、ここでは動的なむンデックス付けはあたりうたく動䜜しないため、通垞のように4぀の䞉角圢のデヌタをロヌドし、䟿利な方法でベクトルを転眮したすマクロ_MM_TRANSPOSE

理論的には、独自のSIMDレゞスタで4぀の有限2次の各コンポヌネントを蚈算する必芁がありたすたずえば、 a00有限2次の4぀のコンポヌネントを持぀__m128 Q_a00がありたす。 この堎合、2次関数の挔算は4ワむドのSIMD呜什に非垞によく適合し、倉換により実際にコヌドの速床が䜎䞋したす。したがっお、初期ベクトルのみを転眮し、次に平面方皋匏を転眮しお、2次関数の蚈算に䜿甚したのず同じコヌドを実行したすが、それを繰り返したす4回。 コヌドは次のようになり、平面方皋匏を蚈算したす残りの郚分は簡朔にするために省略されおいたす。

 unsigned int i00 = indices[(i + 0) * 3 + 0]; unsigned int i01 = indices[(i + 0) * 3 + 1]; unsigned int i02 = indices[(i + 0) * 3 + 2]; unsigned int i10 = indices[(i + 1) * 3 + 0]; unsigned int i11 = indices[(i + 1) * 3 + 1]; unsigned int i12 = indices[(i + 1) * 3 + 2]; unsigned int i20 = indices[(i + 2) * 3 + 0]; unsigned int i21 = indices[(i + 2) * 3 + 1]; unsigned int i22 = indices[(i + 2) * 3 + 2]; unsigned int i30 = indices[(i + 3) * 3 + 0]; unsigned int i31 = indices[(i + 3) * 3 + 1]; unsigned int i32 = indices[(i + 3) * 3 + 2]; // load first vertex of each triangle and transpose into vectors with components (pw0 isn't used later) __m128 px0 = _mm_loadu_ps(&vertex_positions[i00].x); __m128 py0 = _mm_loadu_ps(&vertex_positions[i10].x); __m128 pz0 = _mm_loadu_ps(&vertex_positions[i20].x); __m128 pw0 = _mm_loadu_ps(&vertex_positions[i30].x); _MM_TRANSPOSE4_PS(px0, py0, pz0, pw0); // load second vertex of each triangle and transpose into vectors with components __m128 px1 = _mm_loadu_ps(&vertex_positions[i01].x); __m128 py1 = _mm_loadu_ps(&vertex_positions[i11].x); __m128 pz1 = _mm_loadu_ps(&vertex_positions[i21].x); __m128 pw1 = _mm_loadu_ps(&vertex_positions[i31].x); _MM_TRANSPOSE4_PS(px1, py1, pz1, pw1); // load third vertex of each triangle and transpose into vectors with components __m128 px2 = _mm_loadu_ps(&vertex_positions[i02].x); __m128 py2 = _mm_loadu_ps(&vertex_positions[i12].x); __m128 pz2 = _mm_loadu_ps(&vertex_positions[i22].x); __m128 pw2 = _mm_loadu_ps(&vertex_positions[i32].x); _MM_TRANSPOSE4_PS(px2, py2, pz2, pw2); // p1 - p0 __m128 px10 = _mm_sub_ps(px1, px0); __m128 py10 = _mm_sub_ps(py1, py0); __m128 pz10 = _mm_sub_ps(pz1, pz0); // p2 - p0 __m128 px20 = _mm_sub_ps(px2, px0); __m128 py20 = _mm_sub_ps(py2, py0); __m128 pz20 = _mm_sub_ps(pz2, pz0); // cross(p10, p20) __m128 normalx = _mm_sub_ps( _mm_mul_ps(py10, pz20), _mm_mul_ps(pz10, py20)); __m128 normaly = _mm_sub_ps( _mm_mul_ps(pz10, px20), _mm_mul_ps(px10, pz20)); __m128 normalz = _mm_sub_ps( _mm_mul_ps(px10, py20), _mm_mul_ps(py10, px20)); // normalize; note that areasq/area now contain 4 values, not just one __m128 areasq = _mm_add_ps( _mm_mul_ps(normalx, normalx), _mm_add_ps( _mm_mul_ps(normaly, normaly), _mm_mul_ps(normalz, normalz))); __m128 area = _mm_sqrt_ps(areasq); __m128 areanz = _mm_cmpneq_ps(area, _mm_setzero_ps()); normalx = _mm_and_ps(_mm_div_ps(normalx, area), areanz); normaly = _mm_and_ps(_mm_div_ps(normaly, area), areanz); normalz = _mm_and_ps(_mm_div_ps(normalz, area), areanz); __m128 distance = _mm_add_ps( _mm_mul_ps(normalx, px0), _mm_add_ps( _mm_mul_ps(normaly, py0), _mm_mul_ps(normalz, pz0))); __m128 negdistance = _mm_sub_ps(_mm_setzero_ps(), distance); // this computes the plane equations (a, b, c, d) for each of the 4 triangles __m128 plane0 = normalx; __m128 plane1 = normaly; __m128 plane2 = normalz; __m128 plane3 = negdistance; _MM_TRANSPOSE4_PS(plane0, plane1, plane2, plane3); 

コヌドはもう少し長くなりたした。各反埩で4぀の䞉角圢を凊理するようになり、このためのSSE4.1呜什は䞍芁になりたした。 理論的には、SIMDナニットをより効率的に䜿甚する必芁がありたす。 それがどのように圹立぀か芋おみたしょう。



倧䞈倫、倧䞈倫です。 fillCellQuadrics関数はSIMDなしで元の関数のほが2倍の速床で実行されたすが、コヌドはわずかに加速したすが、これが耇雑さの倧幅な増加を正圓化するかどうかは䞍明です。 理論的には、AVX2を䜿甚しお反埩ごずに8぀の䞉角圢を凊理できたすが、ここではルヌプを手動でさらにスピンする必芁がありたす理想的には、このコヌドはすべおISPCを䜿甚しお生成されたすが、良いコヌドを生成するための私の玠朎な詊みは成功したせんでしたシヌケンスのロヌド/保存の代わりに圌は持続的にギャザヌ/スキャッタヌを発行したため、実行速床が倧幅に䜎䞋したした。 他のこずを詊しおみたしょう。

AVX2 = SSE2 + SSE2


AVX2は、少し独特な䞀連の呜什です。 8幅の浮動小数点レゞスタヌがあり、1぀の呜什で8぀の操䜜を実行できたす。 しかし、実際には、そのような呜什は、レゞスタヌの半分で実行される2぀のSSE2呜什ず違いはありたせん私の知る限り、AVX2を搭茉した最初のプロセッサヌは、2぀以䞊のマむクロオペレヌションで各呜什をデコヌドするこずをサポヌトしおいるため、パフォヌマンスの向䞊は呜什の抜出フェヌズによっお制限されおいたした。 たずえば、 _mm_dp_psは2぀のSSE2レゞスタ間でスカラヌ積を実行し、 _mm256_dp_psは2぀のAVX2レゞスタの2぀の半分間で2぀のスカラヌ積を生成するため、半分ごずに4幅に制限されたす。

このため、AVX2コヌドは普遍的な「8ワむドSIMD」ずは異なる堎合がありたすが、ここでは有利に機胜したす。4ワむドベクトルを転眮しおベクトル化を改善する代わりに、SSE2の代わりにAVX2呜什を䜿甚しお最初のバヌゞョンのSIMDに戻り、ルヌプを2倍にしたす/ SSE4。 4幅のベクトルをロヌドしお保存する必芁がありたすが、䞀般的には、いく぀かの蚭定で_mm256 _mm_を_mm256に、 _mm_を_mm256に倉曎するだけです。

 unsigned int i00 = indices[(i + 0) * 3 + 0]; unsigned int i01 = indices[(i + 0) * 3 + 1]; unsigned int i02 = indices[(i + 0) * 3 + 2]; unsigned int i10 = indices[(i + 1) * 3 + 0]; unsigned int i11 = indices[(i + 1) * 3 + 1]; unsigned int i12 = indices[(i + 1) * 3 + 2]; __m256 p0 = _mm256_loadu2_m128( &vertex_positions[i10].x, &vertex_positions[i00].x); __m256 p1 = _mm256_loadu2_m128( &vertex_positions[i11].x, &vertex_positions[i01].x); __m256 p2 = _mm256_loadu2_m128( &vertex_positions[i12].x, &vertex_positions[i02].x); __m256 p10 = _mm256_sub_ps(p1, p0); __m256 p20 = _mm256_sub_ps(p2, p0); __m256 normal = _mm256_sub_ps( _mm256_mul_ps( _mm256_shuffle_ps(p10, p10, yzx), _mm256_shuffle_ps(p20, p20, zxy)), _mm256_mul_ps( _mm256_shuffle_ps(p10, p10, zxy), _mm256_shuffle_ps(p20, p20, yzx))); __m256 areasq = _mm256_dp_ps(normal, normal, dp_xyz); __m256 area = _mm256_sqrt_ps(areasq); __m256 areanz = _mm256_cmp_ps(area, _mm256_setzero_ps(), _CMP_NEQ_OQ); normal = _mm256_and_ps(_mm256_div_ps(normal, area), areanz); __m256 distance = _mm256_dp_ps(normal, p0, dp_xyz); __m256 negdistance = _mm256_sub_ps(_mm256_setzero_ps(), distance); __m256 normalnegdist = _mm256_blend_ps(normal, negdistance, 0x88); __m256 Qx = _mm256_mul_ps(normal, normal); __m256 Qy = _mm256_mul_ps( _mm256_shuffle_ps(normal, normal, _MM_SHUFFLE(3, 2, 2, 1)), _mm256_shuffle_ps(normal, normal, _MM_SHUFFLE(3, 0, 1, 0))); __m256 Qz = _mm256_mul_ps(negdistance, normalnegdist); 

ここで、受信したQx / Qz / Qz 128ビットの半分ごずに、2次関数を远加するために䜿甚したのず同じコヌドを実行できたす。 代わりに、䞉角圢の1぀のセルに3぀の頂点がある堎合 single_cell == true 、別の䞉角圢に別のセルの3぀の頂点がある可胜性が非垞に高いず想定し、AVX2を䜿甚しお2次の最終集玄も実行したす

 unsigned int c00 = vertex_cells[i00]; unsigned int c01 = vertex_cells[i01]; unsigned int c02 = vertex_cells[i02]; unsigned int c10 = vertex_cells[i10]; unsigned int c11 = vertex_cells[i11]; unsigned int c12 = vertex_cells[i12]; bool single_cell = (c00 == c01) & (c00 == c02) & (c10 == c11) & (c10 == c12); if (single_cell) { area = _mm256_mul_ps(area, _mm256_set1_ps(3.f)); Qx = _mm256_mul_ps(Qx, area); Qy = _mm256_mul_ps(Qy, area); Qz = _mm256_mul_ps(Qz, area); Quadric& q00 = cell_quadrics[c00]; Quadric& q10 = cell_quadrics[c10]; __m256 q0x = _mm256_loadu2_m128(&q10.a00, &q00.a00); __m256 q0y = _mm256_loadu2_m128(&q10.a10, &q00.a10); __m256 q0z = _mm256_loadu2_m128(&q10.b0, &q00.b0); _mm256_storeu2_m128(&q10.a00, &q00.a00, _mm256_add_ps(q0x, Qx)); _mm256_storeu2_m128(&q10.a10, &q00.a10, _mm256_add_ps(q0y, Qy)); _mm256_storeu2_m128(&q10.b0, &q00.b0, _mm256_add_ps(q0z, Qz)); } else { // omitted for brevity } 

結果のコヌドは、倱敗したSSE2アプロヌチよりも単玔、簡朔、高速です。



もちろん、8倍の加速は達成したせんでしたが、2.45倍しか達成したせんでした。 動的なむンデックス付けのために䞍䟿なメモリレむアりトで䜜業するこずを䜙儀なくされるため、ロヌドおよびストレヌゞ操䜜は䟝然ずしお4ワむドであり、蚈算はSIMDに最適ではありたせん。 しかし、今ではfillCellQuadricsプロファむルのパむプラむンのボトルネックでfillCellQuadricsなくなり、他の機胜に集䞭できたす。

集たっお、子䟛たち


テスト実行で4.8秒各実行で48ミリ秒を節玄し、珟圚の䞻な䟵入者はcountTrianglesです。 単玔な関数のように芋えたすが、1回ではなく5回実行されたす。

 static size_t countTriangles(const unsigned int* vertex_ids, const unsigned int* indices, size_t index_count) { size_t result = 0; for (size_t i = 0; i < index_count; i += 3) { unsigned int id0 = vertex_ids[indices[i + 0]]; unsigned int id1 = vertex_ids[indices[i + 1]]; unsigned int id2 = vertex_ids[indices[i + 2]]; result += (id0 != id1) & (id0 != id2) & (id1 != id2); } return result; } 

この関数は、すべおの゜ヌス䞉角圢を反埩凊理し、頂点の識別子を比范しお非瞮退䞉角圢の数を蚈算したす。 ギャザヌ呜什を䜿甚しない限り、SIMDを䜿甚しお䞊列化する方法はすぐにはわかりたせん。

AVX2呜什セットは、x64 SIMDにGather / Scatter呜什ファミリを远加したした。 それぞれが4぀たたは8぀の倀を持぀ベクトルレゞスタを受け取り、同時に4぀たたは8぀のロヌドたたは保存操䜜を実行したす。 ここでGatherを䜿甚するず、3぀のむンデックスをダりンロヌドし、Gatherを䞀床にたたは4たたは8のグルヌプで実行しお、結果を比范できたす。 Intelプロセッサでの収集は、これたでかなり䜎速でしたが、詊しおみたしょう。 簡単にするために、8぀の䞉角圢のデヌタをアップロヌドし、以前の詊みず同じ方法でベクトルを転眮し、各ベクトルの察応する芁玠を比范したす。

 for (size_t i = 0; i < (triangle_count & ~7); i += 8) { __m256 tri0 = _mm256_loadu2_m128( (const float*)&indices[(i + 4) * 3 + 0], (const float*)&indices[(i + 0) * 3 + 0]); __m256 tri1 = _mm256_loadu2_m128( (const float*)&indices[(i + 5) * 3 + 0], (const float*)&indices[(i + 1) * 3 + 0]); __m256 tri2 = _mm256_loadu2_m128( (const float*)&indices[(i + 6) * 3 + 0], (const float*)&indices[(i + 2) * 3 + 0]); __m256 tri3 = _mm256_loadu2_m128( (const float*)&indices[(i + 7) * 3 + 0], (const float*)&indices[(i + 3) * 3 + 0]); _MM_TRANSPOSE8_LANE4_PS(tri0, tri1, tri2, tri3); __m256i i0 = _mm256_castps_si256(tri0); __m256i i1 = _mm256_castps_si256(tri1); __m256i i2 = _mm256_castps_si256(tri2); __m256i id0 = _mm256_i32gather_epi32((int*)vertex_ids, i0, 4); __m256i id1 = _mm256_i32gather_epi32((int*)vertex_ids, i1, 4); __m256i id2 = _mm256_i32gather_epi32((int*)vertex_ids, i2, 4); __m256i deg = _mm256_or_si256( _mm256_cmpeq_epi32(id1, id2), _mm256_or_si256( _mm256_cmpeq_epi32(id0, id1), _mm256_cmpeq_epi32(id0, id2))); result += 8 - _mm_popcnt_u32(_mm256_movemask_epi8(deg)) / 4; } 

AVX2の_MM_TRANSPOSE8_LANE4_PSマクロは_MM_TRANSPOSE4_PSに盞圓したす。これは暙準ヘッダヌにはありたせんが、簡単に衚瀺されたす。 4぀のAVX2ベクトルを受け取り、2぀の4×4行列を互いに独立しお転眮したす。

 #define _MM_TRANSPOSE8_LANE4_PS(row0, row1, row2, row3) \ do { \ __m256 __t0, __t1, __t2, __t3; \ __t0 = _mm256_unpacklo_ps(row0, row1); \ __t1 = _mm256_unpackhi_ps(row0, row1); \ __t2 = _mm256_unpacklo_ps(row2, row3); \ __t3 = _mm256_unpackhi_ps(row2, row3); \ row0 = _mm256_shuffle_ps(__t0, __t2, _MM_SHUFFLE(1, 0, 1, 0)); \ row1 = _mm256_shuffle_ps(__t0, __t2, _MM_SHUFFLE(3, 2, 3, 2)); \ row2 = _mm256_shuffle_ps(__t1, __t3, _MM_SHUFFLE(1, 0, 1, 0)); \ row3 = _mm256_shuffle_ps(__t1, __t3, _MM_SHUFFLE(3, 2, 3, 2)); \ } while (0) 

SSE2 / AVX2呜什セットにはいく぀かの機胜があるため、ベクトルを転眮するずきには浮動小数点レゞスタヌ挔算を䜿甚する必芁がありたす。 デヌタを少し䞍泚意にロヌドしおいたす。 しかし、基本的には重芁ではありたせん。これは、パフォヌマンスの収集によっお制限されるためです。



countTrianglesは玄27高速になり、ひどいCPI呜什あたりのサむクルに気づきたした。玄4倍少ない呜什を送信したすが、収集には倚くの時間がかかりたす。 党䜓の䜜業が高速化されるのは玠晎らしいこずですが、もちろん、パフォヌマンスの向䞊はいくぶん萜ち蟌みたす。 プロファむル内のfillCellQuadricsを远い越しお、リストの䞀番䞊にある最埌の関数に移動したしたが、ただ芋おいたせん。

第6章、すべおが正垞に機胜し始める


最埌の関数はcomputeVertexIdsです。 アルゎリズムでは6回実行されるため、最適化の優れた目暙でもありたす。 SIMDで明確な最適化のために䜜成されたず思われる関数が初めお芋られたす。

 static void computeVertexIds(unsigned int* vertex_ids, const Vector3* vertex_positions, size_t vertex_count, int grid_size) { assert(grid_size >= 1 && grid_size <= 1024); float cell_scale = float(grid_size - 1); for (size_t i = 0; i < vertex_count; ++i) { const Vector3& v = vertex_positions[i]; int xi = int(vx * cell_scale + 0.5f); int yi = int(vy * cell_scale + 0.5f); int zi = int(vz * cell_scale + 0.5f); vertex_ids[i] = (xi << 20) | (yi << 10) | zi; } } 

前の最適化の埌、䜕をすべきかを知っおいたすサむクルを4回たたは8回展開したす。1回の反埩だけを高速化しようずしおも意味がないため、ベクトル成分を転眮し、䞊行しお蚈算を開始したす。 AVX2を䜿甚しおこれを行い、䞀床に8぀の頂点を凊理したす。

 __m256 scale = _mm256_set1_ps(cell_scale); __m256 half = _mm256_set1_ps(0.5f); for (size_t i = 0; i < (vertex_count & ~7); i += 8) { __m256 vx = _mm256_loadu2_m128( &vertex_positions[i + 4].x, &vertex_positions[i + 0].x); __m256 vy = _mm256_loadu2_m128( &vertex_positions[i + 5].x, &vertex_positions[i + 1].x); __m256 vz = _mm256_loadu2_m128( &vertex_positions[i + 6].x, &vertex_positions[i + 2].x); __m256 vw = _mm256_loadu2_m128( &vertex_positions[i + 7].x, &vertex_positions[i + 3].x); _MM_TRANSPOSE8_LANE4_PS(vx, vy, vz, vw); __m256i xi = _mm256_cvttps_epi32( _mm256_add_ps(_mm256_mul_ps(vx, scale), half)); __m256i yi = _mm256_cvttps_epi32( _mm256_add_ps(_mm256_mul_ps(vy, scale), half)); __m256i zi = _mm256_cvttps_epi32( _mm256_add_ps(_mm256_mul_ps(vz, scale), half)); __m256i id = _mm256_or_si256( zi, _mm256_or_si256( _mm256_slli_epi32(xi, 20), _mm256_slli_epi32(yi, 10))); _mm256_storeu_si256((__m256i*)&vertex_ids[i], id); } 

そしお結果を芋おください2回



加速computeVertexIdsしたした。すべおの最適化を考慮しお、プログラムの合蚈実行時間は玄120ミリ秒に短瞮されたした。これは、1秒あたり5,000䞇の䞉角圢の蚈算に盞圓したす。

予想される生産性の向䞊を再び達成できなかったように思えるかもしれたせcomputeVertexIdsん。䞊列化埌に2倍以䞊加速すべきではないでしょうか。この質問に答えるために、この関数が実行する䜜業の量を確認しおみたしょう。

computeVertexIds1回のプログラム開始で6回実行されたす。バむナリ怜玢䞭に5回、最埌に1回実行されお、さらなる凊理に䜿甚される最終識別子が蚈算されたす。この関数は300䞇個の頂点を凊理するたびに、各頂点に察しお12バむトを読み取り、4バむトを曞き蟌みたす。

合蚈100回を超えるむノベヌタヌの実行で、この関数は18億の頂点を凊理し、21 GBを読み取り、7 GBを曞き戻したす。 1.46秒で28 GBを凊理するには、19 GB / sのバス垯域幅が必芁です。を実行しお、メモリ垯域幅を確認できmemcmp(block1, block2, 512 MB)たす。結果は45ミリ秒、぀たり、1぀のコアで玄22 GB /秒ですただし、AIDA64ベンチマヌクは私のシステムで最倧31 GB /秒の読み取り速床を瀺したすが、耇数のコアを䜿甚したす。実際、達成可胜な最倧メモリ制限に近づいおおり、パフォヌマンスをさらに向䞊させるには、これらの頂点を12バむト未満に抑えるために、これらの頂点をより密にパッキングする必芁がありたす。

おわりに


毎秒2800䞇トラむアングルの速床で非垞に倧きなグリッドを簡玠化する、かなり最適化されたアルゎリズムを採甚し、SSEおよびAVX呜什セットを䜿甚しお、ほが2倍、1秒あたり5,000䞇トラむアングルたで高速化したした。この過皋で、SIMDのさたざたな䜿甚方法を孊習する必芁がありたした。3ワむドベクトルを栌玍するレゞスタ、SoA転眮、2぀の3ワむドベクトルを栌玍するAVX2呜什、スカラヌ呜什ず比范しおデヌタロヌドを高速化する呜什を収集し、最埌にストリヌミング凊理にAVX2を盎接適甚したした。

倚くの堎合、SIMDは最適化の最適な出発点ではありたせん。メッシュオプティマむザヌは、プラットフォヌム固有の呜什を䜿甚せずに、アルゎリズム最適化ずマむクロ最適化の倚くの反埩を実行したした。しかし、ある時点で、これらの可胜性は䜿い果たされ、パフォヌマンスが重芁な堎合、SIMDは必芁に応じお䜿甚できる玠晎らしいツヌルです。

これらの最適化がメむンブランチに圓おはたるmeshoptimizerかどうかはわかりたせん。最終的に、これは、アルゎリズムを根本的に倉曎せずにコヌドがどれだけ加速するかを確認するための単なる実隓です。この蚘事が有益であり、コヌドを最適化するためのアむデアを提䟛しおくれるこずを願っおいたす。この蚘事の最終情報源はこちらです。この䜜業は、meshoptimizer 99ab49のバヌゞョンに基づいおいたす、そしおタむ仏モデルがSketchfabで公開されおいたす。

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


All Articles