最適化されない無限の空のループを作成するにはどうすればよいですか?


131

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、次に不正な命令(のud2後にトラップが配置されたdie())でクラッシュします。godboltでは、の呼び出し後に何も生成されないことがわかり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に適切なメモリアクセスなしの無限ループを出力させるにはどうすればよいですか?


3
コメントは詳細な議論のためのものではありません。この会話はチャットに移動さました
Bhargav Rao

2
副作用を伴わないポータブルなソリューションはありません。メモリアクセスが必要ない場合は、揮発性の符号なしcharを登録することをお勧めします。しかし、C ++ 17ではレジスタがなくなります。
スコットM

25
多分これは問題の範囲外ですが、なぜあなたがこれをしたいのか興味があります。確かにあなたの本当の仕事を達成するためのいくつかの他の方法があります。それとも、これは単なるアカデミックなものですか?
ランチャー

1
@Cruncher:プログラムを実行しようとする特定の試みの影響は、本質的に役に立たないか、役に立たない場合よりもかなり悪い場合があります。プログラムが無限ループに陥る結果となる実行は役に立たない場合がありますが、コンパイラーが代替する他の動作よりも望ましい場合があります。
スーパーキャット

6
@Cruncher:の概念がない独立したコンテキストでコードが実行されている可能性がありexit()、継続的な実行の効果が役に立たないほど悪化しないことを保証できない状況をコードが発見した可能性があるためです。自己ジャンプループは、このような状況を処理するためのかなりお粗末な方法ですが、それでも、悪い状況を処理するための最良の方法である場合があります。
スーパーキャット

回答:


77

C11標準はこれを6.8.5 / 6と述べています:

制御式が定数式ではない反復ステートメント156)は、入出力操作を実行せず、揮発性オブジェクトにアクセスせず、その本体、制御式、または(forの場合、ステートメント)その式-3は、実装によって終了すると見なされます。157)

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

156)省略された制御式は、定数式であるゼロ以外の定数に置き換えられます。

157)これは、終了が証明できない場合でも、空のループの削除などのコンパイラー変換を可能にすることを目的としています。

あなたの場合、while(1)は非常に明確な定数式なので、実装によって終了するとは想定されていない場合があります。「いつまでも」ループはプログラミングの一般的な構成要素であるため、そのような実装は絶望的に破られるでしょう。

ただし、ループの後で「到達不能コード」に何が起こるかは、私の知る限り、明確に定義されていません。ただし、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");
}

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

setjmp最初の実行時に0を返すため、このプログラムはにスマッシュしてwhile(1)そこで停止し、「begin」だけを出力する必要があります(\ nがstdoutをフラッシュすると想定)。これはgccで発生します。

ループが単に削除された場合は、「begin」を2回出力し、次に「unreachable」を出力する必要があります。ただし、clang(godbolt)では、「begin」が1回出力され、次に「unreachable」が出力されてから、終了コード0が返されます。どのように配置しても、それはまったく間違っています。

ここでは未定義の動作を主張することはできませんので、これはclangのバグだと思います。とにかく、この動作により、組み込みシステムなどのプログラムではclangが100%役に立たなくなります。この場合、(ウォッチドッグなどを待機している間)プログラムを停止している永遠のループに依存できる必要があります。


15
「これは非常に明確な定数式なので、実装によって終了するとは想定されていない可能性があります」には同意しません。これは本当にひどい言葉の弁護士に入るのですが6.8.5/6もし(これらの)あなたが(これ)を仮定するかもしれない場合の形になります。そうでない(これらの)場合でも、(これ)を想定できないという意味ではありません。これは、条件が満たされている場合のみの仕様であり、基準を使用してやりたいことができる条件が満たされていない場合ではありません。そして観測可能なものがない場合...
kabanus

7
@kabanus引用部分は特別な場合です。そうでない場合(特別な場合)は、通常どおりにコードを評価してシーケンス化します。同じ章を読み続けると、引用された特別な場合を除いて、制御式は各反復ステートメントに指定されたとおりに評価されます(「セマンティクスによって指定されたとおり」)。これは、順序付けされ、明確に定義された、任意の値計算の評価と同じルールに従います。
ランディン

