拡張GCC 6オプティマイザが実用的なC ++コードを壊すのはなぜですか?


148

GCC 6には新しいオプティマイザ機能があります。それthisは常にがnull ではないことを想定し、それに基づいて最適化します。

値の範囲の伝播は、C ++メンバー関数のthisポインターがnullでないことを前提としています。これにより、一般的なnullポインターチェック不要になりますが、一部の非準拠コードベース(Qt-5、Chromium、KDevelopなど)も破損します。一時的な回避策として、-fno-delete-null-pointer-checksを使用できます。間違ったコードは、-fsanitize = undefinedを使用して識別できます。

変更文書は、頻繁に使用される驚くべき量のコードを破壊するため、これを危険だと明確に呼んでいます。

なぜこの新しい仮定が実用的なC ++コードを壊すのでしょうか?不注意または知識のないプログラマーがこの特定の未定義の動作に依存する特定のパターンはありますか?if (this == NULL)不自然なため、誰も書いていないようです。


21
@ベンうまくいけば、それは良い意味でそれを意味します。UBを含むコードは、UBを呼び出さないように書き直す必要があります。それはそれと同じくらい簡単です。ヘック、それを達成する方法をあなたに教えるFAQがしばしばあります。したがって、本当の問題ではありません。すべて良い。
モニカの復活

49
コードでnullポインターの逆参照を擁護している人々に驚いた。ただ素晴らしい。
SergeyA

19
@Ben、未定義の振る舞いを爆発させることは、非常に長い間、非常に効果的な最適化戦術でした。コードをより高速に実行する最適化が大好きなので、私はそれが大好きです。
SergeyA

17
SergeyAに同意します。this暗黙のパラメーターとして渡されるという事実に人々がこだわっているように見えるので、問題全体が始まったので、明示的なパラメーターのように使用し始めました。そうではありません。これをnullで逆参照すると、他のnullポインターを逆参照した場合と同じようにUBが呼び出されます。これですべてです。nullptrsを渡したい場合は、明示的なパラメーター DUHを使用します。速度が遅くなることも、不格好になることもありません。そのようなAPIを含むコードは、とにかく内部の深い部分なので、スコープが非常に制限されています。ストーリーの終わりだと思います。
モニカを

41
悪いコードの循環を断ち切ったGCCへの称賛->悪いコードをサポートする非効率的なコンパイラ->より悪いコード->より非効率なコンパイル-> ...
MM

回答:


87

なぜ善意のある人々がそもそも小切手を書くのかという疑問に答える必要があると思います。

最も一般的なケースは、自然に発生する再帰呼び出しの一部であるクラスがある場合です。

あなたが持っていた場合:

struct Node
{
    Node* left;
    Node* right;
};

Cでは、次のように書くことができます。

void traverse_in_order(Node* n) {
    if(!n) return;
    traverse_in_order(n->left);
    process(n);
    traverse_in_order(n->right);
}

C ++では、これをメンバー関数にすると便利です。

void Node::traverse_in_order() {
    // <--- What check should be put here?
    left->traverse_in_order();
    process();
    right->traverse_in_order();
}

C ++の初期(標準化以前)には、そのメンバー関数は、thisパラメーターが暗黙的な関数の構文糖であることが強調されていました。コードはC ++で記述され、同等のCに変換されてコンパイルされました。thisnull と比較することには意味があり、元のCfrontコンパイラもこれを利用するという明示的な例さえありました。したがって、Cのバックグラウンドに由来する、チェックの明らかな選択は次のとおりです。

if(this == nullptr) return;      

注:Bjarne Stroustrupは、ここでのルールがここthis数年変更されたことにも言及しています

そして、これは長年にわたって多くのコンパイラで機能していました。標準化が起こったとき、これは変わりました。そして最近では、コンパイラは、メンバ関数の呼び出しを活用し始めthisビーイングは、nullptrこの条件が常にあることを意味未定義の動作は、あるがfalse、コンパイラはそれを省略して自由です。

