web-dev-qa-db-ja.com

コンパイラは、ヒープメモリの割り当てを最適化できますか?

newを使用する次の簡単なコードを検討してください(delete[]、ただしこの質問には関係ありません):

int main()
{
    int* mem = new int[100];

    return 0;
}

コンパイラはnew呼び出しを最適化することを許可されていますか?

私の研究では、 g ++(5.2.0) であり、Visual Studio 2015はnew呼び出しを最適化しません。 clang(3.0+)はそうします 。すべてのテストは、完全な最適化を有効にして行われました(g ++およびclangの場合は-O3、Visual Studioのリリースモード)。

newはシステムコールを内部で実行し、コンパイラがそれを最適化することを不可能(および違法)にしないのですか?

[〜#〜] edit [〜#〜]:未定義の動作をプログラムから除外しました:

#include <new>  

int main()
{
    int* mem = new (std::nothrow) int[100];
    return 0;
}

clang 3.0はそれを最適化しない もう、 後のバージョンはそうする

EDIT2

#include <new>  

int main()
{
    int* mem = new (std::nothrow) int[1000];

    if (mem != 0)
      return 1;

    return 0;
}

clangは常に1を返します

66
Banex

歴史的には、clangは N3664:Clarification Memory Allocation で規定されている規則に従っているようです。これにより、コンパイラはメモリ割り当てを最適化できますが、 Nick Lewyckyが指摘する として:

シャフィクは因果関係に違反しているように見えるが、N3664はN3433として始まり、最初に最適化を記述し、とにかく論文を書いたと確信しています。

そのため、clangは最適化を実装し、後にC++ 14の一部として実装される提案になりました。

基本的な質問は、これがN3664より前の有効な最適化であるかどうか、つまり難しい質問です。ドラフトC++標準セクション1.9で説明されている as-ifルール に移動する必要があります。プログラム実行は(emphasis mine =):

この国際規格のセマンティック記述は、パラメータ化された非決定的な抽象マシンを定義しています。この国際規格では、準拠する実装の構造に要件はありません。特に、抽象マシンの構造をコピーまたはエミュレートする必要はありません。むしろ、準拠する実装は、以下で説明するように、抽象マシンの観察可能な動作をエミュレートする(のみ)ために必要です。5

note 5は次のとおりです。

この規定は、「as-if」ルールと呼ばれることもあります。これは、結果が次のとおりである限り、実装はこの国際規格の要件を自由に無視できるためです。プログラムの観察可能な動作から判断できる限り、要件が満たされているかのように。たとえば、実際の実装は、その値が使用されておらず、プログラムの観察可能な動作に影響を与える副作用がないと推定できる場合、式の一部を評価する必要はありません。

newはプログラムの戻り値を変更するため、観察可能な動作を持つ例外をスローする可能性があるため、as-ifルールで許可されていることに反するようです。

ただし、例外をいつスローするかは実装の詳細であるため、clangはこのシナリオでも例外を引き起こさないと判断できるため、new呼び出しを排除してもas -ifルール

as-ifルールの下でも有効であるようで、非スローバージョンへの呼び出しも最適化されます。

しかし、別の翻訳単位で新しい置換グローバル演算子を使用して、これが観察可能な動作に影響を与える可能性があるため、コンパイラはこれが当てはまらないことを証明する何らかの方法を持たなければなりません。そうでなければ、この最適化を実行できませんas-ifルールに違反することなく。この場合、以前のバージョンのclangは このgodboltの例が示す として最適化されていました。このコードは Casey here で提供され、

#include <cstddef>

extern void* operator new(std::size_t n);

template<typename T>
T* create() { return new T(); }

int main() {
    auto result = 0;
    for (auto i = 0; i < 1000000; ++i) {
        result += (create<int>() != nullptr);
    }

    return result;
}

そしてこれを最適化する:

main:                                   # @main
    movl    $1000000, %eax          # imm = 0xF4240
    ret

これは確かに攻撃的すぎるように見えますが、それ以降のバージョンではそうではないようです。

51
Shafik Yaghmour

これは N3664 で許可されています。

実装では、置換可能なグローバル割り当て関数(18.6.1.1、18.6.1.2)の呼び出しを省略することができます。その場合、ストレージは代わりに実装によって提供されるか、別の新しい式の割り当てを拡張することによって提供されます。

この提案はC++ 14標準の一部であるため、C++ 14では、コンパイラisnewの最適化を許可されます。式(スローする場合でも)。

Clang implementation status を見ると、N3664を実装していることが明確に示されています。

C++ 11またはC++ 03でコンパイル中にこの動作を観察した場合は、バグを埋める必要があります。

C++ 14動的メモリ割り当ての前は、プログラムの監視可能なステータスの一部であることに注意してください(現時点ではそのためのリファレンスは見つかりませんが) )、したがって、この場合、適合実装はas-ifルールを適用できませんでした。

18
sbabbi

C++標準は、正しいプログラムの実行方法ではなく、実行すべき内容を示していることに留意してください。標準が書かれ、標準がそれらに使用されなければならない後に、新しいアーキテクチャが発生する可能性があり、実際に発生するので、それは後でまったく伝えることができません。