2
私は同意しますが、中にいることをあわてないことはないint z=3; int y=2; int x=1; printf("%d %d\n", x, z);何がある2ので、空の役に立たないという意味で、アセンブリ内xの後に割り当てられていませんでしたyが、後にz、最適化のために。したがって、最後の文から、通常のルールに従い、whileが停止されたと仮定し(制約が緩和されたため)、最終的な「到達不能」な印刷を残しました。今、私たちはその役に立たないステートメントを最適化します(私たちはこれ以上知りませんから)。
kabanus

2
@MSalters私のコメントの1つが削除されましたが、入力に感謝し、同意します。私のコメントが言ったことは、これが議論の核心だと思います- たとえロジックがソースに残っていても、どのセマンティクスを最適化することを許可するかという点に関してはwhile(1);int y = 2;ステートメントと同じ です。n1528から、それらは同じである可能性があるという印象を受けましたが、私より経験が多い人は反対の方法で論じているため、それは明らかに公式のバグであるため、標準の表現が明示的であるかどうかに関する哲学的議論を超えています、引数は疑わしくレンダリングされます。
kabanus

2
「「フォーエバー」ループは一般的なプログラミング構造なので、そのような実装は絶望的に破られるでしょう。」—私は感情を理解していますが、C ++に同じように適用できるため、引数に欠陥がありますが、このループを最適化したC ++コンパイラは壊れていませんが、準拠しています。
Konrad Rudolph、

52

副作用を引き起こす可能性のある式を挿入する必要があります。

最も簡単な解決策:

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

ゴッドボルトリンク


21
ただし、clangが動作している理由は説明されていません。
ランディン

4
「それはclangのバグだ」と言うだけで十分です。「バグ」と叫ぶ前に、まずここでいくつかのことを試してみたいと思います。
ランディン

3
@Lundinバグかどうかわかりません。この場合、標準は技術的に正確ではありません
P__J__

4
幸い、GCCはオープンソースであり、あなたの例を最適化するコンパイラーを書くことができます。そして、私はあなたが思いつくあらゆる例について、現在そして将来においてそうすることができます。
トーマスウェラー

3
@ThomasWeller:GCC開発者は、このループを最適化するパッチを受け入れません。文書化された=保証された動作に違反します。前のコメントを参照してください。これasm("")は暗黙的asm volatile("");に行われるため、asmステートメントは抽象マシンgcc.gnu.org/onlinedocs/gcc/Basic-Asm.htmlで実行するのと同じ回数実行する必要があります。(注それはだということではない、その副作用はどのメモリやレジスタを含めるようにするために、安全、あなたが拡張ASMを必要とする"memory"あなたはC.基本ASMからあなた今までのアクセスのようなもののためにのみ安全であることを読み取りまたは書き込みメモリにしたい場合はクロバーasm("mfence")cli。)
Peter Cordes、

50

他の回答は、Clangにインラインアセンブリ言語やその他の副作用を伴う無限ループを発生させる方法をすでにカバーしています。これが本当にコンパイラのバグであることを確認したいだけです。具体的には長年のLLVMバグです。「副作用のないすべてのループは終了する必要がある」というC ++の概念を、Cなどの禁止すべきでない言語に適用します。

たとえば、Rustプログラミング言語は無限ループも許可し、LLVMをバックエンドとして使用します。これと同じ問題があります。

短期的には、LLVMは「副作用のないすべてのループは終了しなければならない」と想定し続けるようです。無限ループを許可する言語の場合、LLVMは、フロントエンドがllvm.sideeffectそのようなループにオペコードを挿入することを期待します。これはRustが計画していることなので、Clang(Cコードのコンパイル時)もおそらくそれを行う必要があります。


5
10年以上前のバグの臭いのようなものはありません...複数の修正とパッチが提案されていますが、まだ修正されていません。
Ian Kemp、

4
@IanKemp:彼らがバグを修正するには、バグの修正に10年を要したことを認める必要があります。標準が彼らの振る舞いを正当化するために変更されることを望んでいる方がよいでしょう。もちろん、基準が変更されたとしても、基準への変更が基準の以前の行動上の義務が遡及的に修正されるべき欠陥であることを示すものと見なす人々の目を除いて、それは彼らの行動を正当化しません。
スーパーキャット

