9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C# の関数ポインタ

Posted at

はじめに

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 = &Square;
                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

9
7
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
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?