コンパイラの意図を汲んであげよう

コンパイラの意図を理解して、ささやかに最適化しよう、という話。


とりあえず、

  • 基本的な話 - ポインタと関数とエイリアスとローカル変数と他色々
  • 配列
  • なんか

で、三回分くらいの話にはなるかな。と。


まず、基本的なポインタと関数呼び出しの話。大体ループの最適化と内容が一緒でそのへんの話がわかる人には情報量0なので読み飛ばしてもよいかと。

  • 関数呼び出しを挟んでポインタの先を読むのは危険。
extern void external_func(void);

int nanika( int *ptr )
{
	int x = *ptr;
	external_func();
	return x + (*ptr);
}

まず、こういうコードを考える。これの最適化されたコードはどうなるだろうか。

nanika:
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%esi
	movl	8(%ebp), %esi
	pushl	%ebx
	movl	(%esi), %ebx
	call	external_func
	addl	(%esi), %ebx
	movl	%ebx, %eax
	popl	%ebx
	popl	%esi
	popl	%ebp
	ret

GCCでは、こんな出力になった。これは、ちょっと多いように感じなくもない。もっと、こう、なんとかならないだろうか。
だけど、これは、仕方が無いのだ。ptrの先は何を指してるだろうか。

int obj;

void external_func( void )
{
	obj = 4;
}

int main()
{
	obj = 8;
	nanika( &obj );
}

external_funcの実体と関数nanikaの呼び出しがこんな感じになってるとしよう。
ここで、external_funcでは引数ptrが指すの先の値は書きかえられてしまっている。もし、関数nanikaでポインタの先をレジスタに保存してしまっていたら、external_func呼び出ししたあとのptrが指す値が実際と正しくならない。(説明がアレ)


まず、ここで、基本その一。「コンパイラはポインタまわりでは思っているよりも若干保守的なコードを出す、というか、仕方無いんだけど」と、いうこと。
気をつけないといけない場合は、構造体のポインタとか。

struct point
{
	int x;
	int y;
};
struct owner
{
	struct point *p;
};

extern int external_func();

int func( struct owner *ptr )
{
	int x = ptr->p->x;
	external_func();
	return x + ptr->p->y;
}

これ、と

struct point
{
	int x;
	int y;
};
struct owner
{
	struct point *p;
};

extern int external_func();

int func( struct owner *ptr )
{
	external_func();
	return ptr->p->x + ptr->p->y;
}

これでは、結構違うコードになる。誤差にもならないようなレベルの話であるといえば、そうなんだけど、ちょっとした書きかたの違いだけで変わる、というのは知っておいて損はないと思う。


あと、引数のポインタの扱いとか。

int nanika( int *ptr1, int *ptr2 )
{
	int x = *ptr1;
	*ptr2 = 4;
	return x + *ptr1;
}

これはどうなるだろうか。

nanika:
	movl	4(%esp), %edx
	movl	8(%esp), %ecx
	movl	(%edx), %eax
	movl	$4, (%ecx)
	addl	(%edx), %eax
	ret

GCCだとこうなる。命令ごとにメモリアクセスしてしまっている。
けど、これも仕方が無いことで、

int func()
{
	int x;
	nanika ( &x, &x );
}

こういう風に呼び出されたときも、正しい動きをするには、毎回ptr1を読み直すしかない。こんなコード書きっこないけど、それでも、コンパイラはそういう動きも保証してやらないといけないのだ。おお、かわいそうなコンパイラ
一応、指標としては、引数に複数のポインタを受けとったときは、なるべくまとめてアクセスすることだろうか。変にスーパースカラーとか意識してはいけない

int nanika( int *ptr1, int *ptr2 )
{
	int x = *ptr1 + *ptr1;
	*ptr2 = 4;
	return x;
}

こうしてやれば、

nanika:
	movl	4(%esp), %eax
	movl	8(%esp), %edx
	movl	(%eax), %eax
	movl	$4, (%edx)
	addl	%eax, %eax
	ret

メモリアクセスは一回減る。

int nanika( int * __restrict__ ptr1 , int * __restrict__ ptr2 )
{
	int x = *ptr1;
	*ptr2 = 4;
	return x + *ptr1;
}

ウラワザ的に __restrict__ を使うという方法も。でも、これ、やりすぎると意味不明のバグになりそうなのでお勧めしない。



お、思ったよりも長くなってしまった。コード貼りすぎ。やる気が出れば続く。