🦆

[C#]ParallelOptionsで、CPU使用量の暴走を防ぎながらそこそこ早い行列積

2022/12/31に公開

はじめに

前回の記事にて、並列化により行列積の計算をかなり高速化できました。計算量が多い場合並列化一択ですね……と素直に喜ぶことはできないようです。

下の図は並列処理時の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