「C# が C++ の速度を凌駕している」らしいので、C++側を高速化してみた

# 概要
　2016年5月2日に出たこの記事を見て驚きました。同じようなコードでベンチマークした結果、**C++がC# unsafeに負ける**というとんでもないことになっているではありませんか！
　[気づいたら、C# が C++ の速度を凌駕している！ - espresso3389の日記](http://espresso3389.hatenablog.com/entry/2016/05/02/081308
)
　というわけで、C++側にも逆転の目はないのか、調査してみることにしました。

# まずは小手調べ
　この記事の主張としては、「昔に組んだ画像処理(を模した)コードでベンチマークしたところ僅差だったので、再度トライしたら**C#が勝ってしまった**」ということ。実験条件は明記されていますので、まずは追試してみましょう。
　とは言っても、コンパイラはともかくCPUまで揃えることは現実的ではありません。したがって次のような条件になることをお許し下さい。

> Windows 10 Pro バージョン 1511(OSビルド 10586.218) 64bit版
> .NET Framework 4.6.1
> Visual Studio 2015 Update 2
> CPU: Intel Core i7-4790K(定格、TBは切ってない)
> Memory: 12GB(DDR3)
> csc.exe バージョン 1.2.0.60317
> cl.exe バージョン 19.00.23918 for x64

　説明通りのコンパイルオプションでコンパイル(64bit)して各10回実行すると、次のような結果になりました(単位はミリ秒)。

|言語|速度(最速)|速度(平均)|速度(最遅)|
|----|----------|----------|----------|
|C#(test1)|2320|2346.6|2382|
|C#(test2)|1875|1893|1913|
|C++|**1594**|**1621.9**|**1656**|

　……この時点で**C++が勝っている**気がしますが、話を進めましょう。

# コードをモダンにしてみる
　コードが5年前基準だからか、C++側のコードが凄く怪しく見えます。Windowsべったりのコードですし、少し手直しするだけでC89としてコンパイルが通りそうなぐらいです。というわけで、C++11基準で書き直してみました。

```speedtest2.cpp
// Compile: cl speedtest2.cpp /O2 /GL /EHsc
#include<chrono>
#include<cstdint>
#include<iostream>

using Byte = uint8_t;

void test(){
	//! 画面サイズを設定する
	size_t w = 4321, h = 6789;
	//! ストライドの大きさを計算する
	//! (横幅を4で割り切れる画素数にする)
	size_t stride = ( w + 3 ) & ~3;
	//! 領域を確保する(各画素は1バイト)
	auto *a = new Byte[stride * h];
	//! 二次元直交座標のX・YのXORを画素に代入する
	//! (擬似的な画像処理のテスト)
	for(size_t y = 0; y < h; y++){
		auto p = a + y * stride;
		for(size_t x = 0; x < w; x++){
			p[x] = static_cast<Byte>(x ^ y);
		}
	}
	delete[] a;
}

int main(){
	//! 計測開始
	auto start = std::chrono::system_clock::now();
	//! 計測用コード
	for(size_t i = 0; i < 100; ++i){
		test();
	}
	//! 計測終了・集計
	auto end = std::chrono::system_clock::now();
	auto msec = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
	std::cout << msec << "[ms]" << std::endl;
}
```

　ベンチマーク結果は次の通り。少し遅くなったように見えるのは、XORする際の数値がintからsize_tになったことが影響しているのでしょうか……？　~~そもそもどういった処理なんだこれ~~

|言語|速度(最速)|速度(平均)|速度(最遅)|
|----|----------|----------|----------|
|C++(その2)|1695|1706.8|1723|

