未使用のメンバー変数はメモリを消費しますか?


91

メンバー変数を初期化し、それを参照/使用しないと、実行時にRAMをさらに消費しますか、またはコンパイラーは単にその変数を無視しますか?

struct Foo {
    int var1;
    int var2;

    Foo() { var1 = 5; std::cout << var1; }
};

上記の例では、メンバー 'var1'が値を取得し、コンソールに表示されます。ただし、「Var2」はまったく使用されません。したがって、実行時にそれをメモリに書き込むと、リソースの無駄になります。コンパイラーはこのような状況を考慮に入れて、未使用の変数を単に無視しますか、またはメンバーが使用されているかどうかに関係なく、Fooオブジェクトは常に同じサイズですか?


25
これは、コンパイラ、アーキテクチャ、オペレーティングシステム、および使用する最適化によって異なります。
フクロウ

16
ハードウェアデータフレームのサイズに一致するようにパディング用に、また必要なメモリアライメントを取得するためのハックとして、何もしないstructメンバーを具体的に追加するメトリックトンの低レベルドライバーコードがあります。コンパイラがこれらを最適化し始めた場合、多くの破損が発生します。
Andy Brown、

2
@Andy次のデータメンバーのアドレスが評価されるため、実際には何もしません。これは、これらのパディングメンバーの存在がプログラムで観察可能な動作を行うことを意味します。ここでvar2はありません。
YSC

4
そのような構造体をアドレス指定しているコンパイル単位が同じ構造体を使用している別のコンパイル単位にリンクされる可能性があり、別のコンパイル単位がメンバーをアドレス指定しているかどうかコンパイラーが認識できない場合、コンパイラーがそれを最適化できれば驚きます。
Galik

2
@geza sizeof(Foo)は、定義によって減少することはできません-印刷sizeof(Foo)する場合、8(一般的なプラットフォーム上で)生成する必要があります。コンパイラー、LTOやプログラム全体の最適化を行わなくても、(スタックvar2経由new、スタック上、関数呼び出しのいずれの場合でも)使用するスペース最適化できます。それが不可能な場合、他の最適化と同様に、彼らはそれを行いません。承認された回答を編集すると、誤解される可能性が大幅に低くなると思います。
Max Langhof

回答:


106

「と-IF」のルール黄金C ++ 1つの、状態ならば観測可能な行動計画の未使用データメンバーの存在に依存しない、それを離れて最適化するために、コンパイラが許可されています

未使用のメンバー変数はメモリを消費しますか?

いいえ(「本当に」未使用の場合)。


ここで、2つの質問が頭に浮かびます。

  1. 観察可能な行動がメンバーの存在に依存しないのはいつですか?
  2. そのような状況は実際のプログラムで発生しますか?

例から始めましょう。

#include <iostream>

struct Foo1
{ int var1 = 5;           Foo1() { std::cout << var1; } };

struct Foo2
{ int var1 = 5; int var2; Foo2() { std::cout << var1; } };

void f1() { (void) Foo1{}; }
void f2() { (void) Foo2{}; }

この翻訳単位をコンパイルするようにgccに要求すると、次のように出力されます。

f1():
        mov     esi, 5
        mov     edi, OFFSET FLAT:_ZSt4cout
        jmp     std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
f2():
        jmp     f1()

f2はと同じf1であり、実際のを保持するためにメモリが使用されることはありませんFoo2::var2。(Clangは同様のことを行います)。

討論

これは2つの理由で異なると言う人もいます。

  1. これは簡単な例です
  2. 構造体は完全に最適化されており、カウントされません。

まあ、良いプログラムは、複雑なものの単純な並列ではなく、単純なもののスマートで複雑なアセンブリです。実際には、コンパイラーが最適化するよりも単純な構造を使用して、大量の単純な関数を記述します。例えば:

bool insert(std::set<int>& set, int value)
{
    return set.insert(value).second;
}

