メンバー変数を初期化し、それを参照/使用しないと、実行時にRAM)が必要になりますか、それともコンパイラーは単にその変数を無視しますか?
struct Foo {
int var1;
int var2;
Foo() { var1 = 5; std::cout << var1; }
};
上記の例では、メンバー「var1」が値を取得し、コンソールに表示されます。ただし、「Var2」はまったく使用されません。したがって、実行時にそれをメモリに書き込むと、リソースの無駄になります。コンパイラーはこのような状況を考慮に入れて、未使用の変数を単に無視しますか、それとも、メンバーが使用されているかどうかに関係なく、Fooオブジェクトは常に同じサイズですか?
ゴールデンC++の「as-if」ルール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つの理由で異なると言う人もいます。
まあ、良いプログラムは、複雑なものの単純な並列ではなく、単純なもののスマートで複雑なアセンブリです。実際には、コンパイラーが最適化するよりも単純な構造を使用して、大量の単純な関数を記述します。例えば:
_bool insert(std::set<int>& set, int value)
{
return set.insert(value).second;
}
_
これは、未使用のデータメンバー(ここでは_std::pair<std::set<int>::iterator, bool>::first
_)の純粋な例です。何だと思う? 最適化により削除されます ( ダミーセットを使用した簡単な例 そのアセンブリで泣く場合)。
さて、これが Max Langhofの優れた答えを読んでください (私のためにそれを賛成してください)に最適な時間です。最終的に、コンパイラの出力するアセンブリレベルでは構造の概念が意味をなさない理由を説明します。
いくつかの操作(assert(sizeof(Foo2) == 2*sizeof(int))
など)が何かを壊すので、この答えは間違っているに違いないと主張するコメントがいくつかあります。
Xがプログラムの監視可能な動作の一部である場合2、コンパイラーは、最適化することを許可されていません。プログラムに観察可能な影響を与える「未使用」のデータメンバーを含むオブジェクトには多くの操作があります。このような操作が実行された場合、またはコンパイラーが何も実行されなかったことをコンパイラーが証明できない場合、その「未使用」データメンバーはプログラムの監視可能な動作の一部であり、最適化できません。
観察可能な動作に影響を与える操作には、次のものが含まれますが、これらに限定されません。
sizeof(Foo)
)、memcpy
のような関数でオブジェクトをコピーする、memcmp
など)1)
このドキュメントのセマンティックの説明は、パラメータ化された非決定論的な抽象マシンを定義しています。このドキュメントでは、準拠する実装の構造に要件はありません。特に、抽象マシンの構造をコピーまたはエミュレートする必要はありません。むしろ、以下で説明するように、抽象マシンの観察可能な動作を(のみ)エミュレートするには、準拠する実装が必要です。
2) アサートが成功するか失敗するかのようです。
コンパイラーが生成するコードにはデータ構造の実際の知識がないこと(アセンブリーレベルには存在しないため)とオプティマイザーも認識しないことが重要です。コンパイラーは、データ構造ではなく、関数ごとにcodeのみを生成します。
わかりました、それも定数データセクションなどを書き込みます。
これに基づいて、オプティマイザーはデータ構造を出力しないため、メンバーを「削除」または「削除」しないと既に言うことができます。 codeを出力します。これは、メンバーのuseである場合とない場合があり、その目的の中で、無意味なメンバーの使用(つまり、書き込み/読み取り)。
その要点は、「コンパイラーが関数のスコープ内で(インライン化された関数を含む)を証明できる場合、未使用のメンバーは関数の方法に違いがないことです動作する(そしてそれが返すもの)場合、メンバーの存在がオーバーヘッドを引き起こさない可能性が高いです。
関数と外界との相互作用をコンパイラに対してより複雑/不明確にする(_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
の各使用法は個別に最適化されます。不要なメンバーのために、より多くのメモリを使用する場合とそうでない場合があります。詳細については、コンパイラのマニュアルを参照してください。
コンパイラーは、変数を削除しても副作用がなく、プログラムのどの部分も同じFoo
のサイズに依存していないことを証明できる場合にのみ、未使用のメンバー変数(特にパブリック変数)を最適化します。
構造が実際にまったく使用されていない限り、現在のコンパイラーがそのような最適化を実行するとは思いません。一部のコンパイラは、少なくとも未使用のプライベート変数について警告する場合がありますが、通常はパブリック変数については警告しません。
一般に、たとえば「未使用」のメンバー変数がそこにあるなど、要求したものを取得すると想定する必要があります。
あなたの例では両方のメンバーがpublic
であるため、コンパイラーは(特に他の翻訳単位=他の* .cppファイルから、別々にコンパイルされてリンクされている)一部のコードが「未使用」メンバーにアクセスするかどうかを知ることができません。
YSCの答え は、クラスタイプが自動ストレージ期間の変数としてのみ使用され、その変数へのポインターが取得されない、非常に単純な例を示しています。そこで、コンパイラーはすべてのコードをインライン化し、すべての不要なコードを除去できます。
異なる変換単位で定義された関数間のインターフェースがある場合、通常、コンパイラーは何も知りません。インターフェースは通常、いくつかの事前定義されたABI( that など)に準拠しているため、さまざまなオブジェクトファイルを問題なくリンクできます。通常、メンバーが使用されているかどうかに関係なく、ABIは違いを生じません。したがって、そのような場合、2番目のメンバーは物理的にメモリ内になければなりません(後でリンカによって削除されない限り)。
そして、あなたが言語の境界内にいる限り、排除が起こるのを観察することはできません。 sizeof(Foo)
を呼び出すと、2*sizeof(int)
を取得します。 Foo
sの配列を作成する場合、Foo
の2つの連続するオブジェクトの先頭間の距離は常にsizeof(Foo)
バイトです。
タイプは 標準レイアウトタイプ です。つまり、コンパイル時に計算されたオフセットに基づいてメンバーにアクセスすることもできます( offsetof
マクロを参照) 。さらに、std::memcpy
を使用してchar
の配列にコピーすることにより、オブジェクトのバイト単位の表現を検査できます。これらすべてのケースで、2番目のメンバーがそこにあることがわかります。
var2
を除外するこの質問の他の回答によって提供される例は、単一の最適化手法に基づいています。定数の伝播と、その後の構造全体の省略(var2
だけの省略ではありません)。これは単純なケースであり、最適化コンパイラはそれを実装します。
アンマネージC/C++コードの場合の答えは、コンパイラーは一般にvar2
を省略しないことです。私が知る限り、デバッグ情報ではこのようなC/C++構造体変換はサポートされていません。また、構造体がデバッガーの変数としてアクセスできる場合、var2
を省略できません。私の知る限り、現在のC/C++コンパイラはvar2
の省略に従って関数を特殊化できません。そのため、構造体が非インライン関数に渡されるか、インライン関数から返された場合、var2
は省略できません。
JITコンパイラを備えたC#/ Javaなどのマネージ言語の場合、コンパイラは、使用されているかどうか、およびアンマネージコードにエスケープするかどうかを正確に追跡できるため、var2
を安全に回避できる場合があります。管理された言語での構造体の物理的なサイズは、プログラマーに報告されるサイズとは異なる場合があります。
2019年のC/C++コンパイラは、構造体変数全体を省略しない限り、構造体からvar2
を除外できません。構造体からのvar2
の省略の興味深いケースの場合、答えは次のとおりです。
将来の一部のC/C++コンパイラーは、構造体からvar2
を除外できるようになり、コンパイラーを中心に構築されたエコシステムは、コンパイラーによって生成されたプロセス省略情報に適応する必要があります。
コンパイラとその最適化レベルに依存します。
Gccで_-O
_を指定すると、 以下の最適化フラグ がオンになります。
_-fauto-inc-dec
-fbranch-count-reg
-fcombine-stack-adjustments
-fcompare-elim
-fcprop-registers
-fdce
-fdefer-pop
...
_
_-fdce
_は Dead Code Elimination を表します。
__attribute__((used))
を使用して、gccが静的ストレージで未使用の変数を削除しないようにすることができます。
この属性は、静的ストレージのある変数に付加されているため、変数が参照されていないように見えても、変数を出力する必要があります。
C++クラステンプレートの静的データメンバーに適用される場合、この属性は、クラス自体がインスタンス化されている場合にメンバーがインスタンス化されることも意味します。