失われた時間のマルチスレッド物語

出版物「The Tale of Lost Time」で、ユーザーcrea7orは、現代のCPUでオイラー仮説をどのように反fuしたかを説明しました。

GPUがどのように表示されるかを知りたいと思い、 CUDA並列コンピューティングアーキテクチャを使用して、シングルスレッドコードとCPUのマルチスレッドおよびGPUの完全マルチスレッドを比較しました。

アイロンとコンパイラ
CPU コアi5 4440
GPU GeForce GTX 770
MSVS 2015アップデート3
Takeit CUDA 8
もちろん、ビルドはリリースおよび64ビットで、最適化は/ 02および/ LTCGでした。
ビデオアダプターのリセットを無効にする
計算には2秒以上かかるため、ビデオドライバーはCUDAプログラムを完了します。 これを防ぐには、レジストリキーでリセットする前に十分な時間を指定する必要があります
HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Control \ GraphicsDrivers \ TdrDelayとコンピューターを再起動します。


CPU


まず、CPUのアルゴリズムを並列化しました。 これを行うには、範囲を関数に渡し、外側のループaの値を反復処理します。 次に、範囲1..N全体が断片に分割され、プロセッサコアに供給されます。

void Algorithm_1(const uint32_t N, const std::vector<uint64_t>& powers, const uint32_t from, const uint32_t to) { for (uint32_t a = from; a < to; ++a) { for (uint32_t b = 1; b < a; ++b) { for (uint32_t c = 1; c < b; ++c) { for (uint32_t d = 1; d < c; ++d) { const uint64_t sum = powers[a] + powers[b] + powers[c] + powers[d]; if (std::binary_search(std::begin(powers), std::end(powers), sum)) { const auto it = std::lower_bound(std::begin(powers), std::end(powers), sum); uint32_t e = static_cast<uint32_t>(std::distance(std::begin(powers), it)); std::cout << a << " " << b << " " << c << " " << d << " " << e << "\n"; } } } } } } 

私の場合、4つのコアがあり、 ここで基礎として取ったわずかに変更されたスレッドプールを介して計算を実行しました 。 データ間に依存関係がないため、速度はコアの数にほぼ比例して増加します。

GPU


GPUの場合、少し複雑になります。 外側の2つの列挙ループ( aおよびb )がデプロイされ、関数は内側の2つのループ( cおよびd )を列挙するだけで済みます。 プログラムは、 a = 1..Nおよびb = 1..Nのすべての値に対して並列に実行されると想像できます。 実際、これはそうではありません。それらはまだ並列に実行できませんが、実行を可能な限り並列化することがCUDAの関心事です。これは、ブロックとフローの構成が正しく構成されている場合に役立ちます。

ブロックとストリーム
  int blocks_x = N; int threads = std::min(1024, static_cast<int>(N)); int blocks_y = (N + threads - 1) / threads; dim3 grid(blocks_x, blocks_y); NaiveGPUKernel<<<grid, threads>>>(N); 

blocks_xは、最初のループaの列挙範囲です。
しかし、2番目のサイクルbの検索は、ビデオカードのスレッド数の制限(1024)により困難になり、 threadsblocks_yに同時にカウンターを配置する必要がありました
abは次のように計算されます。
  const int a = blockIdx.x + 1; const int b = threadIdx.x + blockIdx.y * blockDim.x + 1; 

多くのアイドルサイクルが発生するため、これは間違いなく最適な構成ではありません。 しかし、値をそれ以上調整しませんでした。

GPUのコードは非常に単純で、CPUに非常に似ています。バイナリ検索のみを手で行う必要があります。

 __constant__ uint64_t gpu_powers[8192]; inline __device__ int gpu_binary_search(const uint32_t N, const uint64_t search) { uint32_t l = 0; uint32_t r = elements_count - 1; uint32_t m; while (l <= r) { m = (l + r) / 2; if (search < gpu_powers[m]) r = m - 1; else if (search > gpu_powers[m]) l = m + 1; else return l; } return -1; } __global__ void NaiveGPUKernel(const uint32_t N) { const int a = blockIdx.x + 1; const int b = threadIdx.x + blockIdx.y * blockDim.x + 1; if (b >= a) return; for (uint32_t c = 1; c < b; ++c) { for (uint32_t d = 1; d < c; ++d) { const uint64_t sum = gpu_powers[a] + gpu_powers[b] + gpu_powers[c] + gpu_powers[d]; const auto s = gpu_binary_search(N, sum); if (s > 0) { printf("%d %d %d %d %d\n", a, b, c, d, s); } } } } 