つまり、このツリーを走査するには、次のいずれかを実行する必要があります。

  • 呼び出す前にすべてのチェックを行います traverse_in_order

    void Node::traverse_in_order() {
        if(left) left->traverse_in_order();
        process();
        if(right) right->traverse_in_order();
    }
    

    これは、ヌルルートが存在する可能性があるかどうかをすべての呼び出しサイトで確認することも意味します。

  • メンバー関数を使用しない

    これは、古いCスタイルのコード(おそらく静的メソッドとして)を作成し、オブジェクトとしてパラメーターとして明示的に呼び出すことを意味します。例えば。コールサイトではNode::traverse_in_order(node);なく、執筆に戻りnode->traverse_in_order();ます。

  • 標準に準拠した方法でこの特定の例を修正する最も簡単で最も簡単な方法は、実際にはでなくセンチネルノードを使用することだと思いますnullptr

    // static class, or global variable
    Node sentinel;
    
    void Node::traverse_in_order() {
        if(this == &sentinel) return;
        ...
    }
    

最初の2つのオプションはどちらも魅力的ではないように思われます。コードでそれを回避することはできますがthis == nullptr、適切な修正を使用する代わりに、不正なコードを記述しました。

これが、これらのコードベースの一部がthis == nullptrチェックを行うように進化した方法だと思います。


6
1 == 0未定義の動作はどのようにできますか?それは単にfalseです。
Johannes Schaub-litb

11
チェック自体は未定義の動作ではありません。これは常に偽であり、コンパイラによって削除されます。
SergeyA

15
うーん.. this == nullptrイディオムは未定義の動作です。これは、その前にnullptrオブジェクトのメンバー関数を呼び出したため、未定義です。そしてコンパイラはチェックを自由に省略できます
jtlim

6
@Joshua、最初の標準は1998年に公開されました。それ以前に起こったことは、すべての実装が望んでいたことです。暗黒時代。
SergeyA 2016

26
ええと、うわー、インスタンスなしでインスタンス関数を呼び出すことに依存するコードを書いた人がいるなんて信じられません。私は、「traverse_in_orderを呼び出す前にすべてのチェックを実行する」とマークされた抜粋を本能的に使用したでしょうthis。これはおそらく、SOが存在し、UBの危険を脳に定着させ、私がこのような奇妙なハックをするのをやめる時代にC ++を学ぶことの利点だと思います。
underscore_d

65

これは、「実用的な」コードが壊れていて、そもそも未定義の動作が含まれていたためです。nullを使用する理由はありませんthis。通常、これは非常に時期尚早な最適化です。

クラス階層トラバーサルによるポインタの調整は null thisをnull以外のものに変える可能性があるため、これは危険な慣行です。したがって、少なくとも、メソッドがnullで機能するはずのthisクラスは、基本クラスのない最終クラスでなければなりません。何からも派生することはできず、派生することもできません。私たちはすぐに実用的なものから醜いハックランドへと出発します。

実際には、コードは醜いものである必要はありません。

struct Node
{
  Node* left;
  Node* right;
  void process();
  void traverse_in_order() {
    traverse_in_order_impl(this);
  }
private:
  static void traverse_in_order_impl(Node * n)
    if (!n) return;
    traverse_in_order_impl(n->left);
    n->process();
    traverse_in_order_impl(n->right);
  }
};

空のツリーがある場合(たとえば、ルートがnullptr)、このソリューションはnullptrを指定してtraverse_in_orderを呼び出すことにより、未定義の動作に依存しています。

ツリーが空の場合、別名nullの場合、そのツリーでNode* root非静的メソッドを呼び出すことは想定されていません。限目。明示的なパラメーターでインスタンスポインターを取得するCのようなツリーコードを用意しても問題ありません。

ここでの議論は、nullインスタンスポインターから呼び出される可能性のあるオブジェクトに非静的メソッドを記述する必要があるということです。そのような必要はありません。そのようなコードを書くC-with-objectsの方法は、少なくともタイプセーフであるので、C ++の世界ではさらに優れています。基本的に、ヌルthisは非常に微細な最適化であり、使用範囲が狭いため、IMHOを完全に無効にすることはできません。パブリックAPIがnullに依存することはありませんthis


18
@ベン、このコードを書いた人は誰でもそもそも間違っていた。MFC、Qt、Chromiumなどのひどく壊れたプロジェクトに名前を付けているのはおかしいです。それらとの良好なriddance。
SergeyA

19
@ベン、グーグルのひどいコーディングスタイルは私にはよく知られている。複数の人々がGoogleのコードが輝かしい例であると信じているにもかかわらず、Googleのコード(少なくとも一般に入手可能)は、多くの場合ひどく書かれています。これにより、コーディングスタイル(およびガイドラインが適用されている間のガイドライン)を再確認することができます。
SergeyA

