簡単な例によるElbrusプラットフォームのコード最適化

「通常、ハッカーは営利目的でプログラムを作成しません
しかし、私自身の喜びのために。 そのようなプログラム
役に立つかもしれませんが、残っているかもしれません
単なる知性のゲームです。」
ヘンリー・S・ウォーレン。 プログラマー向けのアルゴリズムのトリック[1]


今日は、エルブラスに関するメモを続けます。 パスポート認識システムの起動と最適化に関する最初の記事は、 ここにあります


画像


かつて、同僚と私は、最も簡単な最適化手法がElbrusでどのように機能するかに興味を持ちました。


ElbrusプロセッサにはVLIW(Very Long Instruction Word)アーキテクチャがあります。つまり、「広範な」コマンドワードで動作します。 これは、lccコンパイラがコンパイル中にプログラムのソースコードを分析し、コマンド間の依存関係を判断し、広範なコマンドワードを生成することを意味します。 そのような言葉では、同時に実行される最大23のアクションに適合できます。 SIMD(単一命令複数データ)を使用する場合、この数は33以上の操作に増加する可能性があります。 広義のコマンドは並行して実行され、各プロセッサに6つの算術論理デバイスすべてを確実にロードします。 すべての計算機の並列化と読み込みは、最適化コンパイラの肩に完全に依存します。これにより、コマンドの分析と実行のための機器が大幅に簡素化され、Elbrus-4Cの消費電力が最大45 W削減されます[2、3]。


スマートエンジンでは、スマートコンパイラを備えたこのような珍しいプラットフォームでのループの展開など、通常の最適化がどのように機能するのか疑問に思いました。


C ++の簡単な例を調べ、Elbrus-4CとIntel Core i7-6700Kでの作業結果を比較しました。 Elbrusでは、Core i7-Microsoft Visual Studio Community 2015にlccコンパイラバージョン1.20.17を使用しました。lccには、-O3および-ffast-mathコンパイルフラグを使用しました。


はじめに、Elbrus-4CおよびIntel Core i7-6700Kの特徴を以下に示します。


エルブルス-4CIntel Core i7-6700K
建築エルブルススカイレイク
周波数、GHz0.84
コアの数44(8 cハイパースレッディング)
技術プロセス65 nm14 nm
キャッシュL1サイズ、データ64 Kb32 Kb
キャッシュL1サイズ、命令128 Kb32 Kb
キャッシュl2サイズ8 Mb1 Mb
キャッシュl3サイズ-8 Mb
RAMタイプDDR3-1600DDR4-2133
消費電力4591

これらのプロセッサのクロック周波数は大きく異なります。Elbrus-4Cの場合は800 MHz、Intel Core i7の場合は4 GHzです。 また、Elbrusのキャッシュ構造は異なります。L3キャッシュはありませんが、L2キャッシュのサイズは8 Mb(コアあたり2 Mb)であり、レビュー済みのCore i7には1 Mb(コアあたり0.25 Mb)があります。 ElbrusのL1キャッシュ、特に命令キャッシュも大きくなります(128 Kb対32 Kb)。


例1.サイクル展開


この最適化は、サイクルの本体を増やすことで、サイクルの反復回数を減らすことを目的としています(したがって、サイクルを終了するための条件のチェック回数を減らします)。 このような最適化は、ほとんどすべてのプログラムで発生する単純なループに適しています。


次に例を考えます。


#define N 64000 unsigned int x = 0; unsigned int a[N]; for (int i = 0; i < N; ++i) a[i] = i; //  for (int k = 0; k < N;) { x += a[k]; a[k++] = k; } 

最後のサイクルを展開しようとしました。 時間測定の結果(サイクルの10,000回の繰り返し)を表1に示します。Elbrus(コンパイラフラグはmptr32)で32ビットアドレッシングモードが使用されたことに注意してください。 また、測定時間にGHz単位のプロセッサクロック周波数を掛けて、1 GHzでの動作時間を計算しました。 この方法で得られた無次元の値により、クロック周波数の違いを考慮して、ElbrusとCore i7のパフォーマンスを比較できます。


表1. Nに依存する動作時間-展開された反復の数。


エルブルス-4Cエルブルス-4CIntel Core i7Intel Core i7
N時間、ミリ秒1 GHz単位の時間時間、ミリ秒1 GHz単位の時間
14013202551020
24003202751100
44013202611044
8401320247988
164013203611444
324523622621048
644513622621048

この例のサイクルの展開は、最新のCore i7とElbrus-4Cの両方で動作時間の増加をもたらさないことがわかります。 検討した非常に単純なサイクルの場合、Elbrus-4Cは、周波数比を考慮して、Core i7よりも効率的に動作します。


例2.さまざまな長さのデータを処理する