# SIMD intrinsicを適用してみる
　画像処理と言えばSIMDだと[ばっちゃが言ってました](http://moto-neta.com/anime/bacchagaitteta/)ので、更なる最適化を図ります。画素がByte型(1byte＝今回は8bit)で、横幅が4の倍数になるようにしていますので、基本的には32bit単位で扱えるわけですね。~~実際には元データがRGBAだったりするので128bitとして扱いやすくなるのですが~~
　元のテスト環境では、SandyBridgeの[Core i7-3770S](http://ark.intel.com/ja/products/65524/Intel-Core-i7-3770S-Processor-8M-Cache-up-to-3_90-GHz)が使用されていました。したがって、整数演算としてAVX2は使えず、SSEナントカ(128bit)で処理することになります。つまり、おおよそこういったコードになるでしょう。

```speedtest3.cpp
//! Compile: / GS / GL / analyze - / W3 / Gy / Zc:wchar_t / Zi / Gm - / O2 / sdl
//!          / Fd"Release\vc140.pdb" / Zc : inline / fp : precise / D "WIN32"
//!          / D "NDEBUG" / D "_CONSOLE" / D "_UNICODE" / D "UNICODE"
//!          / errorReport : prompt / WX - / Zc : forScope / arch : AVX / Gd
//!          / Oy - / Oi / MD / Fa"Release\" /EHsc /nologo /Fo"Release\"
//!          /Fp"Release\sample.pch" 

#include<chrono>
#include<cstdint>
#include<iostream>
#include<immintrin.h>

using Byte = uint8_t;

void test(){
	//! 画面サイズを設定する
	const size_t w = 4321, h = 6789;
	//! ストライドの大きさを計算する
	//! (横幅を4で割り切れる画素数にする)
	const size_t stride = ( w + 3 ) & ~3;
	//! 領域を確保する(各画素は1バイト)
	//! 本来ならアラインメントを揃えたいところだが……
	auto *a = new Byte[stride * h];
	//! 二次元直交座標のX・YのXORを画素に代入する
	//! (擬似的な画像処理のテスト)
	for(size_t y = 0; y < h; y++){
		auto p = a + y * stride;
		const auto simd_y = _mm_set1_epi8(y);
		for(size_t x = 0; x < (w >> 4) << 4; x += 16){
			const auto simd_x = _mm_set_epi8(
				x+15,x+14,x+13,x+12,x+11,x+10,x+ 9,x+ 8,
				x+ 7,x+ 6,x+ 5,x+ 4,x+ 3,x+ 2,x+ 1,x+ 0);
			const auto simd_xor = _mm_xor_si128(simd_x, simd_y);
			_mm_storeu_si128((__m128i*)(p + x), simd_xor);
		}
		for(size_t x = (w >> 4) << 4; x < w; ++x){
			p[x] = static_cast<Byte>(x ^ y);
		}
	}
	delete[] a;
}

int main(){
	//! 計測開始
	auto start = std::chrono::system_clock::now();
	//! 計測用コード
	for(size_t i = 0; i < 100; ++i){
		test();
	}
	//! 計測終了・集計
	auto end = std::chrono::system_clock::now();
	auto msec = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
	std::cout << msec << "[ms]" << std::endl;
}
```

　ベンチマーク結果は次の通り。どういうわけか**プロジェクトファイルを組んでIDEからコンパイルした方が速かった**ので、その際のコンパイルオプションをコメントに付記しています。ちなみにそのままAVX2化(256bit毎処理するように)するとなぜか遅くなりました……。

|言語|速度(最速)|速度(平均)|速度(最遅)|
|----|----------|----------|----------|
|C++(その3)|1460|1474.3|1489|

# まとめ
　いずれにせよ、C++の方が一応速いようです。より画像処理らしいコードでも対決してみたいですね。

|言語|速度(最速)|速度(平均)|速度(最遅)|
|----|----------|----------|----------|
|C#(test1)|2320|2346.6|2382|
|C#(test2)|1875|1893|1913|
|C++|1594|1621.9|1656|
|C++(その2)|1695|1706.8|1723|
|C++(その3)|**1460**|**1474.3**|**1489**|

# おまけ
　元コードのボトルネックを検証してみたところ、**xとyとのXOR結果をByteにキャストする部分が一番重い**という身も蓋もない結論になりました。ご査収ください。
(プロファイリング用に、C++側は意味を変えない範囲内でコードを改変しています)

**※C++**
![2016-05-02_213015.png](https://qiita-image-store.s3.amazonaws.com/0/29802/563a3832-4bb8-86f5-3c49-44f361e02e8d.png)
**※C#(test2)**
![2016-05-02_213316.png](https://qiita-image-store.s3.amazonaws.com/0/29802/960611e2-37d0-7bc0-6ca5-1aa29ab54a8d.png)