4
LLVMがsideeffect(2017年に)opを追加し、フロントエンドがその裁量でループにopを挿入することを期待しているという意味で「修正」されています。LLVMはデフォルトのループをいくつか選択する必要があり、偶然またはその他の方法で、偶然にC ++の動作と一致するものを選択しました。もちろん、連続sideeffectする操作を1つにマージするなど、実行する必要のある最適化作業はまだ残っています。(これが、Rustフロントエンドによる使用を妨げているものです。)したがって、その根拠として、バグはループにopを挿入しないフロントエンド(clang)にあります。
Arnavion

@Arnavion:結果が使用されない限り、または結果が使用されるまで操作が延期される可能性があることを示す方法はありますが、データがプログラムを無限にループさせる場合、過去のデータ依存関係を続行しようとすると、プログラムが役に立たなくなるより悪くなりますか?オプティマイザがプログラムを役に立たないよりも悪化させるのを防ぐために以前の有用な最適化を妨げる偽の副作用を追加しなければならないのは、効率のレシピのようには聞こえません。
スーパーキャット

その議論はおそらくLLVM / clangメーリングリストに属しています。opを追加したLLVMコミットのFWIWは、それについていくつかの最適化パスも教えました。また、Rust sideeffectはすべての関数の先頭にopsを挿入する実験を行い、実行時のパフォーマンスの低下は見られませんでした。唯一の問題はコンパイル時間の回帰です。これは、以前のコメントで述べたように、連続する操作の融合が欠如しているためと思われます。
Arnavion

32

これはClangのバグです

...無限ループを含む関数をインライン化するとき。while(1);mainに直接表示されたときの動作は異なります。

概要とリンクについては、@ Arnavionの回答を参照してください。この回答の残りの部分は、既知のバグはもちろんのこと、バグであることを確認する前に書かれました。


タイトルの質問に答えるには:最適化されない無限の空のループを作成するにはどうすればよいですか??-
作るdie()マクロではなく、機能を、クラン3.9以降では、このバグを回避します。(以前のClangバージョンは、ループを保持するcallか、無限ループのある関数の非インラインバージョンにを出力します。)print;while(1);print;関数呼び出し元(Godbolt)にインライン化ても安全であるように見えます。 -std=gnu11-std=gnu99何も変わりません。

GNU Cのみに関心がある__asm__("");場合は、ループ内のP__J__も機能し、それを理解するコンパイラーの周囲のコードの最適化を損なうべきではありません。GNU C Basic asmステートメントは暗黙的volatileであるため、これは、C抽象マシンで実行するのと同じ回数「実行」する必要がある目に見える副作用としてカウントされます。(そうです、ClangはGCCマニュアルで文書化されているように、CのGNU方言を実装しています。)


一部の人々は、空の無限ループを最適化することは合法であるかもしれないと主張しました。私は同意しません1、ただしそれを受け入れたとして、Clangがループに到達できない後のステートメントを想定することは合法ではありません。し、実行が関数の終わりから次の関数またはガベージに落ちることを許すランダムな命令としてデコードします。

(これは、Clang ++の標準に準拠します(ただし、あまり有用ではありません)。副作用のない無限ループは、C ++ではUBですが、Cで
はありません。while (1)です。Cの未定義の動作 により、コンパイラーは基本的に何でも出力できます確実にUBに遭遇する実行パス上のコードの場合。asmループ内のステートメントはC ++のこのUBを回避します。しかし、実際には、C ++としてコンパイルされたClangは、インラインの場合を除いて、定数式の無限空ループを削除しません。 Cとしてコンパイル)


手動でインライン化while(1);すると、Clangによるコンパイル方法が変わります。asmに無限ループが存在します。 これは、私たちがルール弁護士のPOVに期待することです。

#include <stdio.h>
int main() {
    printf("begin\n");
    while(1);
    //infloop_nonconst(1);
    //infloop();
    printf("unreachable\n");
}

Godboltコンパイラエクスプローラで、Clang 9.0 -O3 -xcをx86-64のC()としてコンパイルします。

main:                                   # @main
        push    rax                       # re-align the stack by 16
        mov     edi, offset .Lstr         # non-PIE executable can use 32-bit absolute addresses
        call    puts
.LBB3_1:                                # =>This Inner Loop Header: Depth=1
        jmp     .LBB3_1                   # infinite loop


.section .rodata
 ...
.Lstr:
        .asciz  "begin"

同じコンパイラを同じオプションで使用すると、最初に同じmainを呼び出すa がコンパイルされますが、その後は命令の発行が停止されます。したがって、先に述べたように、実行は関数の最後から次の関数に落ちます(ただし、スタックは関数のエントリに対して誤って配置されているため、有効な末尾呼び出しではありません)。infloop() { while(1); }putsmain

