2019年09月13日
GCCの最適化による予期せぬ無限ループの発生
コンパクトな独自の libc を実装していて、GCC のテストを通したところ、WARNING: program timed out. が原因による FAIL が多発しました。調べた結果、非常に意外な結果だったのでメモします。
問題は、calloc の実装でした。以下のように、全く問題無さそうな簡単なコードです。
実はこのコードは古いバージョンの exeGCC で使用されていたもの(ブログに載せるにあたり、スタイルを変更しています)で、GCC 4.8.5 では、以下のように問題ありませんでした。
この最適化を抑制するためには、-fno-builtin オプションしか無いようです。今回は、常に -fno-builtin-malloc を全体に付けることにしました。(ビルトイン最適化は有益なので、抑制は必要最小限にしたいからです。)
#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"