web-dev-qa-db-ja.com

Cでの静的メモリ割り当てと動的メモリ割り当てのコスト

CLinuxにあるオブジェクト/アイテムの正確な数がわかっている場合、メモリ割り当ての推奨される方法_static vs dynamic_がパフォーマンス(実行時間など)に適していることを知りたいと思います。少数のオブジェクト(少量のメモリ)および多数のオブジェクト(大量のメモリ)のコスト。

_e.g., type A[N]_ vs type *A = malloc(sizeof(type) * N)

私にお知らせください。ありがとうございました。

注:これをベンチマークして、おそらく答えを知ることができます。しかし、これら2つの割り当て方法のパフォーマンスの違いを説明する概念を知りたいと思います。

18
samarasa

静的割り当てははるかに高速になります。静的割り当ては、グローバルスコープおよびスタックで発生する可能性があります。

グローバルスコープでは、静的に割り当てられたメモリがバイナリイメージに組み込まれます。これは必要なメモリの合計サイズであり、実行中のバイナリのどこに配置されるかはコンパイル時に計算されます。次に、プログラムがロードされると、オペレーティングシステムローダーはすべてのグローバル静的配列に十分なメモリを割り当てます。私はそれがすべての割り当てに対して一定の時間で起こるとかなり確信しています。 (たとえば、割り当てを増やしても時間はかかりません)

ローカルスコープでは、静的割り当てはスタックに割り当てられます。これには、スタックに固定数のバイトを予約するだけで、割り当てごとに一定の時間で発生します。スタックスペースは非常に限られています。

動的メモリはヒープから割り当てる必要があり、最良の場合でも、ほとんどの割り当てには、n log n時間など、割り当てごとに線形以上にスケーリングする時間がかかります。

また、実際には、動的割り当ては静的割り当てよりも何倍も遅くなります。

@update:以下のコメントで指摘されているように、スタック割り当ては技術的に静的な割り当てではありません(ただし、OPの質問で使用される構文形式の割り当てです)。

また、「割り当て時間」について言えば、メモリを管理するための合計時間(allocとfree)を考慮しています。

一部の動的アロケーターでは、割り当て時間は解放時間よりも高速です。

一部の高速アロケータがメモリ効率を割り当て速度と交換することも事実です。これらの場合、正確なサイズのブロックを割り当てている間、静的割り当てとスタック割り当てはそれぞれ時間と一定時間ではないという点で、静的は依然として「優れています」。

動的アロケーターは、大幅なメモリ効率とのトレードオフを高速化します(たとえば、バディアロケーターは、33kアロケーターが64kを使用するように、2つのサイズのブロックの次の累乗に切り上げます)

15
Rafael Baptista

真の静的割り当て(グローバル、およびstaticとマークされたローカル変数)はセクションに接着され、ロード時にセクションの残りの部分と一緒にロードされます(execvemmapを使用します。それらは基本的に無料で、プログラムのロードコストですでにカバーされています。

静的に既知のサイズの自動配列は、他のローカル変数と一緒に接着し、スタックポインターを調整することで割り当てることができます。これは基本的に、1つの整数の減算(スタックが下に大きくなる)、または最近のプロセッサでは1 nsに近いものです。

可変長配列は他の変数に接着できないため、それぞれ約1 nsのコストがかかります。

シングルスレッドプロセスでさまざまなサイズのmallocsを測定しようとしましたが、これが得られました。これは、malloc+freeペアのコストは、割り当てのスタック変数の約50-100倍です<16MiB。その後、それは上向きに急上昇します(32MiB64MiBは両方とも割り当て<= 16MiBの約100倍の費用がかかります):

Malloc times

ただし、これらはポインタを取得するためのコストにすぎません。実際のアクセスはページフォールトを引き起こし、それによってメモリブロックのコストをさらに増加させる可能性があります。ページフォールトは、スタック割り当てでは非常にまれですが、動的に割り当てられたメモリへの初回アクセスでは発生する可能性があります。

さらに、多くの小さな動的割り当ては、メモリが断片化されるため、キャッシュにかなりの負担をかけます。 (独自のメモリプールまたはglibcの一部として利用可能なobstack機能のいずれかを使用して、この断片化を減らすことができます。)

8
PSkocik

グローバルメモリは通常、プログラム(または共有モジュール/ dll)がロードされ、初期値が事前入力されたときに割り当てられます。これには通常、独自のメモリセクションがあります。

スタックは、新しいスレッドを作成するときにカーネルによって割り当てられるメモリのブロック(一連のメモリページ)です。スタックへのスタックメモリの割り当ては、通常、スタックポインタをデクリメントすることによって行われます(1つのマシン命令-(通常は完全に降順のスタック-新しい割り当てのアドレスは低くなります)。スタックオブジェクトが削除されると、スタックポインタがインクリメントされます)。したがって、スタックには厳密な最初から最後までのセマンティクスがあります。スタックも固定サイズであるため、不足するとスタックオーバーフローが発生します。

Malloc(C)またはnew(C++)を使用すると、メモリはヒープ/フリーストアに割り当てられます。これは任意の数のメモリブロックです。すでに割り当てられているよりも多くのメモリが必要な場合、mallocはカーネルに移動して、システムからより多くのメモリを要求します。通常、mallocは、システムからすでに取得されている解放されたメモリを使用します。

Mallocの呼び出しは遅いと想定する必要があり、パフォーマンスが重要なルーチンやリアルタイムのルーチンでは避ける必要があります。これには2つの理由があります。

  1. ヒープは、任意のサイズのメモリを任意の順序で割り当てて解放する必要があります。適切なサイズのブロックが見つかるまで、解放されたブロックのリストをウォークするために使用されていた古いアルゴリズム。メモリが高度に断片化される可能性があるため、これは潜在的に遅くなる可能性があります。ヒープが複数のスレッドで使用されている場合は、何らかのロックを提供する必要があります。この状況を改善するために多くの研究が行われ、jemallocとtcmallocの最新のヒープは物事を改善します。ただし、これを当てにしないでください。
  2. ヒープアロケータによってすでに管理されているものから必要なメモリを割り当てることができない場合、ヒープアロケータはカーネルに追加のメモリを要求する必要があります。 (UNIXでは、これはmmapまたはsbrkシステムコールで行われます)。カーネルはいくつかの未割り当てのメモリページを見つける必要があり、これらのページをプロセスのメモリ空間にマップする必要があります)。どんな形式のキャッシュもウィンドウの外に出ます)。したがって、これは非常に遅いと予想されます(コンピューターの場合)。