有効なオプションは

  • label: jmp label無限ループを放出する
  • またはその後第2文字列を印刷するために別のコールを発し、そして(我々は、無限ループを除去することができることを受け入れる場合)return 0からmain

クラッシュしたり、「到達不能」を表示せずに続行したりすることは、C11の実装では、気付かないUBがない限り、明らかに問題です。


脚注1:

記録として、私は、 C11が定数式無限ループが空である(I / O、揮発性、同期、またはその他がない)場合でも、終了の仮定を許可しないという証拠の基準を引用する@Lundinの回答に同意します目に見える副作用)。

これは、通常のCPUのループ空のasmループにコンパイルできるようにする一連の条件です。(ソースで本文が空でなくても、ループの実行中は、変数への割り当てを他のスレッドやシグナルハンドラーがデータレースUBなしで見ることができません。したがって、必要に応じて、適合実装がそのようなループ本体を削除できます。次に、ループ自体を削除できるかどうかの問題が残ります。ISOC11は明示的に「いいえ」と言っています。)

C11がそのケースを、実装がループの終了を想定できない場合(およびそれがUBではない場合)として特定した場合、ループが実行時に存在することを意図していることは明らかです。有限時間で無限の量の作業を実行できない実行モデルを持つCPUをターゲットとする実装には、空の定数無限ループを削除する正当な理由がありません。または一般的にさえ、正確な表現は、それらが「終了すると見なされる」かどうかに関するものです。ループが終了できない場合は、数学と無限大についてどのような引数を指定しても、仮想マシンで無限の処理を実行するのにかかる時間に関係なく、後のコードに到達できないことを意味します

さらに、Clangは単なるISO C準拠のDeathStation 9000ではなく、カーネルや組み込みのものを含む実際の低レベルシステムプログラミングに役立つことを目的としています。 したがって、C11 がの削除を許可することについての引数を受け入れるかどうかにかかわらずwhile(1);、Clangが実際にそれを実行することを望んでも意味がありません。と書けばwhile(1);、それはおそらく偶然ではなかったでしょう。(ランタイム変数制御式を使用して)偶然に無限になってしまうループを削除すると便利な場合があり、コンパイラーがそれを行うことは理にかなっています。

次の割り込みまでスピンすることはめったにありませんが、Cでそれを書き込んだ場合、それは間違いなく予想されることです。(GCCとClangで何起こるか、無限ループがラッパー関数内にある場合のClangを除く)。

たとえば、プリミティブOSカーネルでは、スケジューラに実行するタスクがない場合、アイドルタスクが実行される可能性があります。その最初の実装はかもしれませんwhile(1);

または、省電力アイドル機能のないハードウェアの場合は、それが唯一の実装である可能性があります。(2000年代前半までは、x86でそれはまれではないと思いました。hlt命令は存在しましたが、IDKがCPUが低電力のアイドル状態になるまで有意な量の電力を節約した場合。)


1
好奇心から、組み込みシステムに実際にclangを使用している人はいますか?私はそれを見たことがなく、組み込みのみで作業しています。gccは「最近」(10年前)だけが組み込み市場に参入しましたが、私はそれを疑わしく使用してい-ffreestanding -fno-strict-aliasingます。ARMと、おそらくレガシーAVRでうまく動作します。
ランディン

1
@Lundin:IDKは組み込みについてですが、はい、人々はclangでカーネルを構築します。少なくとも時にはLinuxです。おそらくMacOS用のDarwinも。
Peter Cordes、

2
bugs.llvm.org/show_bug.cgi?id=965このバグは関連しているように見えますが、ここに表示されているのは確かではありません。
bracco23

1
@lundin-VxWorksやPSOSのようなRTOSを使用して、90年代を通じて組み込み作業にGCC(および他の多くのツールキット)を使用したと確信しています。GCCが最近組み込み市場に参入しただけだと言う理由がわかりません。
ジェフリアマン

1
@JeffLearmanそれで最近主流になったの?とにかく、gccの厳密なエイリアシングの大失敗はC99の導入後にのみ発生し、その新しいバージョンも、厳密なエイリアシング違反が発生したときにバナナにならないようです。それでも、使用するたびに懐疑的です。clangに関しては、最新バージョンは永遠のループになると明らかに完全に壊れているので、組み込みシステムには使用できません。
ランディン

