最新のC ++へのキー/バリューストア開発の移植


9

Cassandraに似たデータベースサーバーを開発しています。

開発はCで始まりましたが、クラスがなければ非常に複雑になりました。

現在、私はすべてをC ++ 11に移植しましたが、まだ「モダンな」C ++を学習しており、多くのことについて疑問があります。

データベースはキー/値のペアで動作します。すべてのペアにはさらに多くの情報があります。いつが作成され、いつ期限切れになるか(期限切れでない場合は0)。各ペアは不変です。

キーはC文字列、値はvoid *ですが、少なくとも今のところ、値をC文字列として操作しています。

抽象IListクラスがあります。3つのクラスから継承

  • VectorList -C動的配列-std :: vectorに似ていますが、 realloc
  • LinkList -チェックとパフォーマンス比較のために作成
  • SkipList -最終的に使用されるクラス。

将来はRed Black木も作ろうと思います。

それぞれIListに、キーでソートされた、0個以上のペアへのポインターが含まれています。

IList長くなりすぎた場合は、特殊ファイルとしてディスクに保存できます。この特殊ファイルは一種ですread only list

キーを検索する必要がある場合は、

  • 最初にメモリ内IListが検索されます(SkipListSkipListまたはLinkList)。
  • 次に、日付でソートされたファイルに検索が送信されます
    (最新のファイルが最初、最も古いファイル-最後)。
    これらのファイルはすべてメモリにmmapされます。
  • 何も見つからない場合、キーは見つかりません。

私は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つのnull終了バイトのため、バッファの初期サイズは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::BlobPairそう、Pair::Blobちょうどバッファであることを、私はそれをテストしたとき、それはおそらく周りのオブジェクトデータをコピーするので、かなり遅かったです。

私が考えているもう1つの変更は、Pairクラスを削除して置き換え、std::shared_ptrすべてのメソッドをに「プッシュ」することPair::Blobですが、これは可変サイズのPair::Blobクラスでは役に立ちません。

C ++との親和性を高めるために、オブジェクトデザインをどのように改善できるのかと考えています。


完全なソースコードはこちら:https :
//github.com/nmmmnu/HM3


2
なぜ使用しないのですstd::mapstd::unordered_map?値(キーに関連付けられている)がいくつvoid*かあるのはなぜですか?おそらく、いつかそれらを破壊する必要があるでしょう。どのように&いつ?テンプレートを使用しないのはなぜですか?
Basile Starynkevitch、2015

私はstd :: mapを使用していません。現在のケースではstd :: mapよりも良いことをする(または少なくとも試す)ためです。しかし、はい、ある時点でstd :: mapをラップして、IListとしてのパフォーマンスもチェックすることを考えています。
Nick

割り当て解除とd-torsの呼び出しは、要素がある場所、IList::removeまたはIListが破棄されたときに行われます。時間はかかりますが、別のスレッドで行います。std::unique_ptr<IList>とにかく、IListは簡単になるでしょう。新しいリストで「切り替え」て、古いオブジェクトをd-torを呼び出せる場所に保持できるようにします。
Nick

テンプレートを試しました。これはユーザーライブラリではないため、ここでは最善の解決策ではありません。キーは常にC string、データは常に何らかのバッファvoid *またはchar *なので、char配列を渡すことができます。redisまたはで類似のものを見つけることができますmemcached。ある時点でstd::string、キーにchar配列を使用するか固定するかを決定できますが、下線はまだC文字列のままです。
Nick

6
4つのコメントを追加するのではなく、質問
Basile Starynkevitch

回答:


3

私がお勧めするアプローチは、Key-Valueストアのインターフェースに焦点を当てることで、可能な限りクリーンで可能な限り制限のないものにすることです。つまり、呼び出し元に最大限の自由を与えるだけでなく、選択に最大限の自由を与える必要があります。それを実装する方法。

次に、パフォーマンスをまったく気にせずに、可能な限り最小限のクリーンな実装を提供することをお勧めします。私にとってそれはunordered_mapあなたの最初の選択であるように思えるか、またはおそらくmap何らかの種類のキーの順序付けがインターフェースによって公開されなければならない場合です。

したがって、最初にそれをクリーンかつ最小限に機能させる。次に、それを実際のアプリケーションで使用します。そうすることで、インターフェースで対処する必要のある問題が見つかります。次に、それらに対処してください。ほとんどの可能性は、インターフェイスを変更した結果、実装の大部分を書き換える必要があるため、実装の最初の反復に費やす時間が、実行に必要な最小限の時間を超えている場合です。かろうじて作業は時間の無駄です。

次に、それをプロファイリングし、インターフェイスを変更せずに、実装で改善する必要があるものを確認します。または、プロファイルを作成する前に、実装を改善する方法について独自のアイデアを持っている場合もあります。それは問題ありませんが、これらのアイデアにいつでも取り組む必要はありません。

あなたはあなたがより上手になりたいと望んでいると言いますmap。それについて言えることは2つあります。

a)あなたはおそらくしません。

b)すべてのコストで時期尚早の最適化を避けます。

実装に関しては、メモリ割り当てに問題があると思われる問題を回避するためにデザインを構造化する方法に関心があるように見えるため、主な問題はメモリ割り当てにあるようです。C ++でのメモリ割り当ての問題に対処する最良の方法は、適切なメモリ割り当て管理を実装することであり、それらの周りにデザインをねじったり曲げたりすることではありません。言語ランタイムが提供するものにかなりこだわっているJavaやC#などの言語とは対照的に、独自のメモリ割り当て管理を可能にするC ++を使用していることを幸運であると考える必要があります。

C ++でのメモリ管理にはさまざまな方法があり、new演算子をオーバーロードする機能が役立つ場合があります。プロジェクトの単純化したメモリアロケータは、巨大なバイト配列を事前に割り当て、ヒープとして使用します。(byte* heap。)firstFreeByteゼロに初期化されたインデックスがあり、ヒープ内の最初の空きバイトを示します。Nバイトのリクエストが来たら、アドレスを返してにheap + firstFreeByte追加NfirstFreeByteます。したがって、メモリ割り当ては非常に高速で効率的になるため、実質的に問題はありません。

もちろん、すべてのメモリを事前に割り当てるのは良い考えではないかもしれません。そのため、ヒープをオンデマンドで割り当てられるバンクに分割し、任意の瞬間に最新のバンクからの割り当てリクエストを処理し続ける必要があるかもしれません。

データは不変なので、これは良い解決策です。これにより、可変長オブジェクトの考え方を放棄し、必要に応じて各オブジェクトにPairデータへのポインターを含めることができます。これは、データ用の追加のメモリ割り当てに実質的にコストがかからないためです。

オブジェクトをヒープから破棄してメモリを再利用できるようにしたい場合、状況はより複雑になります。ポインタではなくポインタへのポインタを使用して、常にオブジェクトを移動できるようにする必要があります。削除されたオブジェクトのスペースを取り戻すために、ヒープ内を移動します。追加の間接処理によりすべてが少し遅くなりますが、標準のランタイムライブラリのメモリ割り当てルーチンを使用する場合と比較して、すべてが高速です。

ただし、最初にデータベースの単純で最小限の作業バージョンを作成して実際のアプリケーションで使用しないと、これらすべてを考慮する必要はありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.