1. ホーム
  2. c

[解決済み] 最適化されない空の無限ループを作るには?

2022-05-11 18:35:39

質問

C11規格では、定数制御式のある反復処理文は最適化してはいけないことになっているようです。私は、以下のアドバイスを参考にしています。 この回答 からアドバイスを受けています。これは、特に標準草案のセクション 6.8.5 を引用しています。

制御する式が定数式でない反復文は、...実装によって終了すると仮定されることがある。

その回答では、以下のようなループは while(1) ; のようなループは最適化の対象にすべきではないと述べています。

では...なぜClang/LLVMは以下のループを最適化するのでしょうか(コンパイル時に cc -O2 -std=c11 test.c -o test )?

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

私のマシンでは、これは次のように出力されます。 begin と表示され、次に が不正な命令でクラッシュします。 (a ud2 の後に置かれたトラップ die() ). ゴッドボルトについて を呼び出した後、何も生成されないことがわかります。 puts .

の下でClangに無限ループを出力させるのは、意外と難しい作業でした。 -O2 - を繰り返しテストすることができるのに volatile 変数を繰り返しテストすることができますが、それは私が望まないメモリ読み取りを伴います。そして、もし私がこのようなことをしたら

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    volatile int x = 1;
    if(x)
        die();
    printf("unreachable\n");
}

...Clangが表示します。 begin に続いて unreachable というように、無限ループが存在しないかのように表示されます。

最適化をオンにした状態で、Clangに適切な、メモリにアクセスしない無限ループを出力させるにはどうしたらいいでしょうか?

どのように解決するのですか?

C11規格では、6.8.5/6にこのように書かれています。

制御する式が定数式でない反復文。 156) その 入出力操作を行わず、揮発性オブジェクトにアクセスせず、本体、制御式、(for文の場合)式-3において同期または原子操作を行わない。 その本体、制御式、または(for文の場合)その式-3で、同期またはアトミック操作を実行しない場合、実装は終了すると仮定してもよい。 を終了させる。 157)

2つの脚注は規範的なものではありませんが、有用な情報を提供しています。

156) 省略された制御式は、ゼロでない定数式に置き換えられます。

157) これは、以下のような場合でも、空ループの削除などのコンパイラ変換を可能にすることを意図している。 の終了が証明できない場合でも、空のループを削除するなどのコンパイラ変換を可能にするためのものである。

あなたの場合 while(1) は明確な定数表現なので、もしかしたら ではなく は終了することを想定していません。for-ever" ループは一般的なプログラミング構成であるため、そのような実装は絶望的に壊れてしまいます。

ループの後に "unreachable code" に何が起こるかは、しかし、私の知る限りでは、よく定義されていないようです。しかし、clang は確かに非常に奇妙な振る舞いをします。gcc (x86) と機械語コードを比較します。

gcc 9.2 -O3 -std=c11 -pedantic-errors

.LC0:
        .string "begin"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
.L2:
        jmp     .L2

clang 9.0.0 -O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.Lstr:
        .asciz  "begin"

gccはループを生成し、clangはただ森に突っ込んでエラー255で終了します。

私は、これが clang の非準拠の動作であることに傾いています。なぜなら、私はあなたの例をさらにこのように拡大しようとしたからです。

#include <stdio.h>
#include <setjmp.h>

static _Noreturn void die() {
    while(1)
        ;
}

int main(void) {
    jmp_buf buf;
    _Bool first = !setjmp(buf);

    printf("begin\n");
    if(first)
    {
      die();
      longjmp(buf, 1);
    }
    printf("unreachable\n");
}

私はC11を追加しました _Noreturn を追加して、コンパイラをさらに支援しようとしました。そのキーワードだけで、この関数がハングアップすることは明らかなはずです。

setjmp は最初の実行で 0 を返すので、このプログラムは単に while(1) にぶつかり、そこで止まって "begin" とだけ表示するはずです (\n flush stdout と仮定しています)。これはgccで起こります。

ループが単に削除された場合、それは "begin" を 2 回表示した後に "unreachable" を表示するはずです。しかし、clang では ( ゴッドボルト ) では、終了コード 0 を返す前に "begin" を 1 回、そして "unreachable" を出力します。これはどう考えても間違っています。

私はここで未定義の動作を主張するケースを見つけることができないので、これはclangのバグであると私は考えています。とにかく、この動作は、プログラムをぶら下げる永遠のループに頼らなければならない (ウォッチドッグの待ち時間など) 組み込みシステムのようなプログラムにとって、clang を 100% 役に立たなくしています。