疑問
C# では、配列やオブジェクトを作成する際は、new
を用いる。
これは、メモリを動的確保する操作であると考えられる。
そして、確保した後、明示的に開放せずに投げ捨てることが多い。
すると、たとえばこれをループ内で毎回行うと、大量の使用済みオブジェクトをガベージコレクションで処理することになり、効率が悪くなるのではないだろうか?
これを回避するため、ループ内で毎回newするのではなく、ループに入る前にnewを行い、生成した配列やオブジェクトを使い回すべきではないだろうか?
予想
大量に (ループ内で) new により配列を生成すると、使用済み配列をガベージコレクションするコストがかかり、効率が悪くなる。
実験
以下のコードを実行し、ループ処理にかかった時間を Stopwatch
で計測する。
これは、配列を用いた適当な処理を行うプログラムである。
ループ内で毎回newを行うモード new_every_iter
と、ループ前の最初にnewを行うモード new_first
をコマンドラインから指定することができる。
using System;
using System.Diagnostics;
class NewTest
{
public static void Main(string[] args)
{
if (args.Length != 3)
{
Console.WriteLine("Usage: NewTest num_loop num_elem mode");
return;
}
int numLoop = int.Parse(args[0]);
int numElem = int.Parse(args[1]);
string mode = args[2];
if ("new_every_iter".Equals(mode))
{
Stopwatch sw = new Stopwatch();
sw.Start();
long allSum = 0;
for (int i = 1; i <= numLoop; i++)
{
int[] arr = new int[numElem];
for (int j = 0; j < numElem; j++)
{
arr[j] = (int)((long)j * j % numElem);
}
int sum = 0;
for (int j = 0; j < numElem; j++)
{
sum = (sum + arr[j]) % i;
}
allSum += sum;
}
sw.Stop();
Console.WriteLine(allSum);
Console.WriteLine(sw.Elapsed.TotalSeconds);
}
else if ("new_first".Equals(mode))
{
Stopwatch sw = new Stopwatch();
sw.Start();
long allSum = 0;
int[] arr = new int[numElem];
for (int i = 1; i <= numLoop; i++)
{
for (int j = 0; j < numElem; j++)
{
arr[j] = (int)((long)j * j % numElem);
}
int sum = 0;
for (int j = 0; j < numElem; j++)
{
sum = (sum + arr[j]) % i;
}
allSum += sum;
}
sw.Stop();
Console.WriteLine(allSum);
Console.WriteLine(sw.Elapsed.TotalSeconds);
}
else
{
Console.WriteLine("unknown mode");
}
}
}
これを、Windows 11 付属の csc
でコンパイルした。
csc NewTest.cs
csc
のバージョンは以下である。
Microsoft (R) Visual C# Compiler version 4.8.9232.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.
さらに、コンパイルしたバイナリを ILSpy で逆コンパイルし、処理内容を確認した。
結果は以下である。
// NewTest, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
// NewTest
using System;
using System.Diagnostics;
public static void Main(string[] args)
{
if (args.Length != 3)
{
Console.WriteLine("Usage: NewTest num_loop num_elem mode");
return;
}
int num = int.Parse(args[0]);
int num2 = int.Parse(args[1]);
string value = args[2];
if ("new_every_iter".Equals(value))
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
long num3 = 0L;
for (int i = 1; i <= num; i++)
{
int[] array = new int[num2];
for (int j = 0; j < num2; j++)
{
array[j] = (int)((long)j * (long)j % num2);
}
int num4 = 0;
for (int j = 0; j < num2; j++)
{
num4 = (num4 + array[j]) % i;
}
num3 += num4;
}
stopwatch.Stop();
Console.WriteLine(num3);
Console.WriteLine(stopwatch.Elapsed.TotalSeconds);
}
else if ("new_first".Equals(value))
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
long num3 = 0L;
int[] array = new int[num2];
for (int i = 1; i <= num; i++)
{
for (int j = 0; j < num2; j++)
{
array[j] = (int)((long)j * (long)j % num2);
}
int num4 = 0;
for (int j = 0; j < num2; j++)
{
num4 = (num4 + array[j]) % i;
}
num3 += num4;
}
stopwatch.Stop();
Console.WriteLine(num3);
Console.WriteLine(stopwatch.Elapsed.TotalSeconds);
}
else
{
Console.WriteLine("unknown mode");
}
}
最適化によるまとめなどが行われず、ループやnewが意図通り行われていることがわかる。
実験結果
要素数 num_elem
は 1000
とし、ループ回数 num_loop
を変えて実行時間 (秒) の測定を行った。
それぞれ3回測定して平均をとり、小数第4位を四捨五入した。
「性能差」は、「(毎回newの実行時間 - 最初にnewの実行時間)÷最初にnewの実行時間」である。
ループ回数 | 毎回new | 最初にnew | 性能差 |
---|---|---|---|
100000 | 0.588 | 0.560 | 5.011 % |
1000000 | 5.858 | 5.747 | 1.938 % |
10000000 | 57.984 | 56.246 | 3.090 % |
100000000 | 577.003 | 566.640 | 1.829 % |
結論
ループの前に1回だけnewをするよりも、ループ時に毎回newをしたほうが、若干効率が悪くなった。
とはいえ、その差はわずかなものであり、ループ回数を増やしてもあまり変わらなかった。
懸念していたような「オブジェクトが大量に使い捨てられ、ガベージコレクションにより処理を行う分効率が悪くなる」という現象は起こらないようである。
処理時間に十分余裕があるのであれば、ループ内で毎回newをしてもよさそうだ。
とはいえ、これはあくまで今回の実験結果では、の話であり、効率は実際の処理内容によって変わる可能性がある。
たとえば、コンストラクタの処理は重いが、メソッドで初期化して再利用するのは効率よくできるオブジェクトであれば、毎回newせずに使いまわした方がよい可能性もある。
基本はわかりやすい・書きやすい書き方をし、効率を考えるのは実際にパフォーマンスの問題が発生してからにするのがよいだろう。