したがって、パフォーマンスが重要な処理を行う必要がある場合は、すべてのメモリを事前に割り当ててから、すでに割り当てられているメモリを再利用します。常にmallocへの呼び出しを想定すると、無料は遅くなります。

6
doron

確保する必要のあるスペースの量を正確に知っている場合および、主な関心事はメモリ管理に関するパフォーマンスですおよびコードを再入可能にする必要がない場合は、静的ストレージ期間でオブジェクトを割り当てる必要があります。 )、ファイルスコープで宣言するか、staticストレージクラス指定子を使用します。

int data[SOME_NUMBER];

void foo( /* some list of parameters here */ )
{
  static int some_more_data[SOME_OTHER_NUMBER];
  ...
}

datasome_more_dataはどちらもプログラムの存続期間中に存在しますが、some_more_datafoo関数内でのみ表示されます1

実際には、静的な保存期間を持つアイテムには、バイナリイメージ自体にスペースが確保されているため、プログラムがロードされるとすぐにメモリが使用可能になります。このmayは、地域性に関する限り、利点があります。データとコードが同じページにある場合は、交換する必要はありません。もちろん、それはコードに依存します。

明らかな欠点は、実行可能ファイルのフットプリントが大きくなることです。もう1つの欠点は、コードが 再入可能 ではないことです。 fooのすべての呼び出しは、some_more_dataの読み取りまたは書き込み時に、同じメモリブロックで機能します。コードの機能に応じて、それは大したことではないかもしれません。

コードを再入可能にする必要がある場合、静的ストレージ期間のオブジェクトを使用することはできません。データのブロックが比較的小さい場合は、自動保存期間のあるオブジェクト(つまり、通常のローカル変数)を使用します。

void foo( /* some list of parameters here */ )
{
  int some_more_data[SOME_SMALLISH_NUMBER];
  ...
}

この場合、some_more_datafoo関数の存続期間中のみ存在します。 fooを呼び出すたびに、別のsome_more_dataオブジェクトが自動的に割り当てられます。メモリを確保するためのオーバーヘッドは、そもそも関数を呼び出すオーバーヘッドの一部です(少なくとも私の経験では)。ローカル変数を使用しているかどうかに関係なく、スタックポインターは引き続き調整されるため、ローカル変数を使用しても速度が低下することはありません。主な問題は、自動オブジェクトに使用できるメモリが比較的小さいことです。特定のサイズを超えるオブジェクトの場合、このアプローチは単に機能しません。

コードを再入可能にする必要がありメモリの大きなブロックを割り当てる必要がある場合は、動的メモリ管理(malloc/calloc/reallocおよびfree)。コードの設計方法に応じて、パフォーマンスの問題の一部を最小限に抑えることができます。


1.可視性ルールは、ソースからマシンコードへの変換中に適用されます。実行時には実際には適用されません。
2
John Bode