14

参考までに、Clangは次のように誤動作しgotoます。

static void die() {
nasty:
    goto nasty;
}

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

それは質問と同じ出力を生成します、すなわち:

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

私はこれをC11で許可されているように読む方法を見ていない、それは言うだけです:

6.8.6.1(2)gotoステートメントは、囲み関数内の名前付きラベルが前に付いたステートメントに無条件ジャンプします。

goto「繰り返し文」(6.8.5リストではないwhiledofor)何も特別な約「終了-仮定」おぼれるは、しかし、あなたがそれらを読みたい、適用されます。

元の質問のGodboltリンクコンパイラはx86-64 Clang 9.0.0で、フラグは -g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-9.2.0 -fcolor-diagnostics -fno-crash-diagnostics -O2 -std=c11 example.c

x86-64 GCC 9.2などの他のものを使用すると、かなり完璧になります。

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

フラグ: -g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c


適合実装では、実行時間またはCPUサイクルに文書化されていない変換制限があり、超過した場合、またはプログラムの入力が制限を超えた場合に、任意の動作を引き起こす可能性があります。このようなことは、規格の管轄外の、実装の品質の問題です。clangのメンテナが低品質の実装を作成する権利を主張するのは奇妙に思えますが、標準ではそれを許可しています。
スーパーキャット

2
@supercatコメントありがとう...なぜ翻訳制限を超えると、翻訳フェーズが失敗して実行が拒否される以外に何ができるのですか?また:「5.1.1.3診断準拠する実装は...診断メッセージ...前処理変換ユニットまたは変換ユニットに構文規則または制約の違反が含まれている場合 ...」を生成します。実行フェーズでの誤った動作がどのように順応するかはわかりません。
ジョナサンジョ

宇宙にアトムよりも多くのバイト数のスタックを必要とする厳密に準拠するプログラムを作成できるため、実装時に制限をすべて解決する必要がある場合、標準を実装することは完全に不可能です。実行時の制限を「翻訳の制限」と一括する必要があるかどうかは不明ですが、そのような譲歩は明らかに必要であり、それを置くことができる他のカテゴリはありません。
スーパーキャット

1
「翻訳制限」についてのご意見にお応えしました。もちろん実行制限もありますが、なぜ翻訳制限にひとまとめにするべきなのか、なぜ必要だと言っているのか、私にはわかりません。nasty: goto nastyユーザーまたはリソースの枯渇が介入するまでCPUをスピンさせないでください。
ジョナサンジョ

1
標準では、私が見つけた「実行制限」については言及していません。関数呼び出しのネストのようなものは、通常、スタック割り当てによって処理されますが、制限はすべての関数の16枚のコピーを構築できる16の深さへの呼び出し機能、およびへの呼び出しがあることを適合実装bar()の中foo()から呼び出しとして処理される__1fooまでの__2barから、__2foo__3bar、 etc.からfrom __16fooまで__launch_nasal_demons。これにより、すべての自動オブジェクトが静的に割り当てられ、通常は「実行時」の制限が変換制限になります。
スーパーキャット

5

私は悪魔の支持者を演じ、標準がコンパイラが無限ループを最適化することを明示的に禁止していないと主張します。

制御式が定数式ではない反復文156)は、入出力操作を実行せず、揮発性オブジェクトにアクセスせず、本体、制御式、または(forの場合は)同期またはアトミック操作を実行しません。ステートメント)その式-3、実装により終了することが想定される可能性があります157)

これを解析してみましょう。特定の基準を満たす反復ステートメントは、終了すると見なされます。

if (satisfiesCriteriaForTerminatingEh(a_loop)) 
    if (whatever_reason_or_just_because_you_feel_like_it)
         assumeTerminates(a_loop);

これは、基準が満たされない場合に何が起こるかについては何も言わず、標準の他のルールが守られている限り、ループが終了する可能性があると仮定しても明示的に禁止されていません。

do { } while(0)またはwhile(0){}、コンパイラーが気まぐれで終了することを想定しているにもかかわらず、明らかに終了するという基準を満たさないすべての反復ステートメント(ループ)の後にあります。

しかし、コンパイラは単に最適化できるのwhile(1){}でしょうか?

5.1.2.3p4は言う:

