もちゅるの日常

特に決まっていないざっくばらんなブログ

マルチスレッドとキャッシュとメンバ変数

id:motom552:20101031
上の記事の追記に近い覚書

マルチスレッド開発はメモリを愛する事から始める

常にメモリの事を考えながら開発しないと、すぐにパフォーマンスに影響出る事が分かった。

処理内容

単純に0から1<<31までのループ値を加算し総合値を取得する。(0+1+2〜)

シングルスレッド

まあ書く意味あるのかってくらい単純。

rn_uint32 total = 0;
{
	const rn_uint32 endIdx = loop;// loopは(1<<31)
	for( rn_uint32 i=0; i<endIdx; ++i){
		total += i;
	}
}
マルチスレッド

こっちはスレッドクラスを使います。
作成中のクラスを継承してますが、スレッド側からExcute()が呼ばれる事には変わりないです。
下記のようなスレッドクラスを用意しインスタンスを4つ用意する。
インスタンスごとにループ領域を分ける。

class Job	: public RinThreadJob
{
	rn_uint32 m_sum;
	rn_uint32 m_beginIdx;
	rn_uint32 m_endIdx;
//	rn_uint8 m_pad[1<<8];
public:
	Job():m_sum(0),m_beginIdx(0),m_endIdx(0){}
	void Execute()
	{
		rn_uint32 total = 0;
		const rn_uint32 endIdx = m_endIdx;
		for( rn_uint32 i=m_beginIdx; i<endIdx; ++i){
			m_sum += i;
//			total += i;
		}
//		m_sum = total;
	}
	rn_uint32 GetSum(void)const{
		return m_sum;
	}
	void SetRange( const rn_uint32 begin, const rn_uint32 end ){
		m_beginIdx = begin; m_endIdx = end;
	}
};

コメントアウトは後述。
メイン関数部分は下記のように配列で4つ用意します。

void main()
{
	Job job[4];
}

結果

実行タイプ 最小[ms] 最大[ms]
シングルスレッド 6606 6624
マルチスレッド 4101 6959

まあこれぐらいの速度差ならマルチにする価値はないわけで。

理由

そして理由は最初に紹介している記事と同じ理由で、キャッシュの再取得が発生しているため。
メイン関数で配列4つ宣言するということは、メモリが連続して配置されます。
これを各スレッドがアクセスすると、各キャッシュが重複します。
そしてその重複しているメモリつまり、メンバ変数に各スレッドがアクセスしまくります。
※クラスがどのような形でメモリに乗っかるかはid:motom552:20090109を参照してください。

対応

このケースでの対応は2パターンありますが、どちらもスレッド間のアクセス重複を防ぐ方法。

  1. 加算処理等をスタック変数に結果だけ渡す。
  2. 重複しないくらいメモリ間を離す。つまりクラスのサイズをでかくする。
加算処理等をスタック変数に結果だけ渡す。

上のソースの下二つのコメントアウトのようにアクセスが多いループ内をメンバ変数ではなく、
スタック変数にし、結果だけを受け取るようにします。

重複しないくらいメモリ間を離す。つまりクラスのサイズをでかくする。

上のソースの上一つのコメントアウトをはずして、
インスタンスAのメンバ変数(m_sum)とインスタンスBのメンバ変数(m_sum)間のメモリ幅が
キャッシュ取得幅より大きければ重複はしないので、これでも回避はできます。


まあ両方やったほうがいいとは思います。

再度測定

実行タイプ 最小[ms] 最大[ms]
シングルスレッド 6606 6624
マルチ(パッドなし+メンバ直使用) 4101 6959
マルチ(パッドなし+スタック使用) 1222 1247
マルチ(パッドあり+メンバ直使用) 1693 1711
マルチ(パッドあり+スタック使用) 1222 1251

だいぶ価値あるものになったかと思います。
パッドなし+スタック使用がパッドあり+スタック使用と同じなのは、
スタック変数はスレッドごとのメモリにあるので、重複しません。

結論

序盤でも述べているように常にメモリの事を考えておかないと簡単にマルチスレッドの価値を失います。
おまけにキャッシュの状態なんて分からないので、パフォーマンス測定結果をただ見るだけでそれがキャッシュの非効率なのか単純に重いのかは判断難しいと想像。ここらへんは実際に試してみないと分かりませんが。。
知らずに非効率なマルチスレッドになって開発だけはし辛いという・・なんとも無様な。。

疑問

これがC#Javaの場合はどうなるのだろうか。特に気にせず実装しても最大パフォーマンスを保障してくれるのだろうか。

最後に

上記記事はあくまで自分で仮説、測定、考察した結果なので、そもそも間違いがある場合があります。
特にキャッシュ周りとかは記事読んだだけなので。
ただ測定結果を見る限り多分そこまで間違いはないかと。。