2019年09月13日

GCCの最適化による予期せぬ無限ループの発生

コンパクトな独自の libc を実装していて、GCC のテストを通したところ、WARNING: program timed out. が原因による FAIL が多発しました。調べた結果、非常に意外な結果だったのでメモします。



問題は、calloc の実装でした。以下のように、全く問題無さそうな簡単なコードです。
#include <stdlib.h>
#include <string.h>

void *calloc(size_t n, size_t size)
{
    size_t bytes = n * size;
    void *p = malloc(bytes);

    if (p) {
        memset(p, 0, bytes);
    }
    return p;
}
これが、GCC 6.4.0 の arm-eabi で O2 でコンパイルすると
> gcc -S -O2 calloc.c
以下のようなアセンブリコードになりました。
calloc:
	mul	r0, r1, r0
	movs	r1, #1
	b	calloc
	.size	calloc, .-calloc
	.ident	"GCC: (GNU) 6.4.0"
なんと、malloc して memset(0) するコードが、calloc と同じ処理だと判断されて(事実 calloc なので当たり前なのですが)、calloc(bytes, 1) 呼び出しに最適化されてしまったのです。そのためここで無限ループし、timed out していたというのが真相でした。

実はこのコードは古いバージョンの exeGCC で使用されていたもの(ブログに載せるにあたり、スタイルを変更しています)で、GCC 4.8.5 では、以下のように問題ありませんでした。
calloc:
	stmfd	sp!, {r3, r4, r5, lr}
	mul	r4, r1, r0
	mov	r0, r4
	bl	malloc
	subs	r5, r0, #0
	beq	.L2
	mov	r2, r4
	mov	r1, #0
	bl	memset
.L2:
	mov	r0, r5
	ldmfd	sp!, {r3, r4, r5, pc}
	.size	calloc, .-calloc
	.ident	"GCC: (GNU) 4.8.5"
コンパイラの最適化技術が進んだことにより発生した不具合と言えます。

この最適化を抑制するためには、-fno-builtin オプションしか無いようです。今回は、常に -fno-builtin-malloc を全体に付けることにしました。(ビルトイン最適化は有益なので、抑制は必要最小限にしたいからです。)
>gcc -S -O2 -fno-builtin-malloc calloc.c
オプションを付ければ、旧コンパイラと同じようなコードが出ます。
calloc:
	push	{r3, r4, r5, lr}
	mul	r4, r1, r0
	mov	r0, r4
	bl	malloc
	mov	r5, r0
	cbz	r0, .L1
	mov	r2, r4
	movs	r1, #0
	bl	memset
.L1:
	mov	r0, r5
	pop	{r3, r4, r5, pc}
	.size	calloc, .-calloc
	.ident	"GCC: (GNU) 6.4.0"


kmckk at 14:29コメント(2)GCC | 若槻 

コメント一覧

1. Posted by 名無し   2019年09月16日 16:45
抑制を必要最小限にしたいのであれば対処は局所的に行うべきと思います。__asm __volatile("":::"memory");を使用するのはどうでしょうか。
https://godbolt.org/z/kDNpOw
2. Posted by 若槻   2019年09月26日 18:28
なるほど、このような手が!ありがとうございます!

コメントする

名前
 
  絵文字
 
 
記事検索
最新コメント
アクセスカウンター
  • 今日:
  • 昨日:
  • 累計:

QRコード
QRコード