読者です 読者をやめる 読者になる 読者になる

C++11 forループの速度比較と、コンパイラのループ展開

C++ コンパイラ

C++11 forループの速度比較と、コンパイラのループ展開についてテストをしたときの結果です。構造体のメンバを、5で割った余りごとに分けて、頻度を数えるようなソースコードで実験しました(ソースコードは最後にあります)。プロセッサは、Intel(R) Core(TM) i7-3632QM CPU @ 2.20GHzですが、実行結果は、コンパイラ・CPU・メモリ・キャッシュで大きく異ってくると思うので、ざっくりとだけ参考にしてもらえればと思います。

1. ループの方法ごとの速度の比較

Visual Studio 2012は、Release、その他は初期設定のままでコンパイルしました。
g++ 4.8.2は、--std=c++0x -O2 -W -Wall -Wno-sign-compareでコンパイルしました。

方法 Visual Studio 2012(秒) g++ 4.8.2(秒)
(1) 通常のforループ 2.995 3.562
(2) 通常のforループとconst参照 3.035 3.579
(3) c++11のrange-based forループ 3.604 3.502
(4) for_eachとc++11のラムダ式 3.619 3.462
(5) for_eachと関数オブジェクト 3.604 3.477
  • Visual Studio 2012で(1)(2)が速いのは、ループ展開が行われているからです。g++ 4.8.2は、(1)から(5)までおおよそ同じ時間です。デフォルトのオプションでは、ループ展開しないようです。
  • (4)と(5)は、http://msdn.microsoft.com/ja-jp/library/dd293608.aspx にあるように、C++11のラムダ式は関数オブジェクトを使用したものと同じになります。実際に、Visual Studio 2012でコンパイルすると、DebugでもReleaseでも(4)と(5)は、全く同じアセンブリになりました。

2. コンパイラのループ展開の仕方

(同じコードを使いましたが、ちょっと前に違う状態で実験したので、当時の簡易的な実験の結果として見てもらえれば)

ループ展開とは、コンパイラの高速化の手法です。

for (int i = 0; i < 10000; ++i)
{
	frequency[positions[i].x%SZ(frequency)]++;
}

上のようなコードを、下のようなコードに置き換えて、ループ回数を減らします。(動作はかわりません)

for (int i = 0; i < 10000/5; ++i)
{
	frequency[positions[i*5].x%SZ(frequency)]++;
	frequency[positions[i*5+1].x%SZ(frequency)]++;
	frequency[positions[i*5+2].x%SZ(frequency)]++;
	frequency[positions[i*5+3].x%SZ(frequency)]++;
	frequency[positions[i*5+4].x%SZ(frequency)]++;
}

メリット・デメリットについて、詳細はこちらのリンクを参考にしてみてください→ http://ja.wikipedia.org/wiki/%E3%83%AB%E3%83%BC%E3%83%97%E5%B1%95%E9%96%8B

Visual Studio 2012
  • Nを定数にした場合、6以下の約数で最も大きい個数に展開するようです。Nが素数のときや、2でしか割り切れないときは遅くなりました。
N ループ展開の個数 実行時間(秒)
9996 4 2.839
9997 1 3.213
9998 2 3.136
9999 3 2.854
10000 5 2.871
10001 1 3.276
10002 6 2.855
10003 1 3.214
  • Nを変数にした場合(標準入力でNに値を渡した場合)は、ループ展開はされませんでした。
g++ 4.8.2
  • Nを定数にした場合、コンパイルオプションに-funroll-loopsを追加すると、ループ展開されます。12個までループされる展開を確認できましたが、8個や12個まで展開されたときは、かえって遅くなりました。
  • Nを変数にした場合(標準入力でNに値を渡した場合)は、 -funroll-all-loopsを追加すると、ループ展開されます。この場合、8個にループ展開されましたが、Nが8で割り切れないことを考えて、ループを途中で抜けるジャンプ命令が入ってしまうため、かえって遅くなってしまいます。


ソースコードです。

#include <vector>
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <fstream>

using namespace std;
typedef long long ll;
#define SZ(a) ((int)a.size()) 

#include <sys/time.h>
unsigned long long getTime() {
	struct timeval tv;
	gettimeofday(&tv, NULL);
	unsigned long long  result =  tv.tv_sec * 1000LL + tv.tv_usec / 1000LL;
	return result;
}

struct POS
{
	int x;
	int y;
};


class FunctorClass
{
public:
	explicit FunctorClass(vector <int>& frequency) 
		: m_frequency(frequency)
	{
	}

	void operator()(const POS& pos) const
	{
		m_frequency[pos.x%SZ(m_frequency)]++;
	}

private:
	FunctorClass& operator=(const FunctorClass&);
	vector <int>& m_frequency;
};



int main()
{
	int N = 10000;
	vector <POS> positions(N);
	for (int i = 0; i < N; ++i)
	{
		positions[i].x = rand();
		positions[i].y = rand();
	}

	const int DIVIDE = 5;
	vector <int> frequency(DIVIDE);

	const ll start = getTime();

	for(int k = 0; k < 100000; ++k)
	{
#if 1
		// (1) 通常のforループ
		for (int i = 0; i < N; ++i)
		{
			frequency[positions[i].x%SZ(frequency)]++;
		}
#elif 0
		// (2) 通常のforループとconst参照
		for (int i = 0; i < N; ++i)
		{
			const POS& p = positions[i];
			frequency[p.x%SZ(frequency)]++;
		}
#elif 0
		// (3) c++11のrange-based forループ
		for (const POS& p : positions)
		{
			frequency[p.x%SZ(frequency)]++;
		}
#elif 0
		// (4) for_eachとc++11のラムダ式
		for_each(positions.begin(),positions.end(), [&frequency] (const POS& pos) {
			frequency[pos.x%SZ(frequency)]++;
		});
#else
		// (5) for_eachと関数オブジェクト
		for_each(positions.begin(),positions.end(), FunctorClass(frequency));
#endif
	}

	cout << " Time=" << getTime()-start << endl;

	for (int i = 0; i < SZ(frequency); ++i)
	{
		cout << " frequency[" << i << "]=" << frequency[i] << endl;
	}

	return 0;
}