11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

何が改善されたのか

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

背景

インターフェースを介したforeachのパフォーマンスが良くなかった要因としては、主に以下の二つが挙げられます。

  1. 境界チェック(Bounds Checking)
  2. 仮想呼び出し(Virtual Call)

1.境界チェック(Bounds Checking)

C#では、配列の要素にアクセスするたびに境界チェック(Bounds Checking)が行われます。これは、配列のインデックスが範囲外でないかを確認する仕組みです。

int[] array = { 1, 2, 3 };
int value = array[5]; // 範囲外のアクセス → IndexOutOfRangeException

JITは、forループのようにインデックスが範囲内であることが明確な場合、不要な境界チェックを削除してパフォーマンスを向上させます。

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#のinterfacevirtualメソッドを使うと、どのメソッドが実行されるかJITは事前に確定できません。

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')を取得する
  2. MoveNext()メソッドを毎回呼び出して、次の要素に進む
  3. Currentプロパティを取得して、現在の値を参照する

つまり、foreachIEnumerable<T>として扱うと、仮想呼び出しが発生するためJITはメソッドのインライン化を適用できず、結果としてループ展開や境界チェック削除などの最適化が難しくなるのです。

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[]であることが明確な場合
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が確信できる場合
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[]に型推論される場合
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ループのパフォーマンスが比較的悪かったことを全く知らなかったので、面白い発見でした。

参考

11
5
2

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
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?