私は仮想メモリの仕組みを比較的よく知っています。すべてのプロセスメモリはページに分割され、仮想メモリのすべてのページが実メモリ内のページまたはスワップファイル内のページにマップされます。または、物理ページがまだ割り当てられていない新しいページになる場合があります。 OSは、アプリケーションがmalloc
でメモリを要求するときではなく、アプリケーションが実際に割り当てられたメモリからすべてのページにアクセスするときにのみ、新しいページをオンデマンドで実際のメモリにマップします。しかし、私はまだ質問があります。
Linux perf
ツールを使用してアプリをプロファイリングしているときに、これに気付きました。
カーネル関数にかかる時間の約20%があります:clear_page_orig
、__do_page_fault
およびget_page_from_free_list
。これは、このタスクで予想したよりもはるかに多く、いくつかの調査を行いました。
いくつかの小さな例から始めましょう:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define SIZE 1 * 1024 * 1024
int main(int argc, char *argv[]) {
int i;
int sum = 0;
int *p = (int *) malloc(SIZE);
for (i = 0; i < 10000; i ++) {
memset(p, 0, SIZE);
sum += p[512];
}
free(p);
printf("sum %d\n", sum);
return 0;
}
memset
はメモリにバインドされた処理の一部であると想定します。この場合、メモリの小さなチャンクを1回割り当て、それを何度も再利用します。このプログラムを次のように実行します。
$ gcc -O1 ./mem.c && time ./a.out
-O1
clang
と-O2
はループを完全に排除し、瞬時に値を計算します。
結果は次のとおりです。ユーザー:0.520秒、システム:0.008秒。 perf
によると、この時間の99%はmemset
のlibc
にあります。したがって、この場合、書き込みパフォーマンスは約20ギガバイト/秒であり、これは私のメモリの理論上のパフォーマンス12.5 Gb /秒を超えています。これは、L3 CPUキャッシュが原因であるようです。
テストを変更して、ループ内のメモリの割り当てを開始します(コードの同じ部分を繰り返すことはしません)。
#define SIZE 1 * 1024 * 1024
for (i = 0; i < 10000; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
結果はまったく同じです。 free
は実際にはOSのメモリを解放するのではなく、プロセス内の空きリストに追加するだけだと思います。そして、次の反復でのmalloc
は、まったく同じメモリブロックを取得します。そのため、目立った違いはありません。
SIZEを1メガバイトから増やしてみましょう。実行時間は少しずつ増加し、10メガバイト近くで飽和します(10メガバイトと20メガバイトの間で違いはありません)。
#define SIZE 10 * 1024 * 1024
for (i = 0; i < 1000; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
時間の表示:ユーザー:1.184秒、システム:0.004秒。 perf
は依然として99%の時間はmemset
にあると報告していますが、スループットは約8.3 Gb /秒です。その時点で、多かれ少なかれ何が起こっているのか理解しています。
メモリブロックサイズを増やし続けると、ある時点(私にとっては35 Mb)で実行時間が劇的に増加します。ユーザー:0.724秒、システム:3.300秒。
#define SIZE 40 * 1024 * 1024
for (i = 0; i < 250; i ++) {
int *p = (int *) malloc(SIZE);
memset(p, 0, SIZE);
free(p);
}
perf
によると、memset
は時間の18%しか消費しません。
明らかに、メモリはOSから割り当てられ、各ステップで解放されます。前述のとおり、OSは割り当てられた各ページを使用前にクリアする必要があります。つまり、27.3%のclear_page_orig
は異常に見えません:clear memは4秒* 0.273≈1.1秒です。3番目の例と同じです。 memset
は17.9%かかりました。これは≈700ミリ秒につながります。これはclear_page_orig
(最初と2番目の例)。
理解できないこと—最後のケースがメモリのmemset
+ L3キャッシュのmemset
の2倍遅いのはなぜですか?何かできますか?
結果は、ネイティブMac OS、Vmwareの下のUbuntu、およびAmazon c4.largeインスタンスで(わずかな違いはありますが)再現可能です。
また、2つのレベルで最適化の余地があると思います。
ここで何が起こっているかは、いくつかの異なるシステムに関係しているので少し複雑ですが、コンテキストスイッチのコストとは関係ありません。プログラムでシステムコールをほとんど行わない( strace を使用してこれを確認する)。
最初に、malloc
実装の一般的な動作方法に関するいくつかの基本原則を理解することが重要です。
malloc
実装は、初期化中にsbrk
またはmmap
を呼び出すことにより、OSから大量のメモリを取得します。取得されるメモリの量は、一部のmalloc
実装で調整できます。メモリが取得されると、通常、メモリはさまざまなサイズのクラスに分割され、データ構造に配置されるため、プログラムがmalloc(123)
などを使用してメモリを要求すると、malloc
実装はこれらの要件に一致するメモリの一部。free
を呼び出すと、メモリは空きリストに戻され、その後のmalloc
の呼び出しで再利用できます。一部のmalloc
実装では、これがどのように機能するかを正確に調整できます。malloc
実装は、大量のメモリの呼び出しをmmap
システムコールに直接渡すだけで、一度にメモリの「ページ」を割り当てます。ほとんどのシステムでは、1ページのメモリは4096バイトです。mmap
またはsbrk
を介してメモリを要求したプロセスにページを渡す前に、メモリのページをクリアしようとします。これが、perf出力にclear_page_orig
の呼び出しが表示される理由です。この関数は、メモリのページに0を書き込もうとしています。現在、これらの原則は、多くの名前を持っていますが、一般に「デマンドページング」と呼ばれている別のアイデアと交差しています。 「デマンドページング」が意味することは、ユーザープログラムがOSからメモリのチャンクを要求すると(たとえば、mmap
を呼び出すことにより)、メモリはプロセスの仮想アドレス空間に割り当てられますが、物理的なRAMまだそのメモリをサポートしています。
デマンドページングプロセスの概要は次のとおりです。
mmap
と呼ばれるプログラム。最後のケースでパフォーマンスの低下が見られる最も可能性の高い理由は次のとおりです。
memset
はO(n)であり、書き込む必要のあるメモリが多いほど、時間がかかります。アプリケーションがパフォーマンスに非常に敏感な場合は、代わりにmmap
を直接呼び出し、次のことができます。
MAP_POPULATE
フラグを渡すと、すべてのページフォールトが事前に発生し、すべての物理メモリがマップされます。アクセスすると、ページフォールトのコストはかかりません。MAP_UNINITIALIZED
フラグを渡して、プロセスに配布する前にメモリのページをゼロにしないようにします。このフラグの使用はセキュリティ上の問題であり、このオプションを使用することの影響を完全に理解していない限り使用しないでください。機密情報を保存するために他の無関係なプロセスによって使用されたメモリのページがプロセスに発行される可能性があります。また、このオプションを許可するにはカーネルをコンパイルする必要があることにも注意してください。ほとんどのカーネル(AWS Linuxカーネルなど)には、このオプションがデフォルトで有効になっていません。このオプションは、ほぼ間違いなく使用しないでください。このレベルの最適化はほとんどの場合間違いであることに注意してください。ほとんどのアプリケーションには、ページフォールトコストの最適化を伴わない最適化のためのはるかに低いぶら下がり果物があります。実際のアプリケーションでは、次のことをお勧めします。
memset
の使用を避けます。ほとんどの場合、同じプロセスで再利用する前にメモリをゼロにする必要はありません。MAP_POPULATE
フラグを使用します。質問がある場合はコメントを残してください。必要に応じて、この投稿を編集して少し拡張します。
確かではありませんが、ユーザーモードからカーネルへのコンテキストの切り替えのコストに賭けても構わないと思います。 memset
にもかなりの時間がかかります-O(n)になることを覚えておいてください。
更新
Freeは実際にはOSのメモリを解放するのではなく、プロセス内の空きリストに追加するだけだと思います。そして、次の反復でのmallocは、まったく同じメモリブロックを取得します。そのため、目立った違いはありません。
これは原則として正しいです。従来のmalloc
実装は、単一リンクリストにメモリを割り当てます。 free
は、割り当てが使用されなくなったことを示すフラグを設定するだけです。時間が経つにつれ、malloc
は、十分な空きブロックを最初に見つけたときに再割り当てします。これは十分に機能しますが、断片化につながる可能性があります。
現在、より洗練された実装がいくつかあります。 このWikipediaの記事 を参照してください。