【.NET 10 Preview1】インターフェースを介したforeachのパフォーマンスが上がるらしい

先日発表された.NET 10 Preview1 ではランタイムを最適化するための3つの機能強化が追加されました。
その一つである「**配列インターフェースメソッドの脱仮想化(Array Interface Method Devirtualization)**」について調べてみると、色々と面白いことを知ったので紹介します。
間違ってたらごめんなさい。

# 何が改善されたのか
.NET 10 では、JIT(Just-In-Time コンパイラ)が配列をインターフェース経由で操作する際の最適化を強化しました。これにより、`foreach`を使って`IEnumerable<T>`として配列をループする場合でも、従来の`for`ループと同等のパフォーマンスが期待できるようになりました。
簡単に言うと、**インターフェースを介した`foreach`のパフォーマンスが大幅に向上**しました。

# 背景
インターフェースを介した`foreach`のパフォーマンスが良くなかった要因としては、主に以下の二つが挙げられます。
1. 境界チェック(Bounds Checking)
1. 仮想呼び出し(Virtual Call)

### 1.境界チェック(Bounds Checking)
C#では、配列の要素にアクセスするたびに境界チェック(Bounds Checking)が行われます。これは、**配列のインデックスが範囲外でないかを確認する仕組み**です。
```c#
int[] array = { 1, 2, 3 };
int value = array[5]; // 範囲外のアクセス → IndexOutOfRangeException
```
JITは、`for`ループのようにインデックスが範囲内であることが明確な場合、不要な境界チェックを削除してパフォーマンスを向上させます。
```c#
public int Sum(int[] array)
{
    int sum = 0;
    for (int i = 0; i < array.Length; i++) // iが0からarray.Length-1までの範囲に収まることが明確
    {
        sum += array[i]; // JITによる境界チェックの省略
    }
    return sum;
}
```
しかし、`foreach`ループを使い`IEnumerable<T>`として配列を扱うと、インデックスが範囲内であることを毎回確認する必要が生じるため、この最適化が難しくなります。

### 2.仮想呼び出し(Virtual Call)
仮想呼び出し(Virtual Call)とは、**メソッドの呼び出し先が実行時に決定されること**です。C#の`interface`や`virtual`メソッドを使うと、どのメソッドが実行されるかJITは事前に確定できません。
```c#
interface IAnimal
{
    void Speak();
}

class Dog : IAnimal
{
    public void Speak() => Console.WriteLine("Woof!");
}

class Cat : IAnimal
{
    public void Speak() => Console.WriteLine("Meow!");
}

static void MakeAnimalSpeak(IAnimal animal)
{
    animal.Speak(); // 仮想呼び出し
}
```
`MakeAnimalSpeak(new Dog());`を実行すると、JITはDogかCatどちらの`Speak()`を呼ぶのかを事前には特定できないため、仮想テーブル(vtable)を参照し、実行時にメソッドを決定します。この追加処理がパフォーマンスの低下を招きます。

### `foreach`の最適化が難しかった理由
以上の内容を踏まえると、`foreach`の最適化が難しかった理由が見えてきます。
`foreach`ループを使用すると、コンパイラは内部で以下のような処理を実行します。
1. `GetEnumerator()`メソッドを呼び出して、イテレーター(`IEnumerator<T>')を取得する
1. `MoveNext()`メソッドを毎回呼び出して、次の要素に進む
1. `Current`プロパティを取得して、現在の値を参照する

つまり、`foreach`を`IEnumerable<T>`として扱うと、仮想呼び出しが発生するためJITはメソッドのインライン化を適用できず、結果としてループ展開や境界チェック削除などの最適化が難しくなるのです。
```c#
public int Sum(int[] array)
{
    int sum = 0;
    IEnumerable<int> temp = array;

    foreach (var num in temp) // 仮想呼び出しが発生
    {
        sum += num;
    }
    return sum;
}
```

# どう変わる？
.NET 10 では、JITが`IEnumerable<T>`を`T[]`だと事前に特定できる場合は**仮想呼び出しを回避**し、直接`T[]`のメソッドを呼ぶよう最適化されるようになります。
### 事前に特定できる場合って？
* 代入時に`T[]`であることが明確な場合
```c#
static int Sum(int[] array)
{
    int sum = 0;
    IEnumerable<int> temp = array; // `temp`に`int[]`を直接代入

    foreach (var num in temp) // .NET 10 では脱仮想化される
    {
        sum += num;
    }
    return sum;
}
```
* foreachの対象が`T[]`であるとJITが確信できる場合
```c#
static void Print(IEnumerable<int> collection)
{
    foreach (var num in collection) 
    {
        Console.WriteLine(num);
    }
}

static void Main()
{
    int[] numbers = { 1, 2, 3 };
    Print(numbers); // `Print(IEnumerable<int>)` に `int[]` を直接渡す
}
```
* `foreach`のループ対象がローカル変数で`T[]`に型推論される場合
```c#
static int Sum(IEnumerable<int> collection)
{
    int sum = 0;

    foreach (var num in collection) 
    {
        sum += num;
    }

    return sum;
}

static void Main()
{
    int[] numbers = { 1, 2, 3 };
    Console.WriteLine(Sum(numbers)); // `numbers` は `int[]`
}
```
などなど


# まとめ
.NET 10 のJIT最適化により、インターフェースを経由した`foreach`ループのパフォーマンスが大幅に向上しました。これにより、**可読性の高い`foreach`を使用しても、`for`ループと同等のパフォーマンスを期待できるようになります。**
個人的な感想としては、そもそもインターフェースを経由した`foreach`ループのパフォーマンスが比較的悪かったことを全く知らなかったので、面白い発見でした。

# 参考
* https://learn.microsoft.com/ja-jp/dotnet/core/whats-new/dotnet-10/runtime
* https://www.bytehide.com/blog/whats-new-in-dotnet-10#NET_10_Runtime_Improvements