抽象マシンでは、すべての式がセマンティクスで指定されたとおりに評価されます。実際の実装では、その値が使用されておらず、必要な副作用(関数の呼び出しや揮発性オブジェクトへのアクセスによるものも含む)が発生していないと推定できる場合、式の一部を評価する必要はありません。

これはステートメントではなく式について言及しているため、100%説得力はありませんが、次のような呼び出しは確実に許可されます。

void loop(void){ loop(); }

int main()
{
    loop();
}

スキップされます。興味深いことに、clangはそれをスキップし、gccはスキップしません


「これは、基準が満たされない場合に何が起こるかについては何も言っていない」6.8.5.1 whileステートメント:「制御式の評価は、ループ本体の各実行前に行われます。」それでおしまい。これは(定数式の)値の計算であり、評価という用語を定義する抽象マシン5.1.2.3の規則に該当します。「式の評価には、一般に値の計算と副作用の開始の両方が含まれます。」そして同じ章によれば、そのようなすべての評価は順序付けられ、セマンティクスで指定されたとおりに評価されます。
ランディン

1
@Lundinそれではwhile(1){}1評価と無限の評価のシーケンスが絡み合って{}いますが、標準では、これらの評価にゼロ以外の時間をかける必要があると言っていますか?gccの振る舞いの方が便利だと思います。メモリアクセスを伴うトリックや言語外のトリックは必要ありません。しかし、標準がこのclangでの最適化を禁止しているとは確信していません。作る場合while(1){}nonoptimizableする意向で、標準それについて明示的であるべきだと無限ループは5.1.2.3p2で観察可能な副作用として表示されます。
PSkocik

1
1条件を値の計算として扱うなら、それは指定されていると思います。実行時間は重要ではwhile(A){} B;ありません。重要なのは、完全に最適化されない、最適化されB;ない、再シーケンスされない、ということB; while(A){}です。C11抽象機械、強調鉱山引用する:「式A及びBの評価の間の配列ポイントが存在することを意味するすべての値の計算と副作用Aに関連付けられているが、すべての値の計算の前に配列決定されていると副作用Bに関連付けられています」。の値はA(ループによって)明確に使用されます。
ランディン

2
+1「実行なしで無期限に実行が停止する」のは「副作用」の定義における「副作用」であるように思えますが、これは意味があり、真空の標準を超えて役立ちます。これは説明に役立ちますそれが誰かにとって理にかなっていることができる考え方。
mtraceur

1
「無限ループの最適化」の近く:「それ」が標準とコンパイラのどちらを指しているのかは明確ではありません-言い換えれば?与えられた「それはおそらくべきのに」ない「それはおそらくいけないのに」、おそらくという標準である「それは」を参照します。
Peter Mortensen、

2

これは単なる古いバグであると私は確信しています。私は以下に私のテストを残し、特に私が以前に持っていたいくつかの理由のために標準委員会での議論への参照を残します。


これは未定義の動作(最後を参照)であり、Clangには1つの実装しかないと思います。GCCは実際に期待どおりに機能し、unreachableprintステートメントのみを最適化してループを終了します。インラインを組み合わせてループで何ができるかを決定するときに、Clangが奇妙に意思決定をしている方法もあります。

動作は非常に奇妙です。これにより、最終的な出力が削除されるため、無限ループが「確認」されますが、ループも削除されます。

私の知る限り、それはさらに悪いことです。取得したインラインを削除します。

die: # @die
.LBB0_1: # =>This Inner Loop Header: Depth=1
  jmp .LBB0_1
main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

そのため、関数が作成され、呼び出しが最適化されます。これは予想以上に弾力性があります。

#include <stdio.h>

void die(int x) {
    while(x);
}

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

その結果、関数のアセンブリは非常に最適ではなくなりますが、関数呼び出しは再び最適化されます!さらに悪いことに:

void die(x) {
    while(x++);
}

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

ローカル変数を追加してそれを増やし、ポインターを渡し、その他を使用して、他のたくさんのテストを行いましたgoto...この時点で、あきらめます。clangを使用する必要がある場合

static void die() {
    int volatile x = 1;
    while(x);
}

仕事をします。それは(明らかに)最適化に夢中になり、冗長なfinalを残しますprintf。少なくともプログラムは停止しません。たぶんGCCでしょうか?

補遺