newは、内部でシステムコールである必要はありません。オペレーティングシステムやシステムコールの概念がなくても使用できるコンピューターがあります。

したがって、終了動作が変わらない限り、コンパイラはあらゆるものを最適化できます。含むnew

注意点が1つあります。
新しいグローバルオペレーターnewが別の翻訳単位で定義されている可能性があります
その場合、newの副作用は、最適化で除去できないようなものになる可能性があります。ただし、投稿されたコードがコード全体である場合のように、コンパイラがnew演算子に副作用がないことを保証できる場合、最適化は有効です。
newがstd :: bad_allocをスローできることは必須ではありません。この場合、newが最適化されると、コンパイラーは例外がスローされず、副作用が発生しないことを保証できます。

コンパイラーが元の例の割り当てを最適化することは完全に許容されます(ただし、not required)。さらに標準1.9のEDIT1の例では、通常はas-ifルールと呼ばれます:

準拠する実装は、以下で説明するように、抽象マシンの観察可能な動作をエミュレートする(のみ)ために必要です。
[3ページの条件]

cppreference.com で、より人間が読みやすい表現を入手できます。

関連するポイントは次のとおりです。

  • 揮発性物質がないため、1)および2)は適用されません。
  • データの出力/書き込みを行わないか、ユーザーにプロンプ​​トを表示しないため、3)および4)は適用されません。しかし、たとえそれを行ったとしても、EDIT1で明らかに満足します(元の例ではおそらくalsoですが、純粋に理論的な観点からすると、プログラムは違法です理論的にはフローと出力は異なりますが、以下の2つの段落を参照してください)。

例外は、キャッチされていなくても、明確に定義された(未定義ではない!)動作です。ただし、厳密に言うと、newがスローされる場合(発生しない、次の段落も参照)、プログラムの終了コードと、後に続く出力によって、観察可能な動作が異なります。プログラム。

さて、単一の小さな割り当ての特定のケースでは、コンパイラに"疑いの恩恵"を与えることができます割り当てが失敗しないこと。
非常に高いメモリプレッシャーのシステムでも、使用可能な最小割り当て粒度よりも小さい場合、プロセスを開始することさえできず、mainも。そのため、この割り当てが失敗した場合、プログラムは開始されないか、mainが呼び出される前にすでに不自然な終了に遭遇していました。
これまでのところ、割り当てが理論的にはthrowであったとしても、コンパイラがこれを知っていると仮定すると、元の例を最適化することさえ正当です。practically発生しないことを保証できます。

<やや未定>
一方、notは許可されません(また、ご覧のように、コンパイラのバグ)。 EDIT2の例。この値は、外部から観察可能な効果(戻りコード)を生成するために消費されます。
_new (std::nothrow) int[1000]new (std::nothrow) int[1024*1024*1024*1024ll](4TiBの割り当てです!)に置き換えた場合、これは-現在のコンピューターでは-確実に失敗することに注意してください。コールアウト。つまり、0を出力する必要があるコードを記述したにもかかわらず、1を返します。

@Yakkはこれに対して適切な引数を提示しました。メモリに触れない限り、ポインタを返すことができ、実際のRAMは必要ありません。 EDIT2の割り当て。ここで誰が正しいのか、誰が間違っているのかわかりません。

OSがページテーブルを作成する必要があるという理由だけで、少なくとも2桁のギガバイト量のRAMなど)を持たないマシンでは、4TiB割り当てを行うことはほぼ確実に失敗します。もちろん、C++標準では、ページテーブルや、メモリを提供するためにOSが行っていることは重要ではありません。

しかし、一方で、「これはメモリに触れなくても動作します」は、正確にそのような詳細とOSが提供するものに依存しますを仮定します。 RAM触れていない場合、実際には必要ないという仮定はtruebecauseのみで、OSは仮想メモリを提供します。 OSがページテーブルを作成する必要があること(私はそれについて知らないふりをすることができますが、とにかくそれに依存しているという事実は変わりません)。

したがって、最初に一方を仮定してから「他方については気にしない」と言うのは100%正しいとは思いません。

そのため、はい、コンパイラcanは、メモリに触れない限り4TiBの割り当てが一般的に完全に可能であると仮定し、canは、一般的に成功する可能性があると仮定します。成功する可能性が高いと仮定することもあります(成功しない場合でも)。しかし、どのような場合でも、障害の可能性があるときに何かmustが機能すると想定することは決してできないと思います。そして、失敗の可能性があるだけでなく、その例では、失敗は可能性が高い可能性です。
</未定>

7
Damon

スニペットで発生する可能性がある最悪の事態は、newstd::bad_allocをスローすることです。これは未処理です。その場合に起こることは実装定義です。

最良のケースはノーオペレーションであり、最悪のケースは定義されていないため、コンパイラーはそれらを存在しない要因に含めることができます。さて、あなたが実際に可能性のある例外をキャッチしようとした場合:

int main() try {
    int* mem = new int[100];
    return 0;
} catch(...) {
  return 1;
}

...次に operator newへの呼び出しが保持されます

2
Quentin