この記事にはミスがあります。
自身への戒めとして残しているだけであり、参考になるものではありません。
↓ミスを踏まえてテストし直した改訂版を投稿しました!↓
【改訂版】C#におけるループ処理の速度 ~条件/演算子編~
(今度はミスがないといいなぁ)
#概要
プログラミングにおいて最もボトルネックとなりやすいのが、ループ処理です。
なので、ループ処理の速度向上に役立つ知識を記述していきます。
環境やループ内の処理によっても違いが出るので、あくまでも参考程度に考えてください。
テスト環境
プロセッサ :Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz 3.41 GHz 実装メモリ(RAM):32.0GB システム :64ビットオペレーティングシステム 言語 :C# 7.3 .NET Framework 3.5 ツール :Microsoft Visual Studio 2017 #テスト内容 ループ内で **System.Console.WriteLine()** メソッドを用いて、連続した**100万件**の数字を出力します。 使用するテストデータは **String[] testData;** に格納されています。 時間の計測は **System.Diagnostics.Stopwatch** を使用し、**10回分の平均値**を結果として算出しています。String testData = new String[1000000];
for(Int32 i = 0, len = testData.Length; i < len; i++ )
{
testData[i] = i.ToString();
}
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Reset();
sw.Start();
/* ループ処理 */
sw.Stop();
/* ElapsedMilliseconds メンバから経過したミリ秒を取得 */
Int64 res = sw.ElapsedMilliseconds;
#ループ条件による違い
まずはループの条件による速度の違いです。
一番修正しやすい部分ではないでしょうか。
testData.Lengthプロパティを直接条件に使用する方法と、testData.Lengthプロパティを変数にキャッシュして条件に使用する方法を検証しています。
##結果
結果から記載します。
詳細は後述を参照してください。
条件 | 経過時間(ミリ秒) | 1ループあたり |
---|---|---|
プロパティを使用 | 18,070 | 0.0181 |
キャッシュを使用 | 14,609 | 0.0146 |
キャッシュした方が 3,461ミリ秒(3.461秒) 早いことが分かりました。
1ループあたり 0.0035ミリ秒 の差ですので殆ど誤差ではありますが、数千万回や数億回という膨大なループの際は効果が実感できそうですね。
大きなデータを扱う際や、ミリ秒単位での高速な処理を要求されている場合はキャッシュしてからのループが良いでしょう。
##プロパティを使用
配列の長さ(List や Collection の場合は Count)を示すプロパティを使用してループ条件にする場合です。
/* i < testData.Length の部分に注目 */
for (Int32 i = 0; i < testData.Length; i++)
{
System.Console.WriteLine(testData[i]);
}
結果:18,070 ミリ秒(18.07 秒)
** 1ループあたり0.0181ミリ秒**
##キャッシュを使用
配列の長さ(List や Collection の場合は Count)を変数にキャッシュしてからループ条件にする場合です。
/* len = testData.Length の部分に注目 */
for (Int32 i = 0, len = testData.Length; i < len; i++)
{
System.Console.WriteLine(testData[i]);
}
結果:14,609 ミリ秒(14.61 秒)
** 1ループあたり0.0146ミリ秒**
#インクリメント/デクリメントの違い
インクリメント(i++ など、1加算する演算子)、またはデクリメント演算子(i-- など、1減算する演算子)による違いを検証していきます。
##結果
結果から記載します。
詳細は後述を参照してください。
演算子 | 経過時間(ミリ秒) | 1ループあたり |
---|---|---|
i++ | 16,463 | 0.0165 |
++i | 14,241 | 0.0142 |
i-- | 14,338 | 0.0143 |
--i | 14,721 | 0.0147 |
演算子を後方に置く場合、i++ よりも i-- の方が 2,125ミリ秒(2.125秒) 早いことが分かります。
また、演算子は前方に置いた場合、++i は i++ から 2,222ミリ秒(2.222秒) 早くなっていますが、--i は i-- から 383ミリ秒(0.383秒) 遅くなっています。
100万件による検証結果なので、383ミリ秒は完全に誤差と考えても良いでしょう。
++i と --i の差も 97ミリ秒(0.097秒) と誤差。
なので、基本的には インクリメントよりもデクリメントの方が早いものの、演算子を前方に置く場合はその差はなくなると考えるべきでしょう。
効果が大きいのは、インクリメント演算子を前方に置く ++i だと分かるので、ループ時はデクリメントを使うか、演算子を前方に置きましょう。
ただし、たまにバグの原因となるので、演算子を前方に置いた場合と後方に置いた場合の動きの違いについてはしっかり把握しておきましょう。
C#におけるインクリメント/デクリメント演算子の扱い
インクリメント
###演算子が後方にある場合
インクリメントの演算子が後方にある場合(つまり i++)です。
何だかんだでこれを使っている人が多いのではないでしょうか。
/* 後述するデクリメントとの差異を厳密にするため、キャッシュ方式を採用 */
for (Int32 i = 0, len = testData.Length; i < len; i++)
{
System.Console.WriteLine(testData[i]);
}
結果:16,463 ミリ秒(16.46 秒)
** 1ループあたり0.0165ミリ秒**
###演算子が前方にある場合
インクリメントの演算子が前方にある場合(つまり ++i)です。
慣れている人は結構使う場面があるかもしれませんね。
/* 後述するデクリメントとの差異を厳密にするため、キャッシュ方式を採用 */
for (Int32 i = 0, len = testData.Length; i < len; --i)
{
System.Console.WriteLine(testData[i]);
}
結果:14,241 ミリ秒(14.24 秒)
** 1ループあたり0.0142ミリ秒**
デクリメント
###演算子が後方にある場合
デクリメントの演算子が後方にある場合(つまり i--)です。
/* キャッシュしているようなものなので、
上記インクリメントもキャッシュ方式を採用しています */
for (Int32 i = testData.Length - 1; i >= 0; i--)
{
System.Console.WriteLine(testData[i]);
}
結果:14,338 ミリ秒(14.34 秒)
** 1ループあたり0.0143ミリ秒**
###演算子が前方にある場合
デクリメントの演算子が前方にある場合(つまり --i)です。
/* キャッシュしているようなものなので、
上記インクリメントもキャッシュ方式を採用しています */
for (Int32 i = testData.Length - 1; i >= 0; --i)
{
System.Console.WriteLine(testData[i]);
}
結果:14,721 ミリ秒(14.72 秒)
** 1ループあたり0.0147ミリ秒**
#シリーズ
上から順に書いていく予定です
- ステートメント編
- 多重ループ編
- 小テクニック集