この例では、1、4、または8バイトで配列を処理します。 元の配列は8バイトで整列されました。


 #define MASK1 0xF1 #define MASK4 0xF1F2F3F4 #define MASK8 0xF1F2F3F4F5F6F7F8 for (k = 0; k < n; ++k) { [k] &= MASK1; } 

時間測定の結果を表2に示します。


表2. Nに依存する動作時間-処理されたバイト数。


エルブルス-4Cエルブルス-4CIntel Core i7Intel Core i7
N時間、ミリ秒1 GHz単位の時間時間、ミリ秒1 GHz単位の時間
1240019208113244
4600480201804
8300240102408

最新のCore i7とElbrus-4Cの両方で4バイトと8バイトの処理が高速になり、処理されるバイト数の倍数だけ時間が短縮されることがわかります。 さらに、周波数比を考慮すると、ElbrusはCore i7よりも効率的です。


例3. SIMDの使用


この例では、組み込み関数をテストすることにし、 n = 12800 float型の数のスカラー積の計算を調べました。
最適化されていないループ:


 float result = 0.0; for (int i = 0; i < n; ++i) { result += x[i] * y[i]; } 

SSEの使用:


 float *pX = x; float *pY = y; __m128 Sum = _mm_setzero_ps(); int num = n / 4; for (int i = 0; i < num; ++i, pX += 4, pY += 4) { Sum = _mm_add_ps(Sum, _mm_mul_ps(_mm_load_ps(pX), _mm_load_ps(pY))); } float result = _mm_extract_ps(Sum, 0) + _mm_extract_ps(Sum, 1) + _mm_extract_ps(Sum, 2) + _mm_extract_ps(Sum, 3); 

EMLの使用[4](Elbrus用に最適化されたライブラリ):


 double result; eml_Vector_DotProd_32F(x, y, n, &result); 

時間測定の結果を表3に示します。Elbrus-4CのSIMDレジスタのサイズは64ビット(命令セットバージョン3)です。これは一般に、最適化なしのバージョンとSIMD付きのバージョン間で観測された2倍の加速に対応します。 Core i7では、すべてがもっともらしいです。128ビットのレジスタで動作し、4つのタイプのfloatに適合しました。 さらに、組み込み関数のないElbrusは、周波数を考慮してCore i7よりも効率的に動作しますが、組み込み関数の場合、動作時間はほぼ同じです。


表3.スカラー積を計算するための作業時間。


エルブルス-4Cエルブルス-4CIntel Core i7Intel Core i7
時間、ミリ秒1 GHz単位の時間時間、ミリ秒1 GHz単位の時間
最適化なし26321099396
SIMDを使用110882496

例4. 2つの配列間のハミング距離のカウント


ここでは、2つの配列のバイナリ表現間のハミング距離を計算しました。 配列内の対応する数値のバイナリ表現間のハミング距離を取り、それらの合計を見つけました。 8ビットデータの配列の場合、ビット単位の排他的ORと計算された距離テーブルを使用しました。


 int result = 0; for (int i = 0; i < n; ++i) { result += popcnt_table[x[i] ^ y[i]]; } 

32ビットおよび64ビットデータの場合、ビット単位の論理排他的ORと、Intelの組み込み関数_mm_popcnt_u32, _mm_popcnt_u64 、およびElbrusの組み込み関数__builtin_e2k_popcnts, __builtin_e2k_popcntd _mm_popcnt_u32, _mm_popcnt_u64__builtin_e2k_popcnts, __builtin_e2k_popcntdました。 配列xおよびyのバイト単位の全長は変化せず、n = 512に等しくなりました。時間測定の結果を表4に示します。


表4.処理されたバイト数Nに応じたハミング距離の計算時間


エルブルス-4Cエルブルス-4CIntel Core i7Intel Core i7
N時間、ミリ秒1 GHz単位の時間時間、ミリ秒1 GHz単位の時間
1630504155620
41108847188
876611560

この例では、ElbrusとCore i7の両方で64ビットおよび32ビットレジスタのユニット数をカウントするための組み込み関数が、事前に計算されたテーブルを使用したバージョンに比べて大幅に高速化されることがわかります。 さらに、周波数比を考慮して、Elbrusの32ビットpopcntコマンドはCore i7より高速です。 しかし、64ビットの場合、Core i7とElbrusの作業時間は同じです。


例5.データ依存関係を排除する


