10

この質問は主に学術的なものです。これが私にとって実際の問題を引き起こすからではなく、好奇心からお願いします。

次の誤ったCプログラムについて考えてみます。

#include <signal.h>
#include <stdio.h>

static int running = 1;

void handler(int u) {
    running = 0;
}

int main() {
    signal(SIGTERM, handler);
    while (running)
        ;
    printf("Bye!\n");
    return 0;
}

ハンドラーがプログラムフローを中断するため、このプログラムは正しくrunningありません。したがって、いつでも変更できるため、宣言する必要がありますvolatile。しかし、プログラマーがそれを忘れたとしましょう。

gcc 4.3.3は、-O3フラグを使用して、ループ本体を(フラグを最初にチェックした後running)無限ループにコンパイルします。

.L7:
        jmp     .L7

これは予想されていた。

while次に、ループ内に次のような些細なことを入れます。

    while (running)
        putchar('.');

そして突然、gccはループ条件を最適化しなくなりました!ループ本体のアセンブリは次のようになります(ここでも-O3):

.L7:
        movq    stdout(%rip), %rsi
        movl    $46, %edi
        call    _IO_putc
        movl    running(%rip), %eax
        testl   %eax, %eax
        jne     .L7

runningループを通過するたびにメモリから再ロードされることがわかります。レジスターにもキャッシュされません。どうやらgccは、の値runningが変更された可能性があると考えているようです。

では、なぜgccはrunning、この場合の値を再チェックする必要があると突然判断するのでしょうか。

4

5 に答える 5

9

一般的なケースでは、関数がどのオブジェクトにアクセスでき、したがって変更される可能性があるかをコンパイラが正確に知ることは困難です。が呼び出された時点で、GCC は変更可能な実装がputchar()あるかどうかを認識していないため、多少悲観的になり、実際には変更されている可能性があると想定する必要があります。putchar()runningrunning

たとえばputchar()、翻訳単位の後半に実装がある場合があります。

int putchar( int c)
{
    running = c;
    return c;
}

翻訳単位に実装がない場合でも、putchar()たとえば、オブジェクトを変更できるようrunningなオブジェクトのアドレスを渡す可能性のあるものがある可能性があります。putchar

void foo(void)
{
    set_putchar_status_location( &running);
}

handler()関数はグローバルにアクセスできるため、上記の状況のインスタンスである (直接または別の方法で) 自分自身をputchar()呼び出す可能性があることに注意してください。handler()

一方、runningは翻訳単位 ( である) にしか見えないためstatic、コンパイラがファイルの最後に到達するまでに、ファイルにアクセスする機会がないと判断できるはずですputchar()(その場合)。 、コンパイラは戻って while ループのペシミゼーションを「修正」することができます。

は静的であるためrunning、コンパイラは、翻訳単位の外部からアクセスできないと判断し、話している最適化を行うことができる場合があります。ただし、アクセス可能handler()でありhandler()、外部からアクセスできるため、コンパイラはアクセスを最適化できません。静的にしてもhandler()、そのアドレスを別の関数に渡すため、外部からアクセスできます。

最初の例では、上記の段落で述べたことが依然として真実ですがrunning、C言語が基づいている「抽象マシンモデル」は非同期アクティビティを考慮していないため、コンパイラはアクセスを最適化できます.非常に限られた状況 (そのうちの 1 つはvolatileキーワードであり、もう 1 つはシグナル処理ですが、シグナル処理の要件は、最初の例でコンパイラがアクセスを最適化するのを妨げるほど強力ではありませんrunning)。

実際、C99 は、ほとんどこれらの正確な状況での抽象的なマシンの動作について次のように述べています。

5.1.2.3/8「プログラム実行」

例 1:

実装は、抽象セマンティクスと実際のセマンティクスの間の 1 対 1 の対応を定義する場合があります。すべてのシーケンス ポイントで、実際のオブジェクトの値は、抽象セマンティクスによって指定された値と一致します。キーワードvolatileは冗長になります。

あるいは、実装は、翻訳単位の境界を越えて関数呼び出しを行う場合にのみ、実際のセマンティクスが抽象セマンティクスと一致するように、各翻訳単位内でさまざまな最適化を実行する場合があります。このような実装では、呼び出し元の関数と呼び出された関数が異なる翻訳単位にある各関数のエントリと関数の戻り時に、外部にリンクされたすべてのオブジェクトの値と、その中のポインターを介してアクセス可能なすべてのオブジェクトの値が抽象セマンティクスに一致します。 . さらに、そのような各関数エントリの時点で、呼び出された関数のパラメータの値と、その関数内のポインタを介してアクセス可能なすべてのオブジェクトの値は、抽象セマンティクスと一致します。このタイプの実装では、

最後に、C99 標準にも次のように記載されていることに注意してください。

7.14.1.1/5 "signal関数`

abortor関数の呼び出しの結果以外でシグナルが発生した場合、シグナル ハンドラーが...raiseとして宣言されたオブジェクトに値を代入する方法以外で、静的ストレージ期間を持つオブジェクトを参照する場合、動作は未定義です。volatile sig_atomic_t

したがって、厳密に言えば、running変数は次のように宣言する必要がある場合があります。

volatile sig_atomic_t running = 1;
于 2010-03-25T18:57:45.747 に答える
4

の呼び出しputchar()はの値を変更する可能性があるためですrunning(GCCはそれputchar()が外部関数であることのみを認識し、それが何をするのかを知りません-すべてのGCCputchar()が呼び出すことができることを知っていますhandler())。

于 2010-03-25T18:51:11.317 に答える
3

putcharGCCはおそらく、の呼び出しがを含む任意のグローバル変数を変更できると想定していますrunning

関数がグローバル状態に副作用を持たないことを示す純粋関数属性を見てください。putchar()を「純粋」関数の呼び出しに置き換えると、GCCはループ最適化を再導入すると思います。

于 2010-03-25T18:51:59.393 に答える
1

putchar 変更できますrunning

理論的には、リンク時間分析のみがそうではないと判断できます。

于 2011-10-27T02:19:58.723 に答える
1

回答とコメントをありがとうございました。それらは非常に役に立ちましたが、完全なストーリーを提供するものはありません。[編集: Michael Burr の回答は今ではそうであり、これはやや冗長になっています。] ここで要約します。

静的ですrunningが、静的でhandlerはありません。したがって、そのように呼び出されputcharて変更runningされる可能性があります。の実装はputchar現時点では不明であるため、ループhandlerの本体から呼び出すことが考えられます。while

静的handler あるとします。runningそれでは、チェックを最適化できますか? signal実装もこのコンパイル単位の外にあるため、答えはノーです。gcc が知っている限りでは、どこかsignalのアドレスを格納する可能性があり(実際には格納している)、その関数に直接アクセスできない場合でも、このポインターを介して呼び出す可能性があります。handleputcharhandler

では、どのような場合にチェックを最適化して取り除くことができるのでしょうか? runningこれは、ループ本体がこの変換単位の外部から関数を呼び出さない場合にのみ可能であるように思われるため、コンパイル時にループ本体内で何が発生し、何が発生しないかがわかります。

volatileこれは、a を忘れることが実際には最初に思われるほど大したことではない理由を説明しています。

于 2010-03-25T19:42:32.353 に答える