Quantcast
Channel: ループタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 92

C#におけるループ処理の速度 ~条件/演算子編~

$
0
0

この記事にはミスがあります。
自身への戒めとして残しているだけであり、参考になるものではありません。

↓ミスを踏まえてテストし直した改訂版を投稿しました!↓
【改訂版】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回分の平均値を結果として算出しています。

100万件のテストデータを作成するロジック
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ミリ秒 の差ですので殆ど誤差ではありますが、数千万回や数億回という膨大なループの際は効果が実感できそうですね。
大きなデータを扱う際や、ミリ秒単位での高速な処理を要求されている場合はキャッシュしてからのループが良いでしょう。

プロパティを使用

配列の長さ(ListCollection の場合は Count)を示すプロパティを使用してループ条件にする場合です。

処理
/* i < testData.Length の部分に注目 */
for (Int32 i = 0; i < testData.Length; i++)
{
    System.Console.WriteLine(testData[i]);
}

結果:18,070 ミリ秒(18.07 秒)
   1ループあたり0.0181ミリ秒

キャッシュを使用

配列の長さ(ListCollection の場合は 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秒) 早いことが分かります。
また、演算子は前方に置いた場合、++ii++ から 2,222ミリ秒(2.222秒) 早くなっていますが、--ii-- から 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ミリ秒

シリーズ

上から順に書いていく予定です

  • ステートメント編
  • 多重ループ編
  • 小テクニック集

Viewing all articles
Browse latest Browse all 92

Trending Articles