JohannPachelbel
@JohannPachelbel

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

ロック有無による処理時間の差の測定

解決したいこと

現在マルチスレッド勉強中で,ロック有無による処理時間の差を測定しています.以下の関数の処理時間を計測して,ロックなしの方がロックあり(for文内,for文外)に比べて処理時間が短くなると予想したのですが,異なる結果が出ています.特に,処理時間がロックあり(for文外)<ロックなしとなる理由が分からず困っています.この原因について,どなたかご教授願えないでしょうか?

測定結果(10回平均)

inLock(ロックあり(for文内))...約0.75秒
outLock(ロックあり(for文外))...約0.06秒
noLock(ロックなし)...約0.15秒

→処理時間について,ロックあり(for文外)<ロックなし<ロックあり(for文内)の結果となりました.
(予想はロックなし<ロックあり(for文外)<ロックあり(for文内))

該当するソースコード

// 外部の名前空間をインポート
using System;
using System.Threading;

// C#プログラムは通常1つ以上のクラスから構成される
class DataRaceExample
{
    // 共有変数として使用するためstatic宣言
    // 本課題では複数スレッドからのアクセスを想定
    static int sharedVariable = 0;
    const int number = 10000000; // 10^8

    // 共有オブジェクトとして使用するためstatic宣言
    // 本オブジェクトはロックのために使用
    static object lockObject = new object();

    // インスタンス作成
    // 共有変数と操作をインスタンスで共有するためstatic宣言
    static void IncrementSharedVariable_noLock()
    {
        // 100万回インクリメント
        for (int i = 0; i < number; i++)
        {
                sharedVariable++;
        }
    }

    static void IncrementSharedVariable_inLock()
    {
        // 100万回インクリメント
        for (int i = 0; i < number; i++)
        {
            // lockステートメントより同時アクセスを制御
            lock (lockObject)
            {
                sharedVariable++;
            }
        }
    }

    static void IncrementSharedVariable_outLock()
    {
        lock (lockObject)
        {
            // 100万回インクリメント
            for (int i = 0; i < number; i++)
            {
                sharedVariable++;
            }
        }

    }

    // インスタンスを作成せずに実行させるためstatic宣言
    static void Main()
    {
        var sw = new System.Diagnostics.Stopwatch();

        // 2つのスレッドを作成
        // 同じメソッドIncrementSharedVariableを実行
        Thread thread1 = new Thread(IncrementSharedVariable_inLock);
        Thread thread2 = new Thread(IncrementSharedVariable_inLock);
        Thread thread3 = new Thread(IncrementSharedVariable_inLock);
        Thread thread4 = new Thread(IncrementSharedVariable_inLock);

        /*Thread thread1 = new Thread(IncrementSharedVariable_outLock);
        Thread thread2 = new Thread(IncrementSharedVariable_outLock);
        Thread thread3 = new Thread(IncrementSharedVariable_outLock);
        Thread thread4 = new Thread(IncrementSharedVariable_outLock);*/

        /*Thread thread1 = new Thread(IncrementSharedVariable_noLock);
        Thread thread2 = new Thread(IncrementSharedVariable_noLock);
        Thread thread3 = new Thread(IncrementSharedVariable_noLock);
        Thread thread4 = new Thread(IncrementSharedVariable_noLock);*/

        sw.Start();

        // メソッドIncrementSharedVariableを同時に実行
        thread1.Start();
        thread2.Start();
        thread3.Start();
        thread4.Start();

        // Mainメソッドの終了防止のためthread1とthread2の実行が終了するまで待機
        thread1.Join();
        thread2.Join();
        thread3.Join();
        thread4.Join();

        sw.Stop();

        // 最終的なsharedVariableの値を表示(予測できない値になっている)
        Console.WriteLine("Final shared variable value: " + sharedVariable);
        Console.WriteLine("time: " + sw.Elapsed);

        // ウィンドウが閉じて値が見られなくなるのを防ぐため.文字入力まで待機
        // Console.ReadKey();
    }
}

自分で試したこと

ここに問題・エラーに対して試したことを記載してください。
・スレッド数の増加(4,8,16など)
・統合開発環境のコード最適化OFF

0

5Answer

BenchMarkdotNetで計測した結果です
noLockとoutLockのパフォーマンス順位は回によって前後します
1回のロック構文は100万回ループの前では些事であり、無視できるほどの差と言っていいと思います
ここに理由を見つける意義はあまりないように感じます

--1回目のベンチマーク
| Method                          | Mean      | Error     | StdDev   | Min       | Max       |
|-------------------------------- |----------:|----------:|---------:|----------:|----------:|
| IncrementSharedVariable_noLock  |  20.98 ms |  2.314 ms | 0.127 ms |  20.83 ms |  21.06 ms |
| IncrementSharedVariable_inLock  | 212.40 ms | 53.687 ms | 2.943 ms | 209.20 ms | 214.99 ms |
| IncrementSharedVariable_outLock |  20.16 ms |  8.930 ms | 0.490 ms |  19.60 ms |  20.51 ms |
--2回目のベンチマーク
| Method                          | Mean      | Error    | StdDev   | Min       | Max       |
|-------------------------------- |----------:|---------:|---------:|----------:|----------:|
| IncrementSharedVariable_noLock  |  20.76 ms | 1.994 ms | 0.109 ms |  20.64 ms |  20.83 ms |
| IncrementSharedVariable_inLock  | 215.85 ms | 4.775 ms | 0.262 ms | 215.57 ms | 216.09 ms |
| IncrementSharedVariable_outLock |  20.75 ms | 3.105 ms | 0.170 ms |  20.56 ms |  20.90 ms |

ベンチマークは自力で正しく行うのは非常に困難なので、ベンチマークソフトを使うことをお勧めします。
NugetでBenchmarkDotNetをインストールすることで簡単に測定できます。

4Like

「outLock(ロックあり(for文外))」の方は、コンパイラから見れば、"ロックを取得してから「同じ変数について単純にnumber回インクリメントする」という処理が完了するまで絶対にロックが解除されない"ことが明らかです。

このため、「同じ変数についてnumber回インクリメント」の部分が最適化されて高速になっていると考えられます。

一方、noLockの方は、複数のスレッドによるデータ競合が発生しうるため、最適化できません。

1Like

c#、dotnetの最適化で除去されなかった時(inc [rbx]; dec ecx; jnzみたいなコードが出た時)の話ですが、複数プロセッサがある場合ですが、キャッシュ競合(sharing)効果により遅くなることはあります。
lockのばあい同時に動いているスレッドは1つしかないので、メモリ書き込みは実際にはL1までで止まりますが、nolockはあるプロセッサが書き込むと、ほかのプロセッサのキャッシュは無効になることによります(l3やdramまで行ったり、バススヌープで値を待つ必要がある)
https://articlesforengineers.blogspot.com/2023/07/true-sharing-and-false-sharing.html
https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%B9%E3%82%B9%E3%83%8C%E3%83%BC%E3%83%94%E3%83%B3%E3%82%B0

1Like

This answer has been deleted for violation of our Terms of Service.

今はもう非同期プログラミングに Thread という原始的なクラスを使うのではなく、async/await を使う時代なのですが(以下の記事参照。Figure 3 だけでも見てください)、評価・測定の仕方を async/await を使ったコードで行うように考え直すことはできませんか?

第1回 .NET開発における非同期処理の基礎と歴史
https://atmarkit.itmedia.co.jp/fdotnet/chushin/masterasync_01/masterasync_01_02.html

0Like

Your answer might help someone💌