4

C++ でグローバル配列または静的配列を定義する場合、そのメモリはプログラムの開始時にすぐに予約されるのではなく、配列に書き込むと一度だけ予約されます。私が驚いたのは、配列のごく一部に書き込むだけでは、メモリ全体を確保できないことです。グローバル配列にまばらに書き込む次の小さな例を考えてみましょう。

#include <cstdio>
#include <cstdlib>

#define MAX_SIZE 250000000
double global[MAX_SIZE];

int main(int argc, char** argv) {
   if(argc<2) {
      printf("usage: %s <step size>\n", argv[0]);
      exit(EXIT_FAILURE);
   }
   size_t   step_size=atoi(argv[1]);

   for(size_t i=0; i<MAX_SIZE; i+=step_size) {
      global[i]=(double) i;
   }

   printf("finished\n"); getchar();
   return EXIT_SUCCESS;
}

さまざまなステップ サイズに対してこれを実行し、たとえば次のような top の出力を確認します。

./a.out 1000000
./a.out 100000
./a.out 10000
./a.out 1000
./a.out 100

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
15718 user      20   0 1918m 1868  728 S    0  0.0   0:00.00 a.out
15748 user      20   0 1918m  10m  728 S    0  0.1   0:00.00 a.out
15749 user      20   0 1918m  98m  728 S    1  0.8   0:00.04 a.out
15750 user      20   0 1918m 977m  728 S    0  8.1   0:00.39 a.out
15751 user      20   0 1918m 1.9g  728 S   23 15.9   0:00.80 a.out

RES 列は、メモリが小さなブロックでのみ予約されていることを示しています。これは、配列が物理メモリ内で連続している可能性が低いことも意味します。物事のより低いレベルについてより多くの洞察を得た人はいますか?

これには、RES の合計が以下である限り、すべての VIRT の合計が物理メモリを超える多くのプログラムを簡単に実行できるというマイナスの副作用もあります。ただし、それらがすべてグローバル配列に書き込まれるとすぐに、システムは物理メモリを使い果たし、プログラムに sigkill または何かが送信されます。

理想的には、最初にグローバル変数と静的変数のメモリを予約するようにコンパイラに指示したいと思います。可能?

編集

@Magnus: 行は実際には正しい順序になっています。:) たとえば、最初の行./a.out 1000000は、配列の 100 万番目のエントリごとに書き込みを行っていることを意味するため、合計で 250 しかありません。これは、わずか 1868k の RES に相当します。最後の例./a.out 100では、100 エントリごとに書き込まれ、合計メモリも RES=VIRT=1.9g に物理的に割り当てられます。数値から、エントリが配列に書き込まれるたびに、完全な 4k ブロックのようなものが物理メモリに予約されているように見えます。

@Nawaz:配列は仮想アドレス空間で連続していますが、私が理解しているように、OSは怠惰で、実際に必要なときにのみ物理メモリを予約する可能性があります。これは一度に配列全体ではなく小さなブロックで行われるため、物理メモリ内で連続していることをどのように保証できますか?

@Nemo: ありがとうございます。実際にa.out、最初に一時停止してから配列に書き込む複数のインスタンスを呼び出したときに、oom-killerメッセージが表示されました/var/log/messages。実際、あなたのコマンドにより、最初からインスタンスがsysctrl多すぎるのを防ぐことができます。a.outありがとう!

Jun  1 17:49:16 localhost kernel: [32590.293421] a.out invoked oom-killer: gfp_mask=0x280da, order=0, oomkilladj=0
Jun  1 17:49:18 localhost kernel: [32592.110033] kded4 invoked oom-killer: gfp_mask=0x201da, order=0, oomkilladj=0
Jun  1 17:49:20 localhost kernel: [32594.718757] firefox invoked oom-killer: gfp_mask=0x201da, order=0, oomkilladj=0

最後の 2 行は少し気になります。:)

@doron: ありがとう、すばらしい説明です。申し訳ありませんが、賛成/選択できません。

4

4 に答える 4

6

コミットされている仮想メモリ ページを見ています。OS は通常、コードによって明示的に書き込まれたり読み取られたりした場合にのみ、ページをコミットします。これは、配列が連続していることを保証する C++ とは関係ありません。起動時にすべてのプロセスのページを OS にコミットさせる方法を尋ねている場合は、OS 固有のものを使用する必要があります (存在する場合)。

于 2011-06-01T18:09:32.307 に答える
2

ここでは 2 つのことが行われています。仮想メモリと物理メモリ。

プログラムの命令がプログラムの実行を開始する前に割り当てられるのと同じように、静的データ用の仮想メモリ。これは、プログラムのアドレスが常に定義されていることを意味します。

オペレーティング システムは怠惰かもしれませんが、静的データとプログラム命令の両方を物理メモリ RAM にロードする場合には注意が必要です。これが機能する方法は次のとおりです。

  • プロセス ローダーは、静的データ用にプロセス仮想メモリを割り当てましたが、データを RAM にロードしません。
  • これらのアドレスにアクセスしようとすると、プロセッサ例外がトリガーされ、カーネル モードに入ります。
  • カーネルはデータを RAM にロードし、RAM をプロセスの仮想アドレス空間にリンクします。
  • カーネルは、プロセッサ例外が発生した正確なポイントにユーザー モードに戻ります。
  • RAM がプロセスの仮想アドレス空間にリンクされるようになったため、プログラムは何事もなかったかのように実行を続けます。

これは、実行中のプロセスがまったく検出できないという理由だけで、オペレーティング システムが実行できるほんのわずかな操作です。もちろん、メモリが不足している場合を除きます。

于 2011-06-01T20:20:27.377 に答える
1

これは、使用メモリが使用可能な仮想メモリを超えると、「OOM キラー」が起動してプロセスの強制終了を開始する Linux システムのように聞こえます。/var/log/messages で「oom」を grep して確認します。

その場合、この設定:

sysctl -w vm.overcommit_memory=2

...カーネルがプロセスに利用可能な VM を超える割り当てを許可しないようにします。

于 2011-06-01T19:07:57.137 に答える
1

あなたが投稿した表が実質的なものを証明しているとは思いません。

静的ストレージの配列に関する限り、プログラムの開始前に割り当てられます。これは、プログラムがmain()関数に入る前に、ランタイムがメモリをグローバル配列に割り当て、プログラムの期間中持続することを意味します。 :

§3.7.1/1

動的保存期間もローカルでもないすべてのオブジェクトには、静的保存期間があります。これらのオブジェクトのストレージは、プログラム (3.6.2、3.6.3) の期間中持続するものとします。

また、グローバルであろうとローカルであろうと、配列には常に連続したメモリがあります。

于 2011-06-01T18:10:35.747 に答える