Cassandraに似たデータベースサーバーを開発しています。
開発はCで始まりましたが、クラスがなければ非常に複雑になりました。
現在、私はすべてをC++ 11に移植しましたが、まだ「モダン」C++を学習しており、多くのことについて疑問があります。
データベースはキー/値のペアで動作します。すべてのペアにはさらに多くの情報があります。いつが作成され、いつ期限切れになるか(期限切れでない場合は0)各ペアは不変です。
キーはC文字列、値はvoid *ですが、少なくとも当面は値をC文字列として操作しています。
抽象IList
クラスがあります。 3つのクラスから継承
VectorList
-C動的配列-std :: vectorに似ていますが、realloc
を使用しますLinkList
-チェックとパフォーマンス比較のために作成SkipList
-最終的に使用されるクラス。将来的には、Red Black
ツリーも使用する可能性があります。
各IList
には、キーでソートされた、ゼロ以上のポインターのペアへのポインターが含まれます。
IList
が長くなりすぎた場合は、ディスク上の特別なファイルに保存できます。この特殊ファイルは、read only list
の一種です。
キーを検索する必要がある場合は、
IList
が検索されます(SkipList
、SkipList
またはLinkList
)。IList
の実装に疑いはありません。
現在私を困惑させているのは以下の通りです:
ペアは異なるサイズで、new()
によって割り当てられ、std::shared_ptr
がポイントされています。
class Pair{
public:
// several methods...
private:
struct Blob;
std::shared_ptr<const Blob> _blob;
};
struct Pair::Blob{
uint64_t created;
uint32_t expires;
uint32_t vallen;
uint16_t keylen;
uint8_t checksum;
char buffer[2];
};
「バッファ」メンバー変数は、サイズが異なる変数です。キーと値を格納します。
例えば。 keyが10文字で、valueがさらに10バイトの場合、オブジェクト全体はsizeof(Pair::Blob) + 20
になります(2つのヌル終了バイトのため、バッファの初期サイズは2です)
これと同じレイアウトがディスクでも使用されているので、次のようなことができます。
// get the blob
Pair::Blob *blob = (Pair::Blob *) & mmaped_array[pos];
// create the pair, true makes std::shared_ptr not to delete the memory,
// since it does not own it.
Pair p = Pair(blob, true);
// however if I want the Pair to own the memory,
// I can copy it, but this is slower operation.
Pair p2 = Pair(blob);
ただし、この異なるサイズは、C++コードの多くの場所で問題になります。
たとえば、std::make_shared()
は使用できません。これは私にとって重要です。1Mペアの場合、2M割り当てになるからです。
反対側から、動的配列(たとえば、新しいchar [123])に「バッファリング」すると、mmapの「トリック」が失われます。キーを確認する場合は、2つの逆参照を行い、単一のポインタを追加します。 -クラスに8バイト。
また、すべてのメンバーをPair::Blob
からPair
に「プル」しようとしたため、Pair::Blob
は単なるバッファーになりましたが、テストしたところ、おそらくオブジェクトデータをコピーしたため、非常に遅くなりました。
私が考えているもう1つの変更は、Pair
クラスを削除してstd::shared_ptr
に置き換え、すべてのメソッドをPair::Blob
に「プッシュ」して戻すことですが、これは変数サイズPair::Blob
クラスでは役立ちません。
完全なソースコードは次のとおりです。
https://github.com/nmmmnu/HM
私がお勧めするアプローチは、Key-Valueストアのインターフェースに焦点を当てることで、可能な限りクリーンで可能な限り制限のないものにすることです。つまり、呼び出し元に最大限の自由を与えるだけでなく、選択の自由にも最大限の自由を与える必要があります。それを実装する方法。
次に、パフォーマンスをまったく気にすることなく、可能な限り最小限の実装を提供することをお勧めします。私には、unordered_map
が最初の選択であるように思われます。または、何らかの種類のキーの順序をインターフェースで公開する必要がある場合は、おそらくmap
のようです。
したがって、最初にそれをクリーンかつ最小限に機能させる。次に、実際のアプリケーションで使用できるようにします。そうすることで、インターフェースで対処する必要のある問題が見つかります。次に、それらに対処してください。ほとんどの可能性は、インターフェイスを変更した結果、実装の大部分を書き換える必要があるため、実装の最初の反復に費やす時間が、実行に必要な最小限の時間を超えている場合です。かろうじて作業は時間の無駄です。
次に、それをプロファイリングし、インターフェイスを変更せずに、実装で改善する必要があるものを確認します。または、プロファイルを作成する前に、実装を改善する方法について独自のアイデアを持っている場合もあります。それは結構ですが、それはまだ早い段階でこれらのアイデアに取り組む理由はありません。
あなたはあなたがmap
よりもうまくやることを望んでいると言います;それについて言えることは2つあります。
a)あなたはおそらくしません。
b)すべてのコストで時期尚早の最適化を避けます。
実装に関しては、メモリの割り当てが原因であると予想される問題を回避するために設計を構造化する方法に関心があるように思われるため、主な問題はメモリの割り当てにあるようです。 C++でのメモリ割り当ての問題に対処する最良の方法は、適切なメモリ割り当て管理を実装することであり、それらの周りにデザインをねじったり曲げたりすることではありません。 JavaやC#などの言語とは対照的に、独自のメモリ割り当て管理を実行できるC++を使用していることを幸運であると考える必要があります。ランタイムが提供する必要があります。
C++でのメモリ管理にはさまざまな方法があり、new
演算子をオーバーロードする機能が役立つ場合があります。プロジェクトの単純化したメモリアロケータは、巨大なバイト配列を事前に割り当て、ヒープとして使用します。 (byte* heap
。)firstFreeByte
インデックスはゼロに初期化され、ヒープ内の最初の空きバイトを示します。 N
バイトのリクエストが届くと、アドレスheap + firstFreeByte
を返し、N
をfirstFreeByte
に追加します。したがって、メモリ割り当ては非常に高速で効率的になるため、実質的に問題はありません。
もちろん、すべてのメモリを事前に割り当てることは良い考えではないかもしれません。そのため、ヒープをオンデマンドで割り当てられるバンクに分割し、任意の瞬間に最新のバンクからの割り当てリクエストを処理し続ける必要があるかもしれません。
データは不変なので、これは良い解決策です。これにより、可変長オブジェクトの概念を放棄し、各Pair
に必要なデータへのポインターを含めることができます。これは、データ用の追加のメモリ割り当てにほとんどコストがかからないためです。
オブジェクトをヒープから破棄してメモリを解放できるようにしたい場合、状況はさらに複雑になります。ポインタではなくポインタへのポインタを使用して、常にオブジェクトを移動できるようにする必要があります。削除されたオブジェクトのスペースを取り戻すために、ヒープ内を移動します。追加の間接処理によりすべてが少し遅くなりますが、標準のランタイムライブラリのメモリ割り当てルーチンを使用する場合と比較して、すべてが高速です。
ただし、最初にデータベースの単純で最低限の作業バージョンを作成して実際のアプリケーションで使用しない限り、これらすべてを考慮する必要はありません。