64ビットプログラムがより多くのスタックメモリを必要とする理由



フォーラムでは、 64ビットバージョンのプログラムがより多くのメモリとスタックを消費することがよく言われます。 この場合、通常、データサイズが2倍大きいという事実を参照します。 ただし、C / C ++のほとんどの型(char、short、int、float)のサイズは64ビットシステムで同じであるため、これは不合理なステートメントです。 もちろん、たとえば、ポインターのサイズは大きくなりましたが、プログラム内のすべてのデータがポインターで構成されているわけではありません。 メモリとスタック消費の増加の理由はより複雑です。 私はこの問題をさらに詳しく研究することにしました。



この記事では、スタックについて説明します。今後、メモリの割り当てとバイナリコードのサイズについて説明する予定です。 また、この記事はC / C ++言語とVisual Studio開発環境に特化したものであることにすぐに注目します。







最近まで、64ビットプログラムのコードは、32ビットコードに比べて2倍の速さでスタックを吸収できると信じていました。 この仮定に基づいて、万が一のために記事でプログラムスタックを2倍にすることをお勧めします。 しかし、今私は不快な事実を発見しました。 スタックの吸収は、2倍以上に大きくなる可能性があります。 私は以前、スタックの増加が最も悲観的なシナリオの2倍であると考えていたため、驚きました。 私の根拠のない希望の理由は、少し後に明らかになります。 関数を呼び出すときに、64ビットプログラムでパラメーターがどのように送信されるかを考えてみましょう。



x86-64アーキテクチャーの 呼び出し規則を開発する際関数を呼び出すためのさまざまなオプションの存在に終止符を打つことにしました。 Win32には 、stdcall、cdecl、fastcall、thiscallなどの多くの呼び出し規則がありました。 Win64では、 「ネイティブ」呼び出し規約 1つだけです。 __cdeclコンパイラーなどの修飾子は無視されます。 合意の数がこのように急激に減少することの高貴さに全員が同意すると思います。



x86-64呼び出し規約は、x86のfastcall規約に似ています。 x64規則では、最初の4つの整数引数(左から右へ)は、この目的のために特別に選択された64ビットレジスタで渡されます。



RCX:最初の整数引数

RDX:2番目の整数引数

R8:3番目の整数引数

R9:4番目の整数引数



残りの整数引数はスタックを介して渡されます。 thisポインターは整数引数と見なされるため、常にRCXレジスターに配置されます。 浮動小数点値が渡されると、最初の4つの値がXMM0〜XMM3レジスタで送信され、後続の値がスタックを介して送信されます。



この情報から、私は以前、多くの場合64ビットプログラムが32ビットと比較してスタックメモリを節約できると結論付けました。 結局、パラメータがレジスタを介して渡される場合、関数コードは短く、引数をメモリ(スタック)に保存する必要はありません。使用されるスタックメモリのサイズを小さくする必要があります。 しかし、これはそうではありません。



引数をレジスターに渡すことはできますが、コンパイラーはスタック上の引数用にスペースを予約し、RSPレジスター(スタックポインター)の値を減らします。 少なくとも、各関数はスタック上に32バイトを予約する必要があります(RCX、RDX、R8、R9レジスタに対応する4つの64ビット値)。 スタック上のこのスペースにより、スタック上の関数に渡されるレジスタの内容を簡単に保存できます。 呼び出される関数は、レジスタを介してスタックに渡された入力パラメーターをダンプする必要はありませんが、必要に応じて、スタック上の場所を予約することでこれが可能になります。 4つ以上の整数パラメーターが渡される場合、対応する追加のスペースをスタック上で予約する必要があります。



例を考えてみましょう。 特定の関数は、2つの整数パラメーターを子関数に渡します。 コンパイラは、引数値をRCXおよびRDXレジスタに入れ、RSPレジスタから32バイトを減算します。 呼び出された関数は、RCXおよびRDXレジスタを介してパラメーターにアクセスできます。 この関数のコードが他の目的でこれらのレジスタを必要とする場合、サイズが32バイトのスタックの予約スペースにその内容をコピーできます。



説明された特徴は、スタックの吸収速度の実質的な増加をもたらす。 関数にパラメータがない場合でも、32バイトはスタックから「噛み付いた」ままになり、その後は使用されません。 私はそのような不経済なメカニズムを使用することのポイントをキャッチしませんでした。 デバッグの統一と単純化については何かが言われていますが、それは何となく曖昧です。



