はじめに
インターフェイスのメソッド呼び出しはコンパイル時に呼び出されるメソッド(型)は決定せず、実行時に決定します。これは一般的にメソッドのインライン化がされないため、パフォーマンス面でほんのちょっぴり不利です。
値型ジェネリックを使うとインライン化が効く https://ufcpp.net/study/csharp/sp2_generics.html?p=3#pseudo-static
↑ によれば、値型のジェネリックを使うとインライン化されます。
interface IWork
{
int GetNumber(int number);
}
struct StructWork : IWork
{
public readonly int GetNumber(int number) => number;
}
static int SumNumbers<T>(ReadOnlySpan<int> numbers, T work) where T : IWork
{
int sum = 0;
foreach (var n in numbers)
sum += work.GetNumber(n);
return sum;
}
SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], default(StructWork));
これは値型のジェネリックは実行時その型専用のメソッドを作るのでインライン化されるという仕組みです(参照型のジェネリックはひとつだけメソッドが作られ使い回す)。
サンプルコード
テストコード
using System.Runtime.CompilerServices;
using Xunit;
public class __GenericInliningTest
{
interface IWork
{
int GetNumber(int number);
}
class ClassWork : IWork
{
public int GetNumber(int number) => number;
}
class NoInliningWork : IWork
{
[MethodImpl(MethodImplOptions.NoInlining)]
public int GetNumber(int number) => number;
}
struct StructWork : IWork
{
public readonly int GetNumber(int number) => number;
}
static int SumNumbersInterface(ReadOnlySpan<int> numbers, IWork work)
{
int sum = 0;
foreach (var n in numbers)
sum += work.GetNumber(n);
return sum;
}
static int SumNumbers<T>(ReadOnlySpan<int> numbers, T work) where T : IWork
{
int sum = 0;
foreach (var n in numbers)
sum += work.GetNumber(n);
return sum;
}
static void InterfacePerformance(Performance p)
{
var noInliningWork = new NoInliningWork();
p.AddTest("NoInlining", () =>
{
var sum = 0;
for (var n = 0; n < 10000; n++)
sum += SumNumbersInterface([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], noInliningWork);
});
var classWork = new ClassWork();
p.AddTest("Class", () =>
{
var sum = 0;
for (var n = 0; n < 10000; n++)
sum += SumNumbersInterface([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], classWork);
});
IWork structWork = new StructWork();
p.AddTest("Struct", () =>
{
var sum = 0;
for (var n = 0; n < 10000; n++)
sum += SumNumbersInterface([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], structWork);
});
}
static void InliningPerformance(Performance p)
{
var noInliningWork = new NoInliningWork();
p.AddTest("NoInlining", () =>
{
var sum = 0;
for (var n = 0; n < 10000; n++)
sum += SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], noInliningWork);
});
var classWork = new ClassWork();
p.AddTest("Class", () =>
{
var sum = 0;
for (var n = 0; n < 10000; n++)
sum += SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], classWork);
});
p.AddTest("Struct", () =>
{
var sum = 0;
for (var n = 0; n < 10000; n++)
sum += SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], default(StructWork));
});
}
static void InliningPerformance2(Performance p)
{
var noInliningWork = new NoInliningWork();
p.AddTest("NoInlining", () =>
{
var sum = 0;
for (var n = 0; n < 1000000; n++)
sum += SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], noInliningWork);
});
var classWork = new ClassWork();
p.AddTest("Class", () =>
{
var sum = 0;
for (var n = 0; n < 1000000; n++)
sum += SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], classWork);
});
p.AddTest("Struct", () =>
{
var sum = 0;
for (var n = 0; n < 1000000; n++)
sum += SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], default(StructWork));
});
}
static T Sum<T>(ReadOnlySpan<T> values) where T : struct, System.Numerics.IAdditionOperators<T, T, T>, System.Numerics.INumberBase<T>
{
T result = T.Zero;
foreach (var value in values)
result += value;
return result;
}
[Fact]
void SumTest()
{
Assert.Equal(10, Sum([1, 2, 3, 4]));
Assert.Equal(12f, Sum([1.5f, 2.5f, 3.5f, 4.5f]));
}
}
パフォーマンス比較
var noInliningWork = new NoInliningWork();
p.AddTest("NoInlining", () =>
{
var sum = 0;
for (var n = 0; n < 1000000; n++)
sum += SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], noInliningWork);
});
var classWork = new ClassWork();
p.AddTest("Class", () =>
{
var sum = 0;
for (var n = 0; n < 1000000; n++)
sum += SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], classWork);
});
p.AddTest("Struct", () =>
{
var sum = 0;
for (var n = 0; n < 1000000; n++)
sum += SumNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], default(StructWork));
});
Test | Score | % | GC0 |
---|---|---|---|
x64 | |||
NoInlining | 285 | 100.0% | 0 |
Class | 529 | 185.6% | 0 |
Struct | 1,079 | 378.6% | 0 |
実行環境: Windows11 x64 .NET Runtime 10.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
- インライン化しない版との比較です
- 値型はインライン化され高速です
- 参照型もちょっとインライン化されてね? -> 段階コンパイル (Tiered Compilation) https://ufcpp.net/blog/2018/12/tieredcompilation/
数値型のインターフェイス
.NET Framework 時代は 1 + 1
のような数値計算は、すべての組み込み型のオーバーロードを用意する必要がありました。
今は System.Numerics
名前空間に IAdditionOperators
のような、数値計算用のインターフェイスが用意されており、ジェネリックにできます。
static T Sum<T>(ReadOnlySpan<T> values)
where T : struct, System.Numerics.IAdditionOperators<T, T, T>,
System.Numerics.INumberBase<T>
{
T result = T.Zero;
foreach (var value in values)
result += value;
return result;
}
.NET になってからインターフェイスの拡張がありましたので、この辺の導入に至ったわけです。
参考:C# の静的メンバー https://ufcpp.net/study/csharp/oo_interface.html?p=5#static-member
おわりに
このテクニックはかなりマニアックなもので、自分は実践で使ったことはないです。
近年はランタイム最適化によって、よく使うメソッドはインライン化されるようで(前述の段階コンパイル https://ufcpp.net/blog/2018/12/tieredcompilation/)、プログラマが意識して最適化する必要はなくなりつつあります。
とはいえ現時点では最速っぽいので、どうしてもパフォーマンスが必要なときの飛び道具になりそうです。