この例は、Chris Kasperskyによる「プログラムを最適化するための手法」という本から引用しています。 メモリの効率的な使用」[5]。 データの依存関係の解決が生産性の向上にどのように役立つかを示しています。 配列aゼロで埋められます( n = 2097152
データ依存関係の例:


 int x = 0; for (size_t a = 0; a < BLOCK_SIZE; a += 8 * sizeof(int)) { x = *(int *)((char *)p1 + a + 0 * sizeof(int)); a += x; x = *(int *)((char *)p1 + a + 1 * sizeof(int)); a += x; x = *(int *)((char *)p1 + a + 2 * sizeof(int)); a += x; x = *(int *)((char *)p1 + a + 3 * sizeof(int)); a += x; x = *(int *)((char *)p1 + a + 4 * sizeof(int)); a += x; x = *(int *)((char *)p1 + a + 5 * sizeof(int)); a += x; x = *(int *)((char *)p1 + a + 6 * sizeof(int)); a += x; x = *(int *)((char *)p1 + a + 7 * sizeof(int)); a += x; } 

後続の各要素インデックスは、前のコマンドで計算された値に依存するため、メモリからの要素の読み込みは、前の命令の完了後に順次行われます。
これで、コードには依存関係がなくなりました。


 int x = 0; for (size_t a = 0; a < BLOCK_SIZE; a += 8 * sizeof(int)) { x += *(int *)((char *)p2 + a + 0 * sizeof(int)); x += *(int *)((char *)p2 + a + 1 * sizeof(int)); x += *(int *)((char *)p2 + a + 2 * sizeof(int)); x += *(int *)((char *)p2 + a + 3 * sizeof(int)); x += *(int *)((char *)p2 + a + 4 * sizeof(int)); x += *(int *)((char *)p2 + a + 5 * sizeof(int)); x += *(int *)((char *)p2 + a + 6 * sizeof(int)); x += *(int *)((char *)p2 + a + 7 * sizeof(int)); } 

時間測定の結果を表5に示します。データ依存関係の除去は、ElbrusとCore i7の両方で機能し、Core i7の動作時間は約11倍、Elbrusでは約20倍異なります。 データ依存性のあるコードは、1 GHzの観点からCore i7よりもElbrusで動作が遅くなりましたが、中毒がないと、ElbrusはCore i7よりも4倍遅いだけです(周波数差は5倍)。 Elbrusでのこのような結果は、スワップされた要素が直列に配置されている場合に効率的に機能する非同期スワッピング配列(配列プリフェッチバッファー)メカニズムの存在によって説明できます。


表5.依存データおよび独立データの読み取り時間。


エルブルス-4Cエルブルス-4CIntel Core i7Intel Core i7
時間、ミリ秒1 GHz単位の時間時間、ミリ秒1 GHz単位の時間
依存データ60548487348
独立データ3226832

例6.マルチスレッドコンピューティング


もちろん、並列化などの最適化方法を検討せざるを得ませんでした。 実験を純粋にするために、完全に独立したタスク(2つのdouble配列のスカラー積の計算)を行いました。 表6は、N個のタスクのシーケンス時間(T last )、N個のスレッド内のN個のタスクの時間(T ペア )、および加速度Eを示しています。


表6.タスクおよびフローの数Nに応じたスカラー積の逐次および並列計算の時間


エルブルス-4Cエルブルス-4Cエルブルス-4CIntel Core i7Intel Core i7Intel Core i7
NT last 、msT ペア 、msE = T last / T ペアT last 、msT ペア 、msE = T last / T ペア
2262813162.0010335002.07
4525913183.9919945003.99
81051326343.9939875037.93
162104552684.00798010097.91
202632165834.00996712637.89
3242053105353.991594820147.92
4052566131703.991993625287.89

Core i7では、8スレッドで加速がほぼ8倍に達し、さらにわずかに異なることがわかります。 Elbrusでは、4つのストリームで4倍の加速が達成され、ストリームの数が増えても変化しません。 ElbrusとCore i7の速度比は約2.6でしたが、周波数比は5です。


結論


計算を高速化する通常の方法は、Elbrusで非常にうまく機能します。この点で、そのためのプログラミングは特定の知識とスキルを必要としません。 コードの最適化のために検討された基本的な例では、Elbrusは、クロック周波数が800 MHzであり、Core i7と比較して消費電力が半分であることを考えると、非常に優れていることがわかりました。


PSそして、私たちはSPARCでパスポート認識システムも立ち上げました! これは、さらに別のコンピューティングアーキテクチャで認識記事を作成できるようになることを意味します。


更新する 例1の結果にエラーが入り込みました。VLevのコメントのおかげで発見し、修正しました。 結果が更新されました。


使用したソース


[1]ヘンリー・C・ウォーレン・ジュニア プログラマー向けのアルゴリズムトリック。 M .:ウィリアムズ出版社、2014年。ISBN978-5-8459-1838-3-512 C.
[2] http://www.elbrus.ru/arhitektura_elbrus
[3] http://www.mcst.ru/mikroprocessor-elbrus4s
[4] http://www.mcst.ru/vysokoproizvoditelnye_biblioteki
[5]クリス・カスペルスキー。 「テクノロジー最適化プログラム。 メモリの効率的な使用。」 サンクトペテルブルク:BHV-Petersburg、2003。ISBN5-94157-232-8-464S。



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


All Articles