18
@BenこれらのデバイスのChromiumをgcc 6を使用してコンパイルされたChromiumに遡って置き換える人はいません。Chromiumをgcc 6を使用してコンパイルする前に、他の最新のコンパイラーを修正する必要があります。それも大きな仕事ではありません。thisチェックはさまざまな静的コードアナライザーによって選択されるため、すべてのチェックを手動で実行する必要があるわけではありません。パッチはおそらく数百行の些細な変更でしょう。
2016

8
@Ben実際には、null this逆参照は瞬時のクラッシュです。これらの問題は、コード上で静的アナライザーを実行する必要がない場合でも、すぐに見つかります。C / C ++は、「使用する機能に対してのみ支払う」というマントラに従います。チェックが必要な場合は、チェックについて明示する必要があります。thisつまり、コンパイラーがthisnull でないと想定しているため、それが遅すぎる場合はチェックを実行しないでください。それ以外の場合はチェックする必要thisがあります。コードの99.9999%では、このようなチェックは時間の無駄です。
モニカを

10
標準が壊れていると思う人への私のアドバイス:別の言語を使用してください。未定義の動作の可能性がないC ++のような言語の不足はありません。
MM

35

変更文書は、頻繁に使用される驚くべき量のコードを破壊するため、これを危険だと明確に呼んでいます。

文書はそれを危険とは呼んでいない。それは驚くべき量のコードを壊すとも主張していません。これは、この未定義の動作に依存していることがわかっており、回避策オプションが使用されない限り、変更が原因で壊れると思われるいくつかの一般的なコードベースを指摘するだけです。

なぜこの新しい仮定が実用的なC ++コードを壊すのでしょうか?

実際の c ++コードが未定義の動作に依存している場合、その未定義の動作を変更すると、動作が壊れる可能性があります。これが、UBに依存するプログラムが意図したとおりに機能しているように見えても、UBが回避される理由です。

不注意または知識のないプログラマーがこの特定の未定義の動作に依存する特定のパターンはありますか?

それが広範囲にわたるアンチパターンであるかどうかはわかりませんが、知識のないプログラマーは、次のようにすることでプログラムがクラッシュしないように修正できると考えるかもしれません。

if (this)
    member_variable = 42;

実際のバグがnullポインタを別の場所で逆参照している場合。

プログラマーに十分な知識がない場合、プログラマーはこのUBに依存するより高度な(アンチ)パターンを思い付くことができると確信しています。

if (this == NULL)不自然なため、誰も書いていないようです。

できます。


11
「実際のc ++コードが未定義の動作に依存している場合、その未定義の動作に変更を加えると、それが壊れる可能性があります。これがUBを回避する理由です」this * 1000
underscore_d

if(this == null) PrintSomeHelpfulDebugInformationAboutHowWeGotHere(); デバッガーが簡単に伝えることができない一連のイベントの読みやすいログなど。大規模なデータセットで突然ランダムなnullが発生したときに、どこにもチェックを配置するのに何時間も費やすことなく、これをデバッグして楽しんでください。コードを記述していない場合...そして、これに関するUBルールは、C ++の作成後に作成されました。以前は有効でした。
ステファンホッケンハル

@StephaneHockenhullそれ-fsanitize=nullが目的です。
eerorika

@ user2079303問題:運用コードを実行中に速度を落として、実行中にチェックインを残すことができず、会社に多大な費用がかかりますか?サイズが大きくなり、フラッシュに収まりませんか?Atmelを含むすべてのターゲットプラットフォームで動作しますか?-fsanitize=nullSPIを使用してピン#5、6、10、11のSD / MMCカードにエラーを記録できますか?それは普遍的な解決策ではありません。nullオブジェクトにアクセスすることはオブジェクト指向の原則に反すると主張する人もいますが、一部のOOP言語には操作可能なnullオブジェクトがあるため、OOPの普遍的なルールではありません。1/2
ステファンホッケンハル2018年

1
...そのようなファイルに一致する正規表現?たとえば、左辺値が2回アクセスされる場合、それらの間のコードがいくつかの特定のことのいずれかを行わない限り、コンパイラはアクセスを統合する可能性があると言い、コードがストレージへのアクセスを許可される正確な状況を定義するよりもはるかに簡単です。
スーパーキャット2018年

25

壊れた「実用的な」(「バギー」を綴る面白い方法)コードの一部は、次のようになります。

void foo(X* p) {
  p->bar()->baz();
}

またp->bar()、ヌルポインターを返すことがあるという事実を説明するのを忘れていました。つまり、それを呼び出すために逆参照することbaz()は定義されていません。

