【.NET 9.0】System.Threading.Lock のパフォーマンス

# はじめに

.NET 9.0 で同期処理用の [`System.Threading.Lock`](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.lock?view=net-9.0) が追加されました。  
**任意のオブジェクトを `lock` できるのはオーバーヘッドが大きい** <https://ufcpp.net/blog/2024/4/lock-class/> らしく、パフォーマンス改善のために導入されたようです。

今回は**現時点** (.NET 9.0-rc.1) でどのくらいパフォーマンスに違いがあるか検証してみます。

```cs
// Lock の使い方
using System.Threading;

var lockHandle = new Lock();
lock (lockHandle)
{
    // Do something
}
```

## サンプルコード

<details>
<summary>テストコード</summary>

```cs
using System.Threading;

public class _LockClassTest
{
    void HowToUse()
    {
        var lockHandle = new Lock();
        lock (lockHandle)
        {
            // Do something
        }
    }

    static void IncrementTest(Performance p)
    {
        p.AddTest("lock_object", () =>
        {
            var count = 0;
            var lockHandle = new object();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    ++count;
            }
        });

        p.AddTest("lock_Lock", () =>
        {
            var count = 0;
            var lockHandle = new Lock();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    ++count;
            }
        });

        p.AddTest("Lock.EnterScope()", () =>
        {
            var count = 0;
            var lockHandle = new Lock();
            for (int n = 0; n < 10000; n++)
            {
                using (lockHandle.EnterScope())
                    ++count;
            }
        });

        p.AddTest("Monitor_object", () =>
        {
            var count = 0;
            var lockHandle = new object();
            for (int n = 0; n < 10000; n++)
            {
                Monitor.Enter(lockHandle);
                ++count;
                Monitor.Exit(lockHandle);
            }
        });

        p.AddTest("InterLocked", () =>
        {
            var count = 0;
            for (int n = 0; n < 10000; n++)
            {
                Interlocked.Increment(ref count);
            }
        });

        p.AddTest("NoLock", () =>
        {
            var count = 0;
            for (int n = 0; n < 10000; n++)
            {
                ++count;
            }
        });
    }

    static void GetHashCodeTest(Performance p)
    {
        p.AddTest("lock_lockHandle", () =>
        {
            var hash = 0;
            var lockHandle = new object();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    hash += lockHandle.GetHashCode();
            }
        });

        p.AddTest("lock_obj", () =>
        {
            var hash = 0;
            var lockHandle = new object();
            var obj = new object();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    hash += obj.GetHashCode();
            }
        });

        p.AddTest("Lock_lockHandle", () =>
        {
            var hash = 0;
            var lockHandle = new Lock();
            for (int n = 0; n < 10000; n++)
            {
                lock (lockHandle)
                    hash += lockHandle.GetHashCode();
            }
        });

        p.AddTest("NoLock", () =>
        {
            var hash = 0;
            var obj = new object();
            for (int n = 0; n < 10000; n++)
            {
                hash += obj.GetHashCode();
            }
        });
    }
}

```

</details>

## パフォーマンス比較

### テストその１：インクリメント

単純なインクリメントを行う、同期処理のテストです。

```cs
var count = 0;
var lockHandle = new object();
for (int n = 0; n < 10000; n++)
{
    lock (lockHandle)
        ++count;
}
```

|Test|Score|%|CG0|
|---|---:|---:|---:|
|lock_object|1,195|100.0%|0|
|lock_Lock|1,256|105.1%|0|
|Lock.EnterScope()|1,188|99.4%|0|
|Monitor_object|1,479|123.8%|0|
|InterLocked|5,598|468.5%|0|
|NoLock|42,696|3,572.9%|0|

実行環境: Windows11 x64 .NET Runtime 9.0.0  
**Score** は高いほどパフォーマンスがよいです。  
**GC0** はガベージコレクション回数を表します（少ないほどパフォーマンスがよい）。

- ロックハンドルが `object` の場合と比較して、`Lock` は 5% くらいパフォーマンスが良いです
- `lock(Lock)` と `using(Lock.EnterScope())` はコンパイラによって同じコードになります（糖衣構文）
- スコアはブレがあります（10% 程度）
- インクリメント等の数値の単純な操作は、[`System.Threading.Interlocked`](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.interlocked?view=net-8.0) を使うのが高速です

### テストその２：`GetHashCode()` を使う場合

ロック用の値とハッシュ値は共有しているらしく、ロック中はハッシュコードの取得が遅くなるとのことで、その検証です。

```cs
var hash = 0;
var lockHandle = new object();
for (int n = 0; n < 10000; n++)
{
    lock (lockHandle)
        hash += lockHandle.GetHashCode();
}
```

|Test|Score|%|CG0|
|---|---:|---:|---:|
|lock_lockHandle|885|100.0%|0|
|lock_obj|1,018|115.0%|0|
|Lock_lockHandle|1,036|117.1%|0|
|NoLock|3,907|441.5%|0|

実行環境: Windows11 x64 .NET Runtime 9.0.0  
**Score** は高いほどパフォーマンスがよいです。  
**GC0** はガベージコレクション回数を表します（少ないほどパフォーマンスがよい）。

- ロック中にロックしたオブジェクトからハッシュコードを取得した場合、15% 程度パフォーマンスが悪いです
- `Lock` オブジェクトの場合、ロック中にハッシュコードを取得しても影響なさそうです
- とはいえ、ロック中のオブジェクトのハッシュコードを取得することはまずなさそうです

## おわりに

検証の結果、現時点 (.NET 9.0-rc.1) では `lock(Lock)` は従来の方法よりも 5% くらいパフォーマンスに優れるようです。既存のコードをリファクタリングするほどではないですが、新しいコードでは取り入れてもいいでしょう。

マルチスレッドのプログラムは難しくパフォーマンスより正しさ・読みやすさを優先したいところですが、`Lock` クラスは構文的にも既存のコードにそのまま置き換えられるのが良さげです。  
パフォーマンス改善に加えて、そのオブジェクトの用途がスレッドのロックに限定されるため、コードの可読性も上がるかもしれません。