2021年9月21日火曜日

Visual Studio のLoop Unrollingは、制御不可

ループアンローリングは、勝手に入ったり入らなかったりします。コンパイルオプション、#pragma を試してみましたが、試した限り効力はありませんでした。

1重ループでは、何をやってもループアンローリングしてしまいます。しかし、2重ループ(run_code1A)では、ループアンローリングは入りませんでした。

while (address_cnt < n) {
			int col = address_ptr[address_cnt];
			float f = *(fp + col);
			__m128 B = _mm_load_ss(fp + address_ptr[address_cnt]);
			sum = _mm_add_ps(sum, B);
			++address_cnt;
		}

自前のループアンローリングは、次のように記述しています。

		}//Unrolling
#define USE_MY_UNROLLING
#ifdef USE_MY_UNROLLING
		while (address_cnt < n - 4) {

			__m128 B = _mm_load_ss(fp + address_ptr[address_cnt]);
			__m128 C = _mm_load_ss(fp + address_ptr[address_cnt + 1]);
			__m128 D = _mm_load_ss(fp + address_ptr[address_cnt + 2]);
			__m128 E = _mm_load_ss(fp + address_ptr[address_cnt + 3]);
			B = _mm_add_ps(B, C);
			D = _mm_add_ps(D, E);
			B = _mm_add_ps(B, D);
			sum = _mm_add_ps(sum, B);

			address_cnt += 4;
		}
#endif
		while (address_cnt < n) {
			int col = address_ptr[address_cnt];
			float f = *(fp + col);
			__m128 B = _mm_load_ss(fp + address_ptr[address_cnt]);
			sum = _mm_add_ps(sum, B);
			++address_cnt;
		}

ベクトル化手法を取らなくても、上の記述だけで、Eigenより速くなりました。(

Eigenは、SparseSpMVについて明示的アンローリングは行っていません。)

 (instance19:マルチスレッド数4、FirstOrderIteration10万回 SpMVあたりのデータ)

■Eigen  :3.96sec

■上記コード(ループアンローリングなし):3.42sec

■               (ループアンローリングあり):2.996sec

 これをマルチスレッド化するには、OpenMPの ScheduleをDynamicにして、さらにchunk数を指定すると上手く動作しました。Eigenの記述をそのまま使いました。 以上、ベクトル化前でも、ループアンローリング記述を追加するだけで、10-20%程度は、速度アップを期待できます。また、下のように2重ループで組んでも、データ的には、連続的に一重ループとして記述することが、重要なようです。キャッシュに収まり易いこともありますが、生成されるコードもシンプルになります。今回のようなクリティカルな関数では、一つのアセンブリコードの増減が速度に直結します。

void run_code1A(row_relay_struct* rrs, int rows, unsigned short* address_ptr, unsigned short* coeff_address_ptr, __m128* ifptr, float* ofptr, float* coeff_ptr) {


	/*LUT				 __m128
		rrs rows		 row_relay_struct rows

		one_address_ptr  unsigned short k
		address_ptr	     unsigned short n
		bits_ptr         unsigned char  n
		coff_address_ptr        unsigned short m
		coff_ptr				float          m




	*/
	int threads = Eigen::nbThreads();

#pragma omp parallel for schedule(dynamic,(rows+threads*4-1)/(threads*4)) num_threads(threads)
	
	for (auto row = 0;row < rows;++row) {
	
		__m128 sum = { 0,0,0,0 };



		int n = rrs[row].n;
		//assert(one_address_ptr + n == address_ptr);

		int address_cnt = rrs[row].address_cnt;
		if (row) {
			assert(rrs[row - 1].n == address_cnt);
		}
		float* fp = reinterpret_cast(ifptr);

		//Unrolling
#define USE_MY_UNROLLING
#ifdef USE_MY_UNROLLING
		while (address_cnt < n - 4) {

			__m128 B = _mm_load_ss(fp + address_ptr[address_cnt]);
			__m128 C = _mm_load_ss(fp + address_ptr[address_cnt + 1]);
			__m128 D = _mm_load_ss(fp + address_ptr[address_cnt + 2]);
			__m128 E = _mm_load_ss(fp + address_ptr[address_cnt + 3]);
			B = _mm_add_ps(B, C);
			D = _mm_add_ps(D, E);
			B = _mm_add_ps(B, D);
			sum = _mm_add_ps(sum, B);

			address_cnt += 4;
		}
#endif
		while (address_cnt < n) {
			int col = address_ptr[address_cnt];
			float f = *(fp + col);
			__m128 B = _mm_load_ss(fp + address_ptr[address_cnt]);
			sum = _mm_add_ps(sum, B);
			++address_cnt;
		}
		n = rrs[row].m;
		address_cnt = rrs[row].coff_cnt;
		if (row) {
			assert(rrs[row - 1].m == address_cnt);
		}
		while (address_cnt < n) {
			unsigned short col = coeff_address_ptr[address_cnt];

			float* fp = reinterpret_cast(ifptr);
			__m128 c = _mm_load_ss(fp + col);
			__m128 d = _mm_load_ss(coeff_ptr + address_cnt);
			c = _mm_mul_ss(c, d);
			sum = _mm_add_ps(sum, c);
			++address_cnt;
		}
		_mm_store_ss(ofptr + row, sum);
	}
	
}

まとめ 1)VisualStudioは、ループアンローリングは、制御できない 2)ループアンローリング効果は、10%-20%程度 3)OpenMp for threadingは、staticよりもchunk付きdynamic scheduling が速い 4)2重ループでもデータは、シーケンシャルで1重ループに。

0 件のコメント:

コメントを投稿