壊れていたではないすべてのコードは、明示的に含まれているif (this == nullptr)if (!p) return;をチェックします。一部のケースは、メンバー変数にアクセスしない単純な関数であり、問​​題なく動作するように見えました。例えば:

struct DummyImpl {
  bool valid() const { return false; }
  int m_data;
};
struct RealImpl {
  bool valid() const { return m_valid; }
  bool m_valid;
  int m_data;
};

template<typename T>
void do_something_else(T* p) {
  if (p) {
    use(p->m_data);
  }
}

template<typename T>
void func(T* p) {
  if (p->valid())
    do_something(p);
  else 
    do_something_else(p);
}

このコードfunc<DummyImpl*>(DummyImpl*)では、nullポインターを使用して呼び出すとp->DummyImpl::valid()、callへのポインターの「概念的な」逆参照がありますが、実際には、メンバー関数はfalseにアクセスせずに戻るだけ*thisです。これreturn falseはインライン化できるため、実際にはポインタにアクセスする必要はまったくありません。したがって、一部のコンパイラでは問題なく動作するように見えます。nullを逆参照するためのsegfaultはなく、p->valid()falseであるため、コードはdo_something_else(p)nullポインタをチェックするを呼び出し、何もしません。クラッシュや予期しない動作は見られません。

GCC 6を使用しても引き続きが呼び出されますp->valid()が、コンパイラーはpnull ではない式(そうでない場合p->valid()は未定義の動作)からその式を推測し、その情報を書き留めます。推定された情報はオプティマイザによって使用されるため、への呼び出しdo_something_else(p)がインライン化された場合if (p)、コンパイラはそれがnullではないことを記憶し、コードをインライン化するため、チェックは冗長と見なされます。

template<typename T>
void func(T* p) {
  if (p->valid())
    do_something(p);
  else {
    // inlined body of do_something_else(p) with value propagation
    // optimization performed to remove null check.
    use(p->m_data);
  }
}

これは実際にはnullポインターを逆参照するため、以前は機能していたコードが機能しなくなります。

この例では、バグはにありfunc、最初にnullをチェックする必要がありました(または、呼び出し元がnullで呼び出してはなりませんでした)。

template<typename T>
void func(T* p) {
  if (p && p->valid())
    do_something(p);
  else 
    do_something_else(p);
}

覚えておくべき重要な点は、このようなほとんどの最適化は、コンパイラーが「ああ、プログラマーがこのポインターをnullに対してテストしたので、煩わしいために削除する」というものではないということです。何が起こるかというと、インライン化や値の範囲の伝播など、さまざまな一般的な最適化が組み合わされて、これらのチェックが冗長になるためです。これは、以前のチェックまたは逆参照の後に行われるためです。コンパイラーが、ポインターが関数内の点Aで非ヌルであることを認識し、ポインターが同じ関数内の後続の点Bの前に変更されない場合、コンパイラーは、ポインターもBで非ヌルであることを認識します。ポイントAとBは、実際には元々別々の関数に含まれていたコードの一部である可能性がありますが、現在は1つのコードに結合されており、コンパイラーは、ポインターがより多くの場所で非ヌルであるという知識を適用できます。


GCC 6をインストルメントして、そのような使用法に遭遇したときにコンパイル時の警告を出力することは可能thisですか?
jotik


3
@jotik、^^^ TCの発言。それは可能ですが、すべてのコードに対して常に警告が表示されます。値の範囲の伝播は、最も一般的な最適化の1つであり、ほとんどすべてのコードに影響を与えます。オプティマイザはコードを見るだけで、簡略化できます。彼らは、「馬鹿なUBが最適化されたら警告されることを望んでいるばかによって書かれたコードの一部」を目にしません。コンパイラーが「プログラマーが最適化したいと考える冗長チェック」と「プログラマーが役立つと考えるが冗長であるという冗長チェック」の違いを見分けるのは容易ではありません。
ジョナサンウェイクリー2016

1
の無効な使用を含め、さまざまなタイプのUBのランタイムエラーを発生させるようにコードをインストルメントする場合はthis、次のように使用します-fsanitize=undefined
Jonathan Wakely


-25

C ++標準は重要な方法で壊れています。残念ながら、これらの問題からユーザーを保護するのではなく、GCCの開発者は、いかに有害であるかが明確に説明されていても、未定義の動作を口実として限界最適化を実装することを選択しました。

ここで私が非常に詳細に説明するよりもはるかに賢い人です。(彼はCについて話していますが、状況は同じです)。

