【C#】ループ内での変数宣言をした際の処理速度について


# 【C#】ループ内での変数宣言コストについて

皆さん、こんにちは！

プログラミングにおいて、効率的なコーディングは非常に重要です。
特にループ処理では、無駄な計算を避けることで実行時間を大幅に短縮できる可能性があります。

そんなループ処理について、勉強し始めの頃にループ内で変数宣言をすると処理が重くなるケースがある。みたいなことを勉強した記憶があるのですがあやふやだったため自分で検証してみたいと思います。

## 環境

- Windows11
- Visual Studio 2022

## 前提

処理時間の測定には以下の記事を参考に`System.Diagnostics`に定義されている`Stopwatch`クラスを使用します。

https://qiita.com/Kosen-amai/items/81efaf815b48ab9ffbb6

測定にはこちらのコードをひな型として「処理を記載」とした部分を書き換えて確認をしました。以下処理部分のみを記事に記載します。

```csharp
var sw = Stopwatch.StartNew();

// 処理を記載

sw.Stop();
var ts = sw.Elapsed;
Console.Write("処理にかかった時間：");
Console.WriteLine($"　{ts.Hours}時間 {ts.Minutes}分 {ts.Seconds}秒 {ts.Milliseconds}ミリ秒");
```

## int 型

まずはコーディングをする中で一番使用する機会が多い`int`について調査を行います。

ループ内とループ外で変数宣言を行うコードをそれぞれ作成しました。

【ループ内で宣言】

```Csharp
for (int i = 0; i < 1000000000; i++)
{
    int num = i;
    num++;
}
```

【ループ外で宣言】

```Csharp
int num = 0;
for (int i = 0; i < 1000000000; i++)
{
    num = i;
    num++;
}
```

### 結果

どちらも約 740 ミリ秒ほどで**処理時間に違いはありませんでした。**

## String

続いて`int`と同じくらい使用頻度の高い`String`についても同様の検証を行っていきます。

【ループ内で宣言】

```Csharp
for (int i = 0; i < 1000000000; i++)
{
    string str = i.ToString();
    str += "test";
}
```

【ループ外で宣言】

```Csharp
string str = string.Empty;
for (int i = 0; i < 1000000000; i++)
{
    str = i.ToString();
    str += "test";
}
```

### 結果

`String`でもどちらの処理も約 12.45 秒ほどで**処理時間に違いはありませんでした。** `int`の時の 20 倍近い時間がかかっているのは`ToString()`メソッドの分だと思います。

## 自作クラス

続いて、自分で定義したクラスのインスタンス作成で試してみます。名前と年齢を保持する`Person`クラスを作成しました。

```Csharp
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}
```

【ループ内で宣言】

```Csharp
for (int i = 0; i < 1000000000; i++)
{
    var person = new Person("Taro", 22);
}
```

【ループ外で宣言】

```Csharp
var person1 = new Person("", 0);
for (int i = 0; i < 1000000000; i++)
{
    person1.Name = "Taro";
    person1.Age = 24;
}
```

### 結果

ループ内で宣言した場合が約 8.10 秒、ループ外で宣言した場合が約 4.70 秒と明らかにループ外で宣言したほうが処理速度で優っている結果になりました。

## 自作クラス(メソッドで初期化)

さらに自作クラスのインスタンス作成について調査していきます。上のケースでは処理結果は同じですが宣言時の初期化とプロパティへのアクセスで処理内容に若干の違いがあります。なるべく同じになるように`Person`クラスにプロパティをセットする`SetProperty()`メソッドを追加して測定しました(実際のプロジェクトではこのようなメソッドは使用しないかもしれないですが。。。)。

```Csharp
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public void SetProperty(string name, int age)
    {
        Name = name;
        Age = age;
    }
}
```

【ループ内で宣言】

```Csharp
for (int i = 0; i < 1000000000; i++)
{
    var person = new Person("Taro", 22);
}
```

【ループ外で宣言】

```Csharp
var person1 = new Person("", 0);
for (int i = 0; i < 1000000000; i++)
{
    person1.SetProperty("Taro",22);
}
```

### 結果

ループ内で宣言したほうが約 7.83 秒、ループ外で宣言したほうが約 7.49 秒となり**若干ですがループ外で宣言したほうが早い結果になりました**。メソッドを使用すると同じ処理結果でもプロパティアクセスよりも時間がかかることがわかります。