これは、std::pair<std::set<int>::iterator, bool>::first未使用のデータメンバー(ここでは)の純粋な例です。何だと思う?それは離れて最適化されますダミーセットを使用した簡単な例そのアセンブリがあなたを泣かせる場合)。

マックスラングホフの優れた答え読むのに最適な時期です(私のために賛成してください)。最終的に、コンパイラの出力するアセンブリレベルでは構造の概念が意味をなさない理由を説明します。

「しかし、私がXを実行すると、未使用のメンバーが最適化されて離れてしまうという事実が問題になります!」

いくつかの操作(などassert(sizeof(Foo2) == 2*sizeof(int)))が何かを壊すので、この答えは間違っているに違いないと主張する多くのコメントがあります。

Xがプログラムの監視可能な動作の一部である場合2、コンパイラーは最適化することを許可されません。プログラムに目に見える影響を与える「未使用」のデータメンバーを含むオブジェクトには多くの操作があります。そのような操作が実行された場合、またはコンパイラーが何も実行されなかったことを証明できない場合、その「未使用」のデータメンバーはプログラムの監視可能な動作の一部であり、最適化して取り除くことはできません

観察可能な動作に影響を与える操作には、次のものが含まれますが、これらに限定されません。

  • オブジェクトのタイプのサイズ(sizeof(Foo))、
  • 「未使用」の後に宣言されたデータメンバーのアドレスを取得します。
  • 以下のような機能を持つオブジェクトをコピーしmemcpy
  • オブジェクトの表現を操作する(などmemcmp
  • オブジェクト予選揮発性
  • など

1)

[intro.abstract]/1

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

2)アサートが成功するか失敗するかのようです。


回答の改善を示唆するコメントがチャットにアーカイブされました
コーディグレイ

1
でも、assert(sizeof(…)…)実際には制約はありませんコンパイラ-それが提供しなければならないsizeofようなものでコードできるようmemcpy仕事にするが、それは彼らがそのようなAにさらされる可能性がある場合を除き、コンパイラは何とかそのバイト数を使用する必要が意味するものではありませんmemcpyそれはできます「Tはとにかく正しい値を生成するために書き換えます。
Davis Herring

@Davis絶対に。
YSC

63

コンパイラーが生成するコードにはデータ構造の実際の知識がないこと(アセンブリーレベルには存在しないため)とオプティマイザーも認識しないことが重要です。コンパイラは、データ構造ではなく、各関数のコードのみを生成します

わかりました、それも定数データセクションなどを書き込みます。

これに基づいて、オプティマイザーはデータ構造を出力しないため、メンバーを「削除」または「削除」しないと既に言うことができます。メンバーを使用する場合と使用しない場合があるコードを出力します。その目的の1つは、メンバーの無意味な使用(つまり、書き込み/読み取り)を排除することでメモリまたはサイクルを節約することです。


その要点は、「コンパイラーが関数の範囲内(インライン化された関数を含む)で、未使用のメンバーが関数の動作(および関数が返すもの)に違いがないことを証明できる場合、その可能性はメンバーの存在はオーバーヘッドを引き起こしません。」

関数と外界との相互作用をコンパイラーにとってより複雑/不明確にする(より複雑なデータ構造を取得/返すなど) std::vector<Foo>、別のコンパイル単位で関数の定義を非表示にする、インライン化を禁止/無効化するなど)。 、使用されていないメンバーが効果がないことをコンパイラーが証明できない可能性が高まっています。

コンパイラーが行う最適化にすべて依存するため、ここには難しいルールはありませんが、(YSCの回答に示されているように)些細なことを行う限り、複雑なこと(たとえば、std::vector<Foo>インライン化するには大きすぎる関数からは)おそらくオーバーヘッドが発生します。


ポイントを説明するために、次の例を検討しください。

struct Foo {
    int var1 = 3;
    int var2 = 4;
    int var3 = 5;
};

int test()
{
    Foo foo;
    std::array<char, sizeof(Foo)> arr;
    std::memcpy(&arr, &foo, sizeof(Foo));
    return arr[0] + arr[4];
}

