ショートバージョン:のcalloc()
代わりに常に使用してくださいmalloc()+memset()
。ほとんどの場合、それらは同じです。場合によっては、完全にcalloc()
スキップできるため、作業が少なくなりますmemset()
。他の場合でcalloc()
は、チートしてメモリを割り当てることさえできません!ただし、malloc()+memset()
常にすべての作業を行います。
これを理解するには、メモリシステムの短いツアーが必要です。
思い出のクイックツアー
ここには4つの主要部分があります。プログラム、標準ライブラリ、カーネル、ページテーブルです。あなたはすでにあなたのプログラムを知っているので...
ほとんどのメモリアロケータはmalloc()
、calloc()
小さな割り当て(1バイトから数百KBまで)を取得し、それらをより大きなメモリプールにグループ化するために存在します。たとえば、16バイトを割り当てる場合、malloc()
は最初にそのプールの1つから16バイトを取得しようとし、次にプールが枯渇したときにカーネルにメモリを追加するよう要求します。しかし、以来、あなたはおよそ求めているプログラムは、一度に大量のメモリ割り当てのためにされ、malloc()
そしてcalloc()
ちょうどカーネルから直接、そのメモリを要求します。この動作のしきい値はシステムによって異なりますが、しきい値として1 MiBが使用されているのを見ました。
カーネルは、実際のRAMを各プロセスに割り当て、プロセスが他のプロセスのメモリに干渉しないようにする責任があります。これはメモリ保護と呼ばれ、1990年代からよく見られる汚れであり、1つのプログラムがシステム全体を停止せずにクラッシュする可能性があるのはこのためです。そのため、プログラムがより多くのメモリを必要とする場合、メモリを取得するだけでなく、mmap()
またはのようなシステムコールを使用してカーネルからメモリを要求しsbrk()
ます。カーネルは、ページテーブルを変更して、各プロセスにRAMを割り当てます。
ページテーブルは、メモリアドレスを実際の物理RAMにマップします。プロセスのアドレス(32ビットシステムでは0x00000000〜0xFFFFFFFF)は実メモリではなく、仮想メモリ内のアドレスです。 プロセッサはこれらのアドレスを4 KiBページに分割し、ページテーブルを変更することで各ページを異なる物理RAMに割り当てることができます。カーネルのみがページテーブルを変更できます。
うまくいかない方法
256 MiBの割り当てが機能しない方法を次に示します。
プロセスがcalloc()
256 MiBを呼び出して要求します。
標準ライブラリはmmap()
256 MiBを呼び出して要求します。
カーネルは256 MiBの未使用のRAMを検出し、ページテーブルを変更することでプロセスに割り当てます。
標準ライブラリはでRAMをゼロにし、memset()
から戻りますcalloc()
。
プロセスは最終的に終了し、カーネルはRAMを再利用して、別のプロセスで使用できるようにします。
実際の仕組み
上記のプロセスは機能しますが、この方法では発生しません。主な違いは3つあります。
プロセスがカーネルから新しいメモリを取得するとき、そのメモリはおそらく以前に他のプロセスによって使用されていました。これはセキュリティ上のリスクです。そのメモリにパスワード、暗号化キー、または秘密のサルサレシピがある場合はどうなりますか?機密データの漏洩を防ぐために、カーネルは常にメモリをスクラブしてからプロセスに渡します。メモリをゼロにすることでメモリをスクラブすることもできます。新しいメモリがゼロになる場合は保証することもできるため、mmap()
返される新しいメモリが常にゼロになることが保証されます。
メモリを割り当てるが、すぐにはメモリを使用しないプログラムはたくさんあります。ときどきメモリが割り当てられますが、使用されません。カーネルはこれを知っており、怠惰です。新しいメモリを割り当てるとき、カーネルはページテーブルにまったく触れず、プロセスにRAMを割り当てません。代わりに、プロセス内のアドレス空間を見つけ、そこに何が入るかをメモし、プログラムが実際にRAMを使用する場合にRAMを配置することを約束します。プログラムがこれらのアドレスから読み取りまたは書き込みを試みると、プロセッサはページフォールトをトリガーし、カーネルがそれらのアドレスにRAMを割り当ててプログラムを再開します。メモリを使用しない場合、ページフォールトは発生せず、プログラムは実際にはRAMを取得しません。
一部のプロセスは、メモリを割り当て、変更せずにメモリから読み取ります。つまり、さまざまなプロセス間でメモリ内の多くのページが、から返される初期のゼロで満たされる可能性がありますmmap()
。これらのページはすべて同じであるため、カーネルはこれらのすべての仮想アドレスがゼロで満たされたメモリの単一の共有4 KiBページを指すようにします。そのメモリに書き込もうとすると、プロセッサは別のページフォールトをトリガーし、カーネルがステップインして、他のプログラムと共有されていないゼロの新しいページを提供します。
最終的なプロセスは次のようになります。
プロセスがcalloc()
256 MiBを呼び出して要求します。
標準ライブラリはmmap()
256 MiBを呼び出して要求します。
カーネルは256 MiBの未使用のアドレススペースを見つけ、そのアドレススペースが現在何のために使用されているかをメモして返します。
標準ライブラリは、結果mmap()
が常にゼロで埋められる(または実際にRAMを取得するとすぐに満たされる)ことを認識しているため、メモリに触れないため、ページフォールトが発生せず、RAMがプロセスに提供されることはありません。 。
プロセスは最終的に終了し、RAMは最初から割り当てられていなかったため、RAMを再利用する必要はありません。
を使用memset()
してページをゼロにするmemset()
と、ページフォールトがトリガーされ、RAMが割り当てられ、すでにゼロで埋められていてもゼロになります。これは膨大な量の余分な作業であり、なぜcalloc()
とよりも高速であるかを説明malloc()
しmemset()
ます。もし最後とにかくメモリを使って、calloc()
まだ速くよりmalloc()
とmemset()
その差はそれほどばかげではありません。
これは常に機能するとは限りません
すべてのシステムが仮想メモリをページングしているわけではないため、すべてのシステムがこれらの最適化を使用できるわけではありません。これは、80286などの非常に古いプロセッサや、洗練されたメモリ管理ユニットには小さすぎる組み込みプロセッサに適用されます。
これは、小さい割り当てでも常に機能するとは限りません。割り当てcalloc()
が少ない場合、カーネルに直接アクセスするのではなく、共有プールからメモリを取得します。一般に、共有プールには、で使用および解放された古いメモリからのジャンクデータが格納されてfree()
いるcalloc()
可能性があるため、そのメモリを呼び出してmemset()
、それを消去することができます。一般的な実装では、共有プールのどの部分が初期状態であり、まだゼロで埋められているかを追跡しますが、すべての実装がこれを行うわけではありません。
いくつかの間違った答えを払拭する
オペレーティングシステムによっては、後でメモリをゼロにする必要がある場合に備えて、カーネルが空き時間にメモリをゼロにする場合としない場合があります。Linuxは事前にメモリをゼロにしません。DragonflyBSDも最近、この機能をカーネルから削除しました。ただし、他の一部のカーネルは、事前にメモリをゼロにします。いずれにしても、パフォーマンスの大きな違いを説明するには、ページをゼロに調整するだけでは十分ではありません。
calloc()
機能は、一部の特殊なメモリ揃えバージョンを使用していないmemset()
、それははるかに高速とにかくそれをすることはないだろう。memset()
最新のプロセッサーのほとんどの実装は、次のようなものです。
function memset(dest, c, len)
// one byte at a time, until the dest is aligned...
while (len > 0 && ((unsigned int)dest & 15))
*dest++ = c
len -= 1
// now write big chunks at a time (processor-specific)...
// block size might not be 16, it's just pseudocode
while (len >= 16)
// some optimized vector code goes here
// glibc uses SSE2 when available
dest += 16
len -= 16
// the end is not aligned, so one byte at a time
while (len > 0)
*dest++ = c
len -= 1
ご覧のとおり、memset()
非常に高速であり、メモリの大きなブロックに対しては何も改善されません。
memset()
すでにゼロ化されているメモリをゼロ化するという事実は、メモリが2回ゼロ化されることを意味しますが、これは2倍のパフォーマンスの違いを説明するだけです。ここでのパフォーマンスの違いははるかに大きくなっています(私のシステムでmalloc()+memset()
との間で3桁以上測定しましたcalloc()
)。
パーティートリック
10回ループする代わりに、NULLを返すmalloc()
かcalloc()
NULLを返すまでメモリを割り当てるプログラムを作成します。
追加するとどうなりますmemset()
か?