もう1つの瞬間に注目しましょう。 RSPスタックポインターは、次の関数呼び出しの前に16バイト境界で整列する必要があります。 したがって、64ビットコードでパラメーターなしで関数呼び出すときに使用されるスタックの合計サイズは、8(戻りアドレス)+ 8(アライメント)+ 32(引数用に予約)= 48バイトです!



これが実際に何をもたらすかを検討してください。 以下では、実験のために、Visual Studio 2010を使用します。次の形式の再帰関数を作成します。



  void StackUse(size_t *深さ)
 {
   volatile size_t * ptr = 0;

   if(深さ!= NULL)
     ptr =深さ;

   cout << * ptr << endl;

   (* ptr)++;

   StackUse(深さ);

   (* ptr)-;
 }


この関数は少しわかりにくいので、オプティマイザーが「何も」にしないようにします。 ここでの主なことは、関数にポインター型の引数と、ポインター型の1つのローカル変数があることです。 関数が32ビットおよび64ビットバージョンで消費するスタックの数と、1メガバイトスタック(デフォルトサイズ)で再帰的に呼び出すことができる回数を見てみましょう。



リリース32ビット:最後に表示された番号(スタックの深さ)-51331

コンパイラーは、この関数を呼び出すときに20バイトを使用します。



64ビットリリース:最後に表示された番号は21288です

コンパイラーは、この関数を呼び出すときに48バイトを使用します。



したがって、StackUse関数の64ビットバージョンは、32ビットバージョンの2倍以上の食いしん坊です。



データ配置規則を変更すると、吸収されたスタックのサイズにも影響することに注意してください。 関数が引数として構造体をとると仮定します。



  struct S
 {
   char a;
   size_t b;
   char c;
 };

 void StackUse(S s){...} 


64ビットモードで再コンパイルすると、アライメント規則の変更と「b」メンバーのサイズの変更による「S」構造のサイズが12バイトから24バイトに増加します。 構造は値によって関数に渡されます。 したがって、スタック内の構造も2倍のメモリを占有します。



本当にそんなに悪いの? いや 64ビットコンパイラで使用できるレジスタの数が多いことを忘れてはなりません。 実験関数のコードを複雑にしましょう:



  void StackUse(size_t * depth、char a、int b)
 {
   volatile size_t * ptr = 0;

   int c = 1;
   int d = -1;

   for(int i = 0; i <b; i ++)
     for(char j = 0; j <a; j ++)
       for(char k = 0; k <5; k ++)
         if(* depth> 10 && k> 2)
         {
           c + = j * k-i;
           d-=(i-j)* c;
         }

   if(深さ!= NULL)
     ptr =深さ;

   cout << c << "" << d << "" << * ptr << endl;
   (* ptr)++;

   StackUse(深さ、a、b);

   (* ptr)-;
 } 


打ち上げ結果:



リリース32ビット:最後に表示される番号は16060です

コンパイラーは、この関数を呼び出すときに64バイトを使用します。



64ビットリリース:最後に印刷された番号-21310

コンパイラーは、この関数を呼び出すときに48バイトを使用します。



この例では、64ビットコンパイラは追加のレジスタを使用し、より効率的なコードを構築することができたため、使用されるスタックメモリの量を減らすことができました。



結論

  1. 64ビットバージョンのプログラムが32ビットバージョンと比較して使用するスタックメモリを予測することは不可能です。 サイズは、小さく(可能性は低い)、かなり大きくすることができます。
  2. 64ビットプログラムの場合、念のため、予約されたスタックのボリュームを2〜3倍増やす価値があります。 心の安らぎのために3倍良い。 これを行うには、プロジェクト設定にStack Reserve Sizeパラメーター(switch / STACK:reserve)があります。 デフォルトでは、スタックサイズは1メガバイトです。
  3. 64ビットプログラムがより多くのスタックメモリを消費することを心配しないでください。 64ビットシステムには、はるかに多くの物理メモリがあります。 8ギガバイトのメモリを搭載した64ビットシステムの2メガバイトスタックは、2ギガバイトのメモリを搭載した32ビットシステムの1メガバイトスタックよりもメモリの割合が少なくなります。


サイトリンク

  1. レイモンド・チェン。 呼び出し規約の歴史、パート5:amd64。 http://www.viva64.com/go.php?url=325
  2. ケビン・フライ。 x64 ABI対 x86 ABI(別名AMD64およびEM64Tの呼び出し規約)。 http://www.viva64.com/go.php?url=326
  3. MSDN x64ソフトウェアの規則。 http://www.viva64.com/go.php?url=327
  4. ウィキペディア x86呼び出し規約。 http://www.viva64.com/go.php?url=328



All Articles