メソッドをコールするとそれまで保持していた情報等をスタックに退避させ、戻ってきたときにスタックからとってくるという工程が必要になるためその分低速になっています。

## 参考

https://qiita.com/Kosen-amai/items/81efaf815b48ab9ffbb6

https://porinkey.hatenadiary.org/entry/20110504/1304538043

## 雑記：処理の順番について

### 追記(20240715)

@albireo 様にご指摘いただき解決いたしました。ありがとうございました。

合計値を格納していた`sumTs`の値をクリアせずに値を加算していました。。。
皆様はめんどくさがらず、変数をきっちり用意しましょう。

忘れないように反省と教訓の意味も込めて、記事の内容は残すことにしました。

<details><summary>内容</summary>

### 内容

本筋とは外れますが検証中にハマってしまったポイントがあったためご紹介します。

ハマってしまったコードがこちらです。

```Csharp
    public static void Main()
    {
        Console.WriteLine("ループ内で変数宣言した場合");

        var sw = new Stopwatch();
        var sumTs = new TimeSpan(0);
        for (int j = 0; j < 5; j++)
        {
            sw.Restart();
            for (int i = 0; i < 1000000000; i++)
            {
                var person = new Person("Taro", 22);
            }

            sw.Stop();
            var ts = sw.Elapsed;
            sumTs = sumTs + ts;
        }
        sumTs = sumTs /5;
        Console.Write("処理にかかった時間：");
        Console.WriteLine($"　{sumTs.Hours}時間 {sumTs.Minutes}分 {sumTs.Seconds}秒 {sumTs.Milliseconds}ミリ秒");

        Console.WriteLine("\n-------------------------------------------------");

        Console.WriteLine("ループ外で変数宣言した場合");

        for (int j = 0; j < 5; j++)
        {
            sw.Restart();
            var person1 = new Person("", 0);
            for (int i = 0; i < 1000000000; i++)
            {
                person1.SetProperty("Taro", 22);
                //person1.Name = "Taro";
                //person1.Age = 24;
            }

            sw.Stop();
            var ts = sw.Elapsed;
            sumTs = sumTs + ts;
        }
        sumTs = sumTs / 5;
        Console.Write("処理にかかった時間：");
        Console.WriteLine($"　{sumTs.Hours}時間 {sumTs.Minutes}分 {sumTs.Seconds}秒 {sumTs.Milliseconds}ミリ秒");
    }
```

ごちゃごちゃしていますが、"自作クラス(メソッドで初期化)"で測定した「ループ内で変数宣言の測定」→「ループ外で変数宣言の測定」を順番に行っているコードになります。なんてことのないコードに見えますが**実行結果が大問題**です。

```
ループ内で変数宣言した場合
処理にかかった時間：　0時間 0分 7秒 809ミリ秒

-------------------------------------------------
ループ外で変数宣言した場合
処理にかかった時間：　0時間 0分 9秒 557ミリ秒
```

ごらんのとおり、測定結果に大きな差が発生しています。ループ外で変数宣言をしたほうが時間がかかるという結果になっています。どう考えても違和感のある結果です。

「コンストラクタとメソッドの実行速度に差があるのか？コンパイル時の最適化で奇跡的にループ内で宣言したほうが効率的なコードが出力されたのか？」などいろいろ調査しましたが結論から言うと連続で実行していることが問題でした。

その証拠に「ループ**外**で変数宣言の測定」→「ループ**内**で変数宣言の測定」の順番で実行したところ

```
ループ外で変数宣言した場合
処理にかかった時間：　0時間 0分 7秒 580ミリ秒


-------------------------------------------------
ループ内で変数宣言した場合
処理にかかった時間：　0時間 0分 9秒 309ミリ秒
```

上記のようになり今度は「ループ内で宣言」のほうが時間がかかっています。

なぜこのようになっているのかは不明ですが連続で実行すると正確な結果にならないことが判明しました。同じような測定を行われる際はお気を付けください。

</details>

## 最後に

ここまでご覧くださりありがとうございました。
ループ内での変数宣言が処理速度にどの程度影響するのか調査をしていきました。

結論から言うと、`int`や`String`など普段使用する一般的な方は影響がありませんでした。クラスインスタンス作成は処理速度に影響があったため注意が必要です。

今回検証したこと以外にも、クラス内に大量のプロパティがあるほうがインスタンス作成に時間がかかるのか？など疑問点もあるため折を見て確認してみたいと思います。

この記事で皆様のコーディングライフの助けになれれば幸いです！
ではまた次の記事で！！