デビッドとの議論の後、私は標準が「条件が一定である場合、ループが終了すると仮定しないかもしれない」とは言っていません。そのため、標準で認められている動作(標準で定義されている)はありません。一貫性についてのみ議論します。コンパイラがループを終了することを想定しているため、ループを最適化している場合、次のステートメントは最適化しないでください。

ヘックn1528は、私がそれを正しく読んだ場合、未定義の動作としてこれらを持っています。具体的には

そうすることの主な問題は、コードが潜在的に終了しないループを通過できることです。

ここからは、何が許可されているかではなく、何が必要か(期待されるか?)の議論にのみ展開できると思います。


コメントは詳細な議論のためのものではありません。この会話はチャットに移動さました
Bhargav Rao

「プレーンすべてのバグ」あなたが意味するか:昔ながらのバグを」
Peter Mortensen、

@PeterMortensen "ole"も私には問題ありません。
kabanus

2

これはClangコンパイラのバグのようです。die()関数に静的関数であるという強制がない場合は、削除しstaticて次のようにしinlineます。

#include <stdio.h>

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

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

Clangコンパイラーでコンパイルすると期待どおりに機能し、移植性もあります。

コンパイラエクスプローラ(godbolt.org)-clang 9.0.0-O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        jmp     .LBB0_1
.Lstr:
        .asciz  "begin"

どうstatic inlineですか?
SSアン

1

以下は私にとってはうまくいくようです:

#include <stdio.h>

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

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

godbolt

Clangに1つの関数を最適化しないように明示的に指示すると、期待どおりに無限ループが発生します。うまくいけば、特定の最適化をすべて無効にするのではなく、特定の最適化を選択的に無効にする方法があります。printfただし、Clangは2番目のコードの発行を拒否します。それを強制するために、内部のコードをさらに変更する必要がありmainました:

volatile int x = 0;
if (x == 0)
    die();

無限ループ関数の最適化を無効にし、無限ループが条件付きで呼び出されるようにする必要があるようです。実世界では、とにかく後者がほとんどの場合当てはまります。


1
printfループが実際に永久に実行される場合、2番目を生成する必要はありません。その場合、2番目はprintf実際には到達不可能であり、したがって削除できるためです。(Clangのエラーは、到達不能を検出することと、到達不能コードに到達するようにループを削除することの両方にあります)。
nneonneo

GCCドキュメント__attribute__ ((optimize(1)))。ただし、clangはそれをサポートされていないものとして無視します: godbolt.org/z/4ba2HMgcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Peter Cordes

0

準拠する実装は、プログラムの実行時間または実行する命令の数に任意の制限を課す可能性があり、実際の多くの場合、制限に違反した場合、または「as-if」ルールの下で任意の方法で動作します。 -違反が避けられないと判断した場合。実装がN1570 5.2.4.1にリストされているすべての制限を名目上実行する少なくとも1つのプログラムを正常に処理できる場合、変換制限、制限の存在、それらが文書化される範囲、およびそれらを超えることの影響はありません。規格の管轄外のすべての実装品質の問題。

標準の意図は、while(1) {}副作用もbreakステートメントもないループが終了することをコンパイラが想定するべきではないということは非常に明白だと思います。一部の人々が考えるかもしれないこととは反対に、標準の作成者は、コンパイラー作成者を愚かで鈍感なものにするように勧めていませんでした。適合する実装は、中断されない場合、宇宙に存在するアトムよりも副作用のない命令を実行するプログラムを終了することを決定するのに役立つかもしれませんが、高品質の実装は、あらゆる仮定に基づいてそのようなアクションを実行すべきではありません終了ではなく、そうすることは有用であり、(clangの動作とは異なり)役に立たないよりも悪くはないということに基づいています。


-2

ループには副作用がないため、最適化できます。ループは、実質的には、ゼロ単位の作業の無限回の反復です。これは数学と論理では定義されておらず、標準では、実装がゼロ時間で実行できる場合、実装が無限の数を完了することを許可されているかどうかは明記されていません。Clangの解釈は、無限回ゼロを無限大ではなくゼロとして扱うのに完全に合理的です。規格では、ループ内のすべての作業が実際に完了した場合に無限ループが終了できるかどうかについては言及されていません。