また、 ここで提案されている最適化も実装しました

速度測定


CPUの測定は、GPUよりもはるかに安定していることが判明したため、2回行われました。これは、一度に7回行い、最適な時間を選択しました。 GPUの普及は2倍になる可能性があります。これは、システム内の唯一のビデオアダプターがCUDA計算に使用されたという事実によって説明できます。
NCPU時間、ミリ秒CPU(4スレッド)時間、msGPU時間、ミリ秒
しょーた10058.616.714.8
ベーシック10045.313.010.7
基本最適化#11006.32.112.7
ベース最適化#21001.40.70.8
しょーた2502999.7782.9119.0
ベーシック2502055.6550.190.9
基本最適化#1250227.260.3109.2
ベース最適化#225042.911.96.0
しょーた50072034.219344.11725.83
ベーシック50038219.710200.8976.7
基本最適化#15003725.1926.51140.36
ベース最適化#2500630.7170.248.7
しょーた750396566.0105003.011521.2
ベーシック750218615.057639.25742.5
基本最適化#175019082.74736.86402.1
ベース最適化#27503272.0846.9222.9
ベース最適化#2100010204.42730.31041.9
ベース最適化#2125025133.16515.32445.5
ベース最適化#2150051940.114005.04895.2

そして、アドレナリンをCPUに注入しましょう!


そして、そのアドレナリンはプロファイルに基づく最適化になります。

PGOの場合、GPUがほとんど変化していないため、CPU時間のみを指定します。これは予想されることです。
NCPU
時間、ミリ秒
CPU(4スレッド)
時間、ミリ秒
CPU + PGO
時間、ミリ秒
CPU + PGO
(4スレッド)
時間、ミリ秒
しょーた10058.616.755.315.0
ベーシック10045.313.042.212.1
基本最適化#11006.32.15.21.9
ベース最適化#21001.40.71.30.8
しょーた2502999.7782.92966.1774.1
ベーシック2502055.6550.12050.2544.6
基本最適化#1250227.260.3200.053.2
ベース最適化#225042.911.940.411.4
しょーた50072034.219344.168662.817959.0
ベーシック50038219.710200.838077.710034.0
基本最適化#15003725.1926.53132.9822.2
ベース最適化#2500630.7170.2618.1160.6
しょーた750396566.0105003.0404692.0103602.0
ベーシック750218615.057639.2207975.054868.2
基本最適化#175019082.74736.815496.44082.3
ベース最適化#27503272.0846.93093.8812.7
ベース最適化#2100010204.42730.39781.62565.9
ベース最適化#2125025133.16515.323704.36244.1
ベース最適化#2150051940.114005.048717.512793.5

PGOは最適化#1を最大18%加速することができたことがわかりますが、それ以外の場合、増加はより緩やかでした。

ドラゴンがいます


楽しい機能について言及するのを忘れました。 最初に最初の解決策を検索し、printfの直後にreturnを設定しました。 そのため、パフォーマンスが一桁低下しました。 すべてのソリューションを探し始めたとき、生産性は急激に上がりました。
 __global__ void NaiveGPUKernel(const uint32_t N) { const int a = blockIdx.x + 1; const int b = threadIdx.x + blockIdx.y * blockDim.x + 1; if (b >= a) return; for (uint32_t c = 1; c < b; ++c) { for (uint32_t d = 1; d < c; ++d) { const uint64_t sum = gpu_powers[a] + gpu_powers[b] + gpu_powers[c] + gpu_powers[d]; const auto s = gpu_binary_search(N, sum); if (s > 0) { printf("%d %d %d %d %d\n", a, b, c, d, s); return; <-      } } } } 


さらにできること


CPUについては、サイクルを拡張し、一度に2つの数値を読み取り、ベクトルプロセッサ命令を使用してください。

GPUの場合、計算の順序で再生し、レジスタに保存してみてください。ブロックとフローのパラメーターを選択することをお勧めします。

結論


1. CUDAでの書き込みは簡単です。
2.トリックなしで、簡単なCUDAコードは実装CPUの10倍高速であることが判明しました。
3.数値計算タスクでは、同じ費用でGPUはCPUよりもはるかに高速です。
4. GPUは「スイング」するのに多くの時間を必要とし、非常に短いタスクの場合、加速はそうでない場合があります。
5.より複雑なアルゴリズムは、CPUでは高速ですが、GPUでは低速です。
6.プロファイルの最適化は、コードを高速化し、同時にファイルサイズを削減します。

ソースコード


従来、 GitHubのコードに触れることができます

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


All Articles