Post

C#에서 Function Pointer 사용하기

C#에서 Function Pointer 사용하기

C#의 함수 포인터(Function Pointer)는 C# 9.0부터 도입된 기능으로, 고성능 연산이나 네이티브(C++, Delphi) 라이브러리와의 연동을 위해 만들어진 가장 로우레벨한 함수 호출 방식이다.

C++의 void (*func)(int)와 동일하게 함수의 주솟값을 직접(참조) 가리킨다. 메모리 주소를 직접 다루기 때문에 반드시 unsafe 키워드가 필요하다. csproj에서 AllowUnsafeBlocktrue로 설정한다.

Delegate나 Action 같은 객체를 생성하지 않으므로, 호출 시 가비지 컬렉터(GC) 오버헤드가 없고 CPU가 즉시 해당 주소로 점프하므로 제로 오버헤드로 볼 수 있다.

함수 포인터의 기본 문법은 delegate* 키워드를 사용하여 선언하며, 마지막 파라미터가 리턴 타입이 된다.

1
2
3
4
5
6
7
8
// 선언: <매개변수1, 매개변수2, 리턴타입>
delegate*<int, int, int> addPtr;

// 할당: & 연산자로 정적(static) 함수의 주소를 가져옴
addPtr = &MyStaticFunction;
  
// 호출: 일반 함수처럼 호출
int result = addPtr(10, 20);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Program.cs
using System;  
  
namespace ConsoleApp;  
  
internal class Program  
{  
    private static void Main()  
    {  
        MathEngine math = new();  
        double resulMath = math.Execute(OpType.Divide, 3, 0);  
  
        if (double.IsNaN(resulMath))  
        {  
            Console.WriteLine($"Error: {resulMath}");  
        }  
        else  
        {  
            Console.WriteLine(resulMath);  
        }  
    }  
}
MathEngine.cs (Function Pointer)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
namespace ConsoleApp;  
  
public unsafe class MathEngine  
{  
    private readonly delegate*<double, double, double>[] mOperationTable;
  
    public MathEngine()  
    {  
        mOperationTable = new delegate*<double, double, double>[4];  
        mOperationTable[(int)OpType.Add] = &Add;  
        mOperationTable[(int)OpType.Subtract] = &Subtract;  
        mOperationTable[(int)OpType.Multiply] = &Multiply;  
        mOperationTable[(int)OpType.Divide] = &Divide;  
    }  
  
    private static double Add(double a, double b) => a + b;  
  
    private static double Subtract(double a, double b) => a - b;  
  
    private static double Multiply(double a, double b) => a * b;  
  
    private static double Divide(double a, double b) => b != 0 ? a / b : double.NaN;
  
    public double Execute(OpType op, double a, double b)  
    {  
        int opIndex = (int)op;  
  
        if ((uint)opIndex >= (uint)mOperationTable.Length)  
        {  
            return 0;  
        }  
  
        delegate*<double, double, double> func = mOperationTable[opIndex];
        return func != null ? func(a, b) : 0;

        // if (func != null)
        // {
        //     return mOperationTable[opIndex](a, b);
        // }
        //
        // return 0;
    }  
}  
  
public enum OpType  
{  
    Add = 0,  
    Subtract = 1,  
    Multiply = 2,  
    Divide = 3  
}
Dictionary vs. Function Pointer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;  
using System.Collections.Generic;  
  
public class DictionaryExample  
{  
    private readonly Dictionary<string, Action<string>> mCommandMap = new();
  
    public DictionaryExample()  
    {  
        mCommandMap.Add("Print", (msg) => Console.WriteLine($"[Print] {msg}"));
        mCommandMap.Add("Log", ShowLog);  
    }  
  
    private static void ShowLog(string message)  
    {  
        Console.WriteLine($"[Log] {DateTime.Now}: {message}");  
    }  
  
    public void Execute(string commandName, string data)  
    {  
        if (mCommandMap.TryGetValue(commandName, out var action))  
        {  
            action(data);  
        }  
        else  
        {  
            Console.WriteLine("명령어를 찾을 수 없습니다.");  
        }  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;  
  
public unsafe class FunctionPointerExample  
{  
    private readonly delegate*<int, void>[] mFunctionTable;  
  
    public FunctionPointerExample()  
    {  
        mFunctionTable = new delegate*<int, void>[2];  
        mFunctionTable[0] = &DoubleValue;  
        mFunctionTable[1] = &SquareValue;  
    }  
  
    private static void DoubleValue(int x) => Console.WriteLine($"Double: {x * 2}");
  
    private static void SquareValue(int x) => Console.WriteLine($"Square: {x * x}");
  
    public void Execute(int index, int value)  
    {  
        if (index >= 0 && index < mFunctionTable.Length)  
        {  
            mFunctionTable[index](value);  
        }  
    }  
}

Dictionary는 Execute("Print", "Hello")와 같이 이름으로 호출하므로 코드가 직관적이지만 내부적으로 문자열 비교와 해시 계산이 발생한다. Function Pointer는 Execute(0, 10)와 같이 인덱스로 호출하므로 매우 빠르다.

This post is licensed under CC BY 4.0 by the author.