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