はじめに
C# にも関数ポインタが存在します(.NET7 で導入)。
参考: https://ufcpp.net/study/csharp/interop/functionpointer/
-
static
メソッド限定 -
unsafe
コード必須 - デリゲートよりちょっとだけパフォーマンスがよい
C 言語での関数ポインタは、継承がない C 言語で振る舞いを抽象化する手段でした。
int (*func)(int, int);
func = max;
func(3, 5);
この機能は C# では型安全やガベージコレクション、インスタンスメソッドの関係からデリゲートとして実装されています。
static int Func1() => 1;
Func<int> func = Func1;
Assert.Equal(1, func());
C# でも関数ポインタを利用できます。デリゲートはクラスなのでヒープアロケーションが発生するため、その僅かなインスタンス生成もケチりたいときに有効です。
unsafe
{
delegate*<int> ptr = &Func1;
Assert.Equal(1, ptr());
}
テストコード
テストコード
using System.Runtime.CompilerServices;
using Xunit;
public class __FunctionPointerTest
{
[Fact]
void HowToUse()
{
static int Func1() => 1;
Func<int> func = Func1;
Assert.Equal(1, func());
unsafe
{
delegate*<int> ptr = &Func1;
Assert.Equal(1, ptr());
}
}
static void FunctionPerformance(Performance p)
{
p.AddTest("NoInlining Function", () =>
{
[MethodImpl(MethodImplOptions.NoInlining)]
int Square(int num) => num * num;
var sum = 0;
for (var i = 0; i < 1000; i++)
sum += Square(i);
});
p.AddTest("Normal Delegate", () =>
{
int Square(int num) => num * num;
var func = Square;
var sum = 0;
for (var i = 0; i < 1000; i++)
sum += func(i);
});
p.AddTest("Static Delegate", () =>
{
static int Square(int num) => num * num;
var func = Square;
var sum = 0;
for (var i = 0; i < 1000; i++)
sum += func(i);
});
p.AddTest("Function Pointer", () =>
{
static int Square(int num) => num * num;
unsafe
{
delegate*<int, int> ptr = □
var sum = 0;
for (var i = 0; i < 1000; i++)
sum += ptr(i);
}
});
p.AddTest("Inlining Function", () =>
{
int Square(int num) => num * num;
var sum = 0;
for (var i = 0; i < 1000; i++)
sum += Square(i);
});
}
}
パフォーマンス計測
int Square(int num) => num * num;
var sum = 0;
for (var i = 0; i < 1000; i++)
sum += Square(i);
Test | Score | % | CG0 |
---|---|---|---|
x86 | |||
NoInlining Function | 75,538 | 100.0% | 0 |
Normal Delegate | 49,171 | 65.1% | 0 |
Static Delegate | 39,895 | 52.8% | 0 |
Function Pointer | 66,823 | 88.5% | 0 |
Inlining Function | 394,652 | 522.5% | 0 |
x64 | |||
NoInlining Function | 22,317 | 100.0% | 0 |
Normal Delegate | 8,559 | 38.4% | 0 |
Static Delegate | 8,698 | 39.0% | 0 |
Function Pointer | 22,126 | 99.1% | 0 |
Inlining Function | 21,698 | 97.2% | 0 |
実行環境: Windows11 x64 .NET Runtime 10.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
- 関数ポインタは、デリゲートを実行するより少し高速です
- 関数ポインタは、通常のメソッドの呼び出しに近いスコアです
- インライン化されたメソッド呼び出しは非常に高速で、これにはかないません
おわりに
C# の関数ポインタは unsafe
コード限定ですし、使い所はかなり限られそうです。基本的にデリゲートを使うのがいいでしょう。
多態性は通常だとクラスの継承やインターフェイスの継承を使いますが、高速化するため抽象部分をインライン化するテクニックがあります。
参考: 値型ジェネリックを使うとインライン化が効く https://ufcpp.net/study/csharp/sp2_generics.html?p=3#pseudo-static