1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

.NET 9.0 で同期処理用の System.Threading.Lock が追加されました。
任意のオブジェクトを lock できるのはオーバーヘッドが大きい https://ufcpp.net/blog/2024/4/lock-class/ らしく、パフォーマンス改善のために導入されたようです。

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

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

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

サンプルコード

テストコード
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();
            }
        });
    }
}

パフォーマンス比較

テストその1:インクリメント

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

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 を使うのが高速です

テストその2:GetHashCode() を使う場合

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

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 クラスは構文的にも既存のコードにそのまま置き換えられるのが良さげです。
パフォーマンス改善に加えて、そのオブジェクトの用途がスレッドのロックに限定されるため、コードの可読性も上がるかもしれません。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?