てくメモ

trivial な notes

【C#】Generic Math を試して測ってみる

Generic Math とは、.NET 7.0 で登場したインターフェイスの静的抽象メンバーを利用したジェネリックな数値処理のコンセプト。
参考:
【Generic Math】 C# 11 での演算子の新機能 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C


例として、順列の総数 (nPr) を考えてみる。
普通にintを使えば以下のような感じ。

// n * (n - 1) * (n - 2) * ... * (n - r + 1)
public static int NPRRegularCore(int n, int r)
{
    int result = 1;
    for (int i = 0; i < r; i++, n--)
    {
        result *= n;
    }

    return result;
}


Generic Math を利用してみる。

// ここでは拡張メソッド
public static T NPR<T>(this T n, T r)
    where T : INumber<T> // ← Generic Math なインターフェース
{
    T result = T.One;
    for(T i = T.Zero; i < r; i++, n--)
    {
        result *= n;
    }

    return result;
}

T.OneT.Zeroが目立つけれど、他にもインクリメント・デクリメント(++--)、比較(<)、乗算(*)などがシレッと行えている。Generic な Math。


これは、INumber<TSelf>インターフェースを備えた型ならいずれでも利用できる。
このインターフェースについてはプリミティブな数値型はもちろん、BigIntegerなど多くの数値型が備えている。

var npr1 = 4.NPR(3);         // int
var npr2 = 5L.NPR(2);        // long
var npr3 = 255U.NPR(0U);     // uint
var npr4 = ((nint)6).NPR(4); // nint

// 24, 20, 1, 360
Console.WriteLine($"{npr1}, {npr2}, {npr3}, {npr4}");


上記ではINumber<TSelf>で型制約しているが、扱うのは整数ということでIBinaryInteger<TSelf>が適当かもしれない。
性質に応じて様々なインターフェースが用意されている。より詳しくは以下参考


数値型に共通する処理をジェネリックに書けるのはとてもよい。

ところで、型ベタ書きと較べて速度的な遜色はあるだろうか。
Generic Math は前述のとおりインターフェイスの静的抽象メンバーという仕組みに基づいている。

この仕組みのメソッドについては、型に紐づく静的メソッドの呼び出しになっている。
参考:インターフェース - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

インターフェースメソッドの仮想呼び出しによりませんということで、インターフェースを利用しているからといって具象版に大きく後れを取ることはない……?


ということで、上述 nPr を BenchmarkDotNet に投げてみる。

計測用コード折りたたみ

        private static class NPR<T>
            where T : INumber<T>
        {
            public static int NPRRegularCore(int n, int r)
            {
                int result = 1;
                for (int i = 0; i < r; i++, n--)
                {
                    result *= n;
                }

                return result;
            }

            public static T NPRGenMathCore(T n, T r)
            {
                T result = T.One;
                for (T i = T.Zero; i < r; i++, n--)
                {
                    result *= n;
                }

                return result;
            }
        }

        [Benchmark]
        public int NPRRegular() => NPR<int>.NPRRegularCore(12, 8);

        [Benchmark]
        public int NPRGenMath() => NPR<int>.NPRGenMathCore(12, 8);

     Method |     Mean |     Error |    StdDev | Allocated |
----------- |---------:|----------:|----------:|----------:|
 NPRRegular | 4.019 ns | 0.6816 ns | 0.0374 ns |         - |
 NPRGenMath | 4.532 ns | 1.9404 ns | 0.1064 ns |         - |


若干の差はある。(int版が式で完結している部分が呼び出しになっていることが要因か)
これが許容できなかったり既に仕組みがあるなら型別に機械的生成といった手段となるだろうけれど、通常のケースで一から型共通の数値処理を書くなら可能なら Generic Math が使いたい選択肢と言えそうに思った。