なぜ有害なのですか?

以前に機能していた安全なコードを新しいバージョンのコンパイラで単純に再コンパイルすると、セキュリティの脆弱性が発生する可能性があります。フラグを使用して新しい動作を無効にすることができますが、既存のメイクファイルには明らかにそのフラグが設定されていません。また、警告が生成されないため、以前の妥当な動作が変更されたことは開発者には明らかではありません。

この例では、開発者はを使用して整数オーバーフローのチェックを組み込んでおりassert、無効な長さが指定された場合にプログラムを終了します。GCCチームは、整数オーバーフローが定義されていないことに基づいてチェックを削除したため、チェックを削除できます。これにより、問題が修正された後、このコードベースの実際の実例が脆弱性の影響を受けて再作成されました。

全部読んでください。それはあなたを泣かせるのに十分です。

わかりましたが、これはどうですか?

昔、かなり一般的なイディオムが次のようなものでした。

 OPAQUEHANDLE ObjectType::GetHandle(){
    if(this==NULL)return DEFAULTHANDLE;
    return mHandle;

 }

 void DoThing(ObjectType* pObj){
     osfunction(pObj->GetHandle(), "BLAH");
 }

したがって、イディオムは次のとおりです。pObjがnullでない場合は、そのハンドルを使用します。それ以外の場合は、デフォルトのハンドルを使用します。これはGetHandle関数にカプセル化されます。

トリックは、非仮想関数を呼び出しても実際にはthisポインターを使用しないため、アクセス違反は発生しないということです。

まだわかりません

そのように書かれた多くのコードが存在します。行を変更せずに誰かが単純に再コンパイルした場合、へのすべての呼び出しDoThing(NULL)はクラッシュするバグです-運が良ければ。

運が悪いと、クラッシュするバグの呼び出しがリモート実行の脆弱性になります。

これは自動的に発生することもあります。自動化されたビルドシステムがありますよね?最新のコンパイラにアップグレードしても無害ですよね?しかし、今はそうではありません。コンパイラがGCCの場合はそうではありません。

わかりましたので教えてください!

彼らは言われました。彼らは結果の完全な知識の中でこれを行っています。

しかし、なぜ?

誰が言えるの?おそらく:

  • 彼らは実際のコードよりもC ++言語の理想的な純粋さを評価します
  • 彼らは人々が基準に従わないことで罰せられるべきだと信じています
  • 彼らは世界の現実を理解していない
  • 彼らは...故意にバグを紹介しています。多分外国政府のために。どこに住んでいますか?すべての政府は世界のほとんどに対して外国であり、ほとんどは世界の一部に対して敵対的です。

またはおそらく何か他のもの。誰が言えるの?


32
答えのすべての1行に同意しません。厳密なエイリアシングの最適化についても同じコメントが出されましたが、それらは今は却下されると期待されます。解決策は、開発者を教育することであり、悪い開発習慣に基づく最適化を妨げないことです。
SergeyA

30
私はあなたが言ったように全部を読んで実際に泣きましたが、主にフェリックスの愚かさで、私があなたが乗り越えようとしていたものではないと思います...
Mike Vine

33
無駄な暴言のために反対票を投じた。「彼らは...故意にバグを導入しています。おそらく外国政府のためです。」本当に?これは/ r / conspiracyではありません。
isanae

31
きちんとしたプログラマーは何度も何度もマントラを繰り返しますが未定義の動作は発生しませんが、これらのnonkは先に進んでそれを行っています。そして何が起こったかを見てください。私は全く同情しません。これは開発者の責任であり、そのように単純です。彼らは責任を取る必要があります。覚えてる?個人的な責任?あなたのマントラに依存している人々は「しかし実際にはどうですか!」そもそもこのような状況が生じたのです。このようなナンセンスを回避することが、そもそも標準が存在する理由です。標準に準拠したコードを作成すれば、問題は発生しません。限目。
オービットでの軽さのレース2016年

18
「以前に機能していた安全なコードを新しいバージョンのコンパイラで単純に再コンパイルすると、セキュリティの脆弱性が発生する可能性があります」- これは常に発生します。1つのコンパイラの1つのバージョンが、残りの部分で許可される唯一のコンパイラであることを義務付けたい場合を除きます。Linuxカーネルが正確にgcc 2.7.2.1でのみコンパイルできたときのことを覚えていますか?人々がブルクラップにうんざりしていたため、gccプロジェクトは分岐しました。それを乗り越えるには長い時間がかかりました。
MM
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.