コンパイラーは、標準で定義されている、観察できない動作を最適化することが許可されています。これには実行時間が含まれます。ループが最適化されていない場合、無限の時間がかかるという事実を維持する必要はありません。これをはるかに短い実行時間に変更することが許可されています-実際には、ほとんどの最適化のポイントです。ループが最適化されました。

clangがコードを単純に変換したとしても、前の反復にかかった時間の半分で各反復を完了することができる最適化CPUを想像できます。それは文字通り無限ループを有限時間で完了するでしょう。そのような最適化CPUは標準に違反しますか?最適化が得意すぎると、最適化CPUが標準に違反すると言うのはかなりばかげているようです。同じことがコンパイラにも当てはまります。


コメントは詳細な議論のためのものではありません。この会話はチャットに移動さました
Samuel Liew

4
あなたの経験(プロファイルから)から判断すると、この投稿はコンパイラを守るためだけに不誠実に書かれていると結論付けることができます。無限の時間がかかるものは、半分の時間で実行するように最適化できると真剣に主張しています。それはすべてのレベルでばかげています、そしてあなたはそれを知っています。
パイプ

@pipe:clangとgccのメンテナーは、標準の将来のバージョンがコンパイラーの動作を許容できるようにしてくれることを望んでいると思います標準で。たとえば、C89のCommon Initial Sequenceの保証はこのように扱われていました。
スーパーキャット

@SSAnne:うーん...これは、gccとclangがポインタの同等性の比較の結果から引き出す不健全な推論の一部をブロックするのに十分ではないと思います。
スーパーキャット

@supercat <s>その他</ s>トンあります。
SSアン

-2

これが不当にそうでない場合は申し訳ありません。私はこの投稿を偶然見つけました。私がGentoo Linuxディストリビューションを長年使用しているため、コンパイラでコードを最適化しない場合は-O0(Zero)を使用する必要があることを知っています。私はそれについて興味があり、上記のコードをコンパイルして実行しましたが、ループは無期限に実行されます。clang-9を使用してコンパイル:

cc -O0 -std=c11 test.c -o test

1
ポイントは、最適化を有効にして無限ループを作ることです。
SSアン

-4

空のwhileループはシステムに副作用をもたらしません。

したがって、Clangはそれを削除します。意図された動作を実現するための「より良い」方法があり、意図をより明確にするように強制します。

while(1); baaaddです。


6
多くの埋め込み構造には、abort()またはの概念はありませんexit()。(おそらくメモリ破損の結果として)関数の継続の実行が危険よりも悪いと判断される状況が発生した場合、組み込みライブラリの一般的なデフォルトの動作は、を実行する関数を呼び出すことwhile(1);です。コンパイラがより有用な動作を代用するオプションを持つことは有用かもしれませんが、そのような単純な構成を継続的なプログラム実行の障壁として扱う方法を理解できないコンパイラ作成者は、複雑な最適化を信頼する能力がありません。
スーパーキャット

あなたの意図をより明確にする方法はありますか?オプティマイザーはプログラムを最適化するためにあり、何もしない冗長ループを削除することは最適化です。これは、数学の世界の抽象的な思考と、より応用的な工学の世界との間の哲学的な違いです。
有名なジャメイ

ほとんどのプログラムには、可能な場合に実行する必要のある一連の有用なアクションと、どのような状況下でも決して実行してはならない、役に立たないより悪いアクションのセットがあります。多くのプログラムは、特定の場合に受け入れ可能な動作のセットを持っています。その1つは、実行時間が観察できない場合、常に「任意の時間を待ってから、セットからアクションを実行する」ことになります。待機以外のすべてのアクションが役に立たないアクションのセットに含まれている場合、「永久に待機する」とは明らかに異なる秒数Nはありません...
スーパーキャット

...「N + 1秒待ってから、他のアクションを実行する」ため、待機以外の許容できるアクションのセットが空であるという事実は観察できません。一方、コードの一部が可能なアクションのセットから許容できないアクションを削除し、それらのアクションのいずれかがとにかく実行される場合、それは観察可能であると見なされます。残念ながら、CおよびC ++言語のルールでは、他のロジック分野や私が特定できる人間の努力とは異なり、「想定」という単語を奇妙な方法で使用しています。
スーパーキャット

1
@FamousJameisは問題ありませんが、Clangはループを削除するだけではありません。その後、すべてを到達不能として静的に分析し、無効な命令を発行します。ループを「削除」しただけでは、期待したことにはなりません。
nneonneo
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.