15

プロジェクトの 1 つでカスタム ヒープ実装を使用しています。これは、次の 2 つの主要部分で構成されています。

  1. 固定サイズのブロック ヒープ。つまり、特定のサイズのブロックのみを割り当てるヒープです。より大きなメモリ ブロック (仮想メモリ ページまたは別のヒープから) を割り当て、アトミックな割り当て単位に分割します。

    割り当て/解放を (O(1) で) 高速に実行し、外部ヒープによって課されることを考慮せずに、メモリ使用量のオーバーヘッドはありません。

  2. グローバル汎用ヒープ。上記の (固定サイズの) ヒープのバケットで構成されます。要求された割り当てサイズを WRT して、適切なバケットを選択し、それを介して割り当てを実行します。

    アプリケーション全体が (かなり) マルチスレッド化されているため、グローバル ヒープは操作中に適切なバケットをロックします。

    : 従来のヒープとは対照的に、このヒープは、割り当てだけでなく、解放にも割り当てサイズを必要とします。これにより、検索や余分なメモリ オーバーヘッド (割り当てられたブロックの前にブロック サイズを保存するなど) なしで、適​​切なバケットを特定できます。やや不便ですが、私の場合はこれで問題ありません。さらに、「バケット構成」はコンパイル時に認識されるため (C++ テンプレート voodoo を介して実装されます)、適切なバケットはコンパイル時に決定されます。

これまでのところ、すべてがうまく機能しています。

最近、私はヒープ操作を頻繁に実行するアルゴリズムに取り組みましたが、当然ヒープのパフォーマンスに大きく影響されます。プロファイリングにより、そのパフォーマンスがロックによってかなり影響を受けることが明らかになりました。つまり、ヒープ自体は非常に高速に動作します (通常の割り当てには、メモリ参照解除命令がいくつか含まれます)。ただし、アプリケーション全体がマルチスレッドであるため、適切なバケットは、インターロック命令に依存するクリティカル セクションによって保護されます。重い。

私は、クリティカル セクションによって保護されていない独自の専用ヒープをこのアルゴリズムに与えることで、これを修正しました。しかし、これはコード レベルでいくつかの問題/制限を課します。たとえば、ヒープが必要な場合はスタック内の深いところにコンテキスト情報を渡す必要があります。これを回避するために TLS を使用することもできますが、これは私の特定のケースで再入に問題を引き起こす可能性があります。

これは私を不思議に思います:シングルスレッドの使用のためにヒープを最適化するための既知の手法はありますか?

編集:

Google の tcmalloc をチェックすることを提案してくれた @Voo に感謝します。

多かれ少なかれ(少なくとも小さなオブジェクトの場合)行ったことと同様に機能するようです。さらに、スレッドごとのキャッシュを維持することで、私が抱えている問題を正確に解決します。

私もこの方向で考えましたが、スレッドごとのヒープを維持することを考えました。次に、別のスレッドに属するヒープから割り当てられたメモリ ブロックを解放するのは、やや注意が必要です。ロックされたキューのようなものにメモリ ブロックを挿入し、他のスレッドに通知して、保留中の割り当てを非同期的に解放する必要があります。非同期の割り当て解除は問題を引き起こす可能性があります。そのスレッドが何らかの理由 (たとえば、積極的な計算を実行するなど) でビジー状態の場合、メモリの割り当て解除は実際には発生しません。さらに、マルチスレッドのシナリオでは、割り当て解除のコストが大幅に高くなります。

OTOH キャッシングを使用したアイデアは、はるかに単純で効率的です。私はそれを解決しようとします。

どうもありがとう。

PS:

確かに、Google の tcmalloc は素晴らしいです。私が行ったものとほぼ同じように実装されていると思います(少なくとも固定サイズの部分)。

しかし、詳しく言うと、私のヒープが優れている点が 1 つあります。ドキュメントによると、tcmalloc は (漸近的に) 約 1% のオーバーヘッドを課しますが、私のオーバーヘッドは 0.0061% です。正確には4/64Kです。

:)

4

2 に答える 2

10

1 つの考えは、スレッドごとにメモリ アロケータを維持することです。グローバル メモリ プールから各アロケータにかなり分厚いメモリ ブロックを事前に割り当てます。チャンキー ブロックを隣接するメモリ アドレスから割り当てるようにアルゴリズムを設計します (詳細は後述)。

特定のスレッドのアロケータのメモリが不足すると、グローバル メモリ プールからより多くのメモリが要求されます。この操作にはロックが必要ですが、現在のケースよりもはるかに少ない頻度で実行する必要があります。特定のスレッドのアロケータが最後のバイトを解放すると、そのアロケータのすべてのメモリがグローバル メモリ プールに返されます (スレッドが終了したと仮定します)。

このアプローチは、現在のアプローチよりも早くメモリを使い果たす傾向があります (メモリは、それを必要としない 1 つのスレッド用に予約できます)。それがどの程度問題になるかは、アプリのスレッドの作成 / 有効期間 / 破棄のプロファイルによって異なります。追加の複雑さを犠牲にして、たとえば、特定のスレッドのメモリアロケーターがメモリ不足であり、グローバルプールが使い果たされているというシグナルを導入することで、他のメモリアロケーターが一部のメモリを解放することで応答できるようにすることで、それを軽減できます。

このスキームの利点は、特定のスレッドのメモリが連続したアドレス空間に割り当てられる傾向があるため、偽共有を排除する傾向があることです。

余談ですが、まだ読んでいない場合は、独自のメモリ管理を実装しているすべての人に、 IBM の Inside Memory Management の記事をお勧めします。

アップデート

目標がマルチスレッド環境に最適化された非常に高速なメモリ割り当てを持つことである場合 (自分で行う方法を学ぶのではなく)、別のメモリ アロケータを調べてください。目標が学習である場合は、おそらくソース コードをチェックしてください。

于 2012-05-21T16:05:00.697 に答える
0

スラブ アロケータと vmem に関する Jeff Bonwick の古典的な論文を読むことをお勧めします。元のスラブ アロケーターは、あなたがやっていることのように聞こえます。マルチスレッドにあまり適していませんが、いくつかのアイデアが得られるかもしれません。

スラブ アロケーター: オブジェクト キャッシュ カーネル メモリ アロケーター

その後、彼は VMEM で概念を拡張しました。これは、複数の CPU 環境で非常に優れた動作をするため、間違いなくいくつかのアイデアを提供します。

Magazines と Vmem: Slab Allocator を多数の CPU と任意のリソースに拡張する

于 2012-05-22T07:35:46.963 に答える