🦆
[C#]ParallelOptionsで、CPU使用量の暴走を防ぎながらそこそこ早い行列積
はじめに
前回の記事にて、並列化により行列積の計算をかなり高速化できました。計算量が多い場合並列化一択ですね……と素直に喜ぶことはできないようです。
下の図は並列処理時のCPU使用量の図です。CPU使用量がほぼ100%であり、使用率が急増する時にファンが回りだすのは正直心臓に悪いです。
今回は、行列積の速度とCPU使用率の安定性の塩梅を探ろうと思います。
注記
この記事は2022/11/28に書きました。以前の記事で頂いたアドバイスを反映できていません。
参考URL
Parallelクラスでお手軽な並列処理 - Qiita
How to use Maximum Degree of Parallelism in C# - Dot Net Tutorials
CPU使用率を抑える方法
ParallelOptions
というものを使います。意外とお手軽で、設定をした後、Parallel.For
の第3引数にオプションを代入するだけです。
// 並列処理オプションの設定
ParallelOptions parallelOptions = new ParallelOptions();
parallelOptions.MaxDegreeOfParallelism = 2;
Parallel.For(0, N, parallelOptions, i =>
{
// Your Task
});
結果
CPU使用量
スレッド数制限とCPU使用量の関係を下の表に記します。CPU使用量を抑えたい場合、スレッド数は4以下がちょうど良さそうです。
スレッド数 | CPU使用量 |
---|---|
2 | 17 |
3 | 25 |
4 | 33 |
5 | 42 |
制限なし | 100 |
プログラム実行時間
プログラム実行時間の比較に前回記事の結果も載せています。スレッド数2だけでも効果があるのが分かります。ぼんやりと眺めている感じではスレッド数3辺りが最高効率でしょうか。
配列 | 転置 | ポインター | Span | 並列化 | スレッド数 | 計算時間 |
---|---|---|---|---|---|---|
ジャグ配列 | - | - | - | - | - | 1分53秒97 |
〃 | - | - | - | 〇 | 2 | 1分3秒55 |
〃 | - | - | - | 〇 | 3 | 42秒57 |
〃 | - | - | - | 〇 | 4 | 34秒86 |
〃 | - | - | - | 〇 | 5 | 29秒85 |
〃 | - | - | - | 〇 | 制限なし | 21秒45 |
〃 | 〇 | - | - | - | - | 41秒26 |
〃 | 〇 | - | - | 〇 | 2 | 27秒31 |
〃 | 〇 | - | - | 〇 | 3 | 17秒87 |
〃 | 〇 | - | - | 〇 | 4 | 16秒24 |
〃 | 〇 | - | - | 〇 | 5 | 13秒14 |
〃 | 〇 | - | - | 〇 | 制限なし | 8秒92 |
〃 | 〇 | 〇 | - | - | - | 30秒32 |
〃 | 〇 | 〇 | - | 〇 | 2 | 19秒62 |
〃 | 〇 | 〇 | - | 〇 | 3 | 13秒24 |
〃 | 〇 | 〇 | - | 〇 | 4 | 12秒63 |
〃 | 〇 | 〇 | - | 〇 | 5 | 9秒82 |
〃 | 〇 | 〇 | - | 〇 | 制限なし | 6秒56 |
〃 | 〇 | - | 〇 | - | - | 43秒55 |
多次元配列 | - | - | - | - | - | 2分19秒99 |
〃 | - | - | - | 〇 | 2 | 1分19秒42 |
〃 | - | - | - | 〇 | 3 | 53秒71 |
〃 | - | - | - | 〇 | 4 | 51秒65 |
〃 | - | - | - | 〇 | 5 | 37秒94 |
〃 | - | - | - | 〇 | 制限なし | 26秒82 |
〃 | 〇 | - | - | - | - | 1分1秒18 |
〃 | 〇 | - | - | 〇 | 2 | 36秒63 |
〃 | 〇 | - | - | 〇 | 3 | 27秒60 |
〃 | 〇 | - | - | 〇 | 4 | 24秒83 |
〃 | 〇 | - | - | 〇 | 5 | 20秒36 |
〃 | 〇 | - | - | 〇 | 制限なし | 15秒93 |
〃 | 〇 | 〇 | - | - | - | 1分15秒19 |
〃 | 〇 | 〇 | - | 〇 | 2 | 1分37秒66 |
〃 | 〇 | 〇 | - | 〇 | 3 | 1分41秒13 |
〃 | 〇 | 〇 | - | 〇 | 4 | 1分43秒31 |
〃 | 〇 | 〇 | - | 〇 | 5 | 1分42秒72 |
〃 | 〇 | 〇 | - | 〇 | 制限なし | 1分23秒61 |
終わりに
CPU使用量と実行時間を見て、適度なスレッド数3を実装しようと思いました。
テストコード
長くなってきたので、コードは最後に載せました。
テストコード
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Sandbox.Tremendous1192.SelfEmployed.UnsafeMathDotNet.Zenn.Matrix
{
/// <summary>
/// 行列に関するテストクラス
/// </summary>
public static partial class TestMatrix
{
/// <summary>
/// 行列積の速度比較
/// </summary>
public static void TestMultiplyUnsafeSpanParallelOptions(int degree=2)
{
// ParallelOptionsでスレッド数を制限する
ParallelOptions option = new ParallelOptions();
option.MaxDegreeOfParallelism = 2;
if (option.MaxDegreeOfParallelism < degree) { option.MaxDegreeOfParallelism = degree; }
// 初期化
int N = 2022; // 左側の行列の行
int D = 2022; // 左側の行列の列
int M = 2022; // 右側の行列の列
Console.WriteLine("左の行列の行数" + N + "\t列数" + D + "\t右の行列の行数" + M);
double[,] left2D=new double[N, D];
double[,] right2D=new double[D, M];
double[][] leftJag = new double[N][];
double[][] rightJag = new double[D][];
for (int i=0;i<N;i++)
{
leftJag[i] = new double[D];
for (int k=0;k<D;k++)
{
left2D[i, k] = i + k;
leftJag[i][k] = i + k;
}
}
for (int k = 0; k < D; k++)
{
rightJag[k] = new double[M];
for (int j = 0; j < M; j++)
{
right2D[k, j] = k + j + 1;
rightJag[k][j] = k + j + 1;
}
}
Stopwatch sw= new Stopwatch();
sw.Start();
// 1-*-1 ジャグ配列の普通の計算 + 並列化
{
double[][] resultJag = new double[N][];
Parallel.For(0, N, option, i =>
{
resultJag[i] = new double[M];
for (int j = 0; j < M; j++)
{
for (int k = 0; k < D; k++)
{
resultJag[i][j] += leftJag[i][k] * rightJag[k][j];
}
}
});
}
GC.Collect();
sw.Stop();
string elapseJagParallel = sw.Elapsed.ToString();
//Console.WriteLine(elapseJag);
Console.WriteLine("1-*-1 ジャグ配列の普通の計算 + 並列化:\t" + elapseJagParallel);
// 2-*-1 ジャグ配列の転置の計算 + 並列化
sw.Restart();
{
double[][] transposedJag = new double[M][];
for (int j = 0; j < M; j++)
{
transposedJag[j] = new double[D];
for (int k = 0; k < D; k++)
{
transposedJag[j][k] = rightJag[k][j];
}
}
double[][] resultJag = new double[N][];
Parallel.For(0, N, option, i =>
{
resultJag[i] = new double[M];
for (int j = 0; j < M; j++)
{
for (int k = 0; k < D; k++)
{
resultJag[i][j] += leftJag[i][k] * transposedJag[j][k];
}
}
});
}
GC.Collect();
sw.Stop();
string elapseJagTParallel = sw.Elapsed.ToString();
//Console.WriteLine(elapseJagT);
Console.WriteLine("2-*-1 ジャグ配列の右の行列の転置 + 並列化:\t" + elapseJagTParallel);
// 2-1-1 ジャグ配列の転置の計算 + ポインター + 並列化
sw.Restart();
unsafe
{
double[][] transposedJag = new double[M][];
for (int j = 0; j < M; j++)
{
transposedJag[j] = new double[D];
for (int k = 0; k < D; k++)
{
transposedJag[j][k] = rightJag[k][j];
}
}
double[][] resultJag = new double[N][];
for (int i = 0; i < N; i++)
{
resultJag[i] = new double[M];
}
Parallel.For(0, N, option, i =>
{
fixed (double* pResultJag = &resultJag[i][0])
{
int j = 0;
for (double* pR = pResultJag; pR != pResultJag + resultJag[i].Length; ++pR)
{
for (int k = 0; k < D; k++)
{
*pR += leftJag[i][k] * transposedJag[j][k];
}
++j;
}
}
});
}
GC.Collect();
sw.Stop();
string elapseJagTPointerParallel = sw.Elapsed.ToString();
//Console.WriteLine(elapseJagTPointer);
Console.WriteLine("2-1-1 ジャグ配列の右の行列の転置 + ポインター + 並列化:\t" + elapseJagTPointerParallel);
Console.WriteLine("\n\n");
// 1-*-1 多次元配列の普通の計算 + 並列化
sw.Restart();
{
double[,] resultMulti = new double[N, M];
Parallel.For(0, N, option, i =>
{
for (int j = 0; j < M; j++)
{
for (int k = 0; k < D; k++)
{
resultMulti[i, j] += left2D[i, k] * right2D[k, j];
}
}
});
}
GC.Collect();
sw.Stop();
string elapseMultiParallel = sw.Elapsed.ToString();
//Console.WriteLine(elapseMulti);
Console.WriteLine("1-*-1 多次元配列の普通の計算 + 並列化:\t" + elapseMultiParallel);
// 2-*-1 多次元配列の転置の計算 + 並列化
sw.Restart();
{
double[,] transposedMulti = new double[M, D];
for (int j = 0; j < M; j++)
{
for (int k = 0; k < D; k++)
{
transposedMulti[j, k] = right2D[k, j];
}
}
double[,] resultMulti = new double[N, M];
Parallel.For(0, N, option, i =>
{
for (int j = 0; j < M; j++)
{
for (int k = 0; k < D; k++)
{
resultMulti[i, j] += left2D[i, k] * transposedMulti[j, k];
}
}
});
}
GC.Collect();
sw.Stop();
string elapseMultiTParallel = sw.Elapsed.ToString();
//Console.WriteLine(elapseMultiT);
Console.WriteLine("2-*-1 多次元配列の右の行列の転置 + 並列化:\t" + elapseMultiTParallel);
// 2-1-1 多次元配列の転置とポインターの計算 + 並列化
sw.Restart();
unsafe
{
double[,] transposedMulti = new double[M, D];
for (int j = 0; j < M; j++)
{
for (int k = 0; k < D; k++)
{
transposedMulti[j, k] = right2D[k, j];
}
}
double[,] resultMulti = new double[N, M];
int count = 0;
fixed (double* pMulti = resultMulti)
{
double* pM = pMulti;
Parallel.For(0, N*M, option, i =>
{
for (int k = 0; k < D; k++)
{
*pM += left2D[count / M, k] * transposedMulti[count % M, k];
}
++count;
++pM;
});
}
}
GC.Collect();
sw.Stop();
string elapseMultiTPointerParallel = sw.Elapsed.ToString();
//Console.WriteLine(elapseMultiTPointer);
Console.WriteLine("2-1-1 多次元配列の右の行列の転置 + ポインター + 並列化:\t" + elapseMultiTPointerParallel);
//Console.WriteLine("1-*-1 ジャグ配列の普通の計算 + 並列化:\t" + elapseJagParallel);
//Console.WriteLine("2-*-1 ジャグ配列の右の行列の転置 + 並列化:\t" + elapseJagTParallel);
//Console.WriteLine("2-1-1 ジャグ配列の右の行列の転置 + ポインター + 並列化:\t" + elapseJagTPointerParallel);
//Console.WriteLine("1-*-1 多次元配列の普通の計算 + 並列化:\t" + elapseMultiParallel);
//Console.WriteLine("2-*-1 多次元配列の右の行列の転置 + 並列化:\t" + elapseMultiT);
//Console.WriteLine("2-1-1 多次元配列の右の行列の転置 + ポインター + 並列化:\t" + elapseMultiTPointerParallel);
}
}
}
Discussion