ここでは重要なこと(アドレスの取得、バイト表現からのバイトの検査と追加)を行いますが、オプティマイザはこのプラットフォームで結果が常に同じであることを理解できます。

test(): # @test()
  mov eax, 7
  ret

のメンバーはFooメモリを占有しなかっただけでなく、Foo存在することすらありませんでした!最適化できない他の使用法がある場合、たとえば、sizeof(Foo)問題になる可能性がありますが、コードのそのセグメントに対してのみです!このようにすべての使用法を最適化できる場合、たとえばの存在はvar3生成コードに影響しません。しかし、それが別の場所で使用されている場合でも、test()最適化されたままになります!

つまり、の各使用法Fooは個別に最適化されます。不要なメンバーが原因でより多くのメモリを使用する場合もあれば、使用しない場合もあります。詳細については、コンパイラのマニュアルを参照してください。


6
Mic drop 「詳細については、コンパイラのマニュアルを参照してください。」:D
YSC 2019年

22

コンパイラーは、変数を削除しても副作用がなく、プログラムのどの部分Fooも同じサイズに依存していないことを証明できる場合にのみ、未使用のメンバー変数(特にパブリック変数)を最適化します。

構造が実際にまったく使用されていない限り、現在のコンパイラーがそのような最適化を実行するとは思いません。一部のコンパイラは、少なくとも未使用のプライベート変数について警告する場合がありますが、通常はパブリック変数については警告しません。


1
それでもなお、godbolt.org / z / UJKguS +未使用のデータメンバーについて警告するコンパイラはありません。
YSC

@YSC clang ++は未使用のデータメンバーと変数について警告します。
Maxim Egorushkin

3
@YSCこれは少し異なる状況だと思います。構造が完全に最適化され、5が直接印刷されるだけです
Alan Birtles

4
@AlanBirtlesどう違うのかわかりません。コンパイラは、プログラムの観察可能な動作に影響を与えないオブジェクトからすべてを最適化しました。したがって、「コンパイラが未使用のメンバー変数を最適化する可能性は非常に低い」という最初の文は間違っています。
YSC

2
@YSCが実際のコードで使用されており、構造がその副作用のために単に構築されているのではなく実際に使用されている場合、おそらく最適化されない可能性が高い
Alan Birtles

7

一般に、たとえば「未使用」のメンバー変数がそこにあるなど、要求したものを取得すると想定する必要があります。

この例では両方のメンバーがpublicであるため、コンパイラーは(特に他の翻訳単位=他の* .cppファイルから、別々にコンパイルされてリンクされている)一部のコードが「未使用」メンバーにアクセスするかどうかを知ることができません。

YSCの答えは、クラスタイプが自動ストレージ期間の変数としてのみ使用され、その変数へのポインターが取得されない、非常に単純な例を示しています。そこで、コンパイラーはすべてのコードをインライン化して、すべての不要なコードを除去できます。

異なる変換単位で定義された関数間のインターフェースがある場合、通常、コンパイラーは何も知りません。インターフェイスは通常、事前定義されたいくつかのABIに従います(そのような異なるオブジェクトファイルは問題なく一緒に連結することができるように)。通常、メンバーが使用されているかどうかにかかわらず、ABIは違いを生じません。したがって、このような場合、2番目のメンバーは物理的にメモリ内になければなりません(後でリンカによって削除されない限り)。

そして、あなたが言語の境界内にいる限り、排除が起こるのを観察することはできません。あなたが電話すればsizeof(Foo)、あなたは得るでしょう2*sizeof(int)Foosの配列を作成する場合、2つの連続するオブジェクトの先頭間の距離Fooは常にsizeof(Foo)バイトです。

タイプは標準のレイアウトタイプです。つまり、コンパイル時に計算されたオフセットに基づいてメンバーにアクセスすることもできます(offsetofマクロを参照)。さらに、charusingの配列にコピーすることにより、オブジェクトのバイトごとの表現を検査できますstd::memcpy。これらすべてのケースで、2番目のメンバーがそこにあることがわかります。


コメントは、詳細な議論のためのものではありません。この会話はチャットに移動しました
コーディグレイ

2
+1:ローカルの構造体オブジェクトが完全に最適化されていない場合、積極的なプログラム全体の最適化のみがデータレイアウト(コンパイル時のサイズとオフセットを含む)を調整する可能性があります。 gcc -fwhole-program -O3 *.c理論的にはそれを行うことができますが、実際にはおそらくできません。(たとえば、プログラムがsizeof()このターゲットにどのような正確な値があるかについていくつかの仮定を行っている場合、およびプログラマが望む場合、プログラマが手動で行うべき非常に複雑な最適化であるため。)
Peter Cordes

6

この質問に対する他の回答によって提供された、除外された例はvar2、単一の最適化手法に基づいています。定数の伝播、およびその後の構造全体の省略(単なるの省略ではありませんvar2)。これは単純なケースであり、最適化コンパイラはそれを実装します。

アンマネージC / C ++コードの答えは、コンパイラーは一般にelideしないということvar2です。私の知る限り、デバッグ情報ではこのようなC / C ++構造体変換はサポートされていません。また、構造体がデバッガーの変数としてアクセスできる場合は、var2省略できません。私の知る限り、現在のC / C ++コンパイラはの省略に従って関数を特殊var2var2できません。そのため、構造体が非インライン関数に渡されるか、インライン関数から返された場合、省略できません。

JITコンパイラを備えたC#/ Javaなどのマネージ言語の場合、コンパイラvar2は、使用されているかどうか、およびアンマネージコードにエスケープするかどうかを正確に追跡できるため、安全に除外できる場合があります。管理された言語での構造体の物理的なサイズは、プログラマーに報告されるサイズとは異なる場合があります。

2019年のC / C ++コンパイラはvar2、構造体変数全体が省略されない限り、構造体から除外できません。var2構造体からの省略の興味深いケースの場合、答えは次のとおりです。

将来の一部のC / C ++コンパイラーはvar2構造体から脱出できるようになり、コンパイラーを中心に構築されたエコシステムは、コンパイラーによって生成されるプロセス省略情報に適応する必要があります。


1
デバッグ情報に関する段落は、「それがデバッグを困難にする場合、最適化することはできません」と要約しますが、これはまったく間違っています。または私は誤解しています。明確にしてもらえますか?
Max Langhof

コンパイラーが構造体に関するデバッグ情報を出力する場合、var2を除外できません。オプションは次のとおりです:(1)構造体の物理表現に対応していない場合、デバッグ情報を出力しません、(2)デバッグ情報で構造体メンバーの省略をサポートし、デバッグ情報を出力します
atomsymbol

おそらくより一般的なのは、集計のスカラー置換(およびデッドストアの省略など)を参照することです。
デイビスヘリング

4

コンパイラとその最適化レベルに依存します。

gccでは、を指定する-Oと、次の最適化フラグがオンになります

-fauto-inc-dec 
-fbranch-count-reg 
-fcombine-stack-adjustments 
-fcompare-elim 
-fcprop-registers 
-fdce
-fdefer-pop
...

-fdceDead Code Eliminationの略です。

を使用__attribute__((used))して、gccが静的ストレージで未使用の変数を削除しないようにすることができます。

この属性は、静的ストレージを使用して変数に関連付けられているため、変数が参照されていないように見えても、変数を発行する必要があります。

C ++クラステンプレートの静的データメンバーに適用される場合、この属性は、クラス自体がインスタンス化されている場合にメンバーがインスタンス化されることも意味します。


これは、静的データメンバー用であり、未使用のインスタンスごとのメンバーではありません(オブジェクト全体がそうでない限り、最適化されません)。しかし、はい、それは数えられると思います。ところで、GCCが用語を曲げない限り、未使用の静的変数を排除することはデッドコードの排除ではありません。
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.