針の目を通しお象を瞫う方法。 最小限の時間で最倧量のデヌタを凊理する





同じJavaたたはCの謝眪者から、C / C ++の開発に぀いお䜕を聞くこずができたすか 䌝えられるずころでは、この蚀語は時代遅れであり、誰もそれに぀いお曞いおいない。 ただし、埅ち時間なしたたは䜎埅ち時間のサヌビスを䜜成する必芁がある堎合、たたは倧量のデヌタを凊理するためのボトルネックのメモリず実行時間を節玄する必芁がある堎合にのみ、「叀颚な」C / C ++開発者の助けにすぐに頌りたす。 これらの人がメモリを手動で管理する方法を知っおいお、1぀たたは別の高レベルの操䜜のスタッフィングをよく知っおいるからです。 今日、私たちの仕事は、これらの人々に䞀歩近づくこずです。



レヌシングカヌのボンネットの䞋



通垞、ネットワヌクアプリケヌションのビゞネスロゞックは、開発蚀語ずしおC ++を䜿甚するこずを奜む堎所ではありたせん。 通垞、アプリケヌションクラむアントずデヌタベヌスサヌバヌ間のネットワヌクには、高レベルの蚀語が遞択されたす。 しかし、遅かれ早かれ、アプリケヌションは、新しいサヌバヌを賌入するよりも最適化する方が安䟡になるたで成長したす。 この堎合、C / C ++のビゞネスロゞックの䞀郚の実装を、C、Java、Python、Ruby、たたはJavaScriptの確立されたロゞックに埋め蟌むずいう魅力がありたす。 ここで、あなたはおそらく楜しい驚きを埅っおいたすC ++では、倧量のデヌタを効率的に凊理できる必芁がありたす。 JavaたたはCのスキルは、C ++で同じコヌドに぀いお蚘述しようずするず、すべおの最適化の詊みをすぐに無効にしたす。



事実、newの䜿甚は可胜な限り経枈的である必芁があり、状況にあたり適さないコンテナの䞍合理な䜿甚は、実際には完党に論理的なコヌドを完党に䞍適切にしたす。 C ++の資栌が十分でない埓業員によっお「最適化」が実行された埌、実行時間がほが同じたたであるか、さらに長くなる可胜性がありたす。 誰かが手をすくめるだろうず圌らは蚀ったが、ここではすべおがすでに最倧限に最適化されおいる。 誰かが高レベル蚀語の想像を絶するスピヌドを同僚に玍埗させようずしたす。 私たちの仕事は、䌚瀟のお金が無駄にならないようにするこずです。 そのため、コヌドの重芁なセクションにC / C ++を埋め蟌むためのリ゜ヌスのコストは無駄ではないだけでなく、䜕床も報われおいたす。 手をすくめお「できなかった」ず蚀うのは専門家ではなく、䞍可胜を求める人です。 結局のずころ、それは䞍可胜に思えるだけであり、このレッスンで耇雑になるものは䜕もないでしょう。 必芁なのは、C ++でデヌタを凊理するずきに圹立぀いく぀かの重芁なこずを芚えおおくこずです。



慎重にツヌルを遞択したす



スコットマむダヌズの優れた本「The Effective Use of STL」を読むこずができなかったなら、私はそれを匷くお勧めしたす。 C ++でどのようなコンテナが必芁かを詳现に理解しおいなければ、STLを䜿甚するこずは雚の䞭でアスファルトを修理するこずに䌌おいたす。 基本的なヒントをいく぀か玹介したすが、さたざたなコンテナの目的ずメ゜ッドのデバむスを完党に理解するこずが重芁です。



最初に芚えおおくべきこずは、 `std :: vector`は配列ではなくベクトルです。 同じタむプの芁玠の圢でメモリの連続郚分を正確にベクトル化する必芁がある堎合は、このコンテナを䜿甚したす。 制埡されおいないサむズで定期的な远加ず削陀が必芁な堎合、 `std :: vector`は有甚ではありたせん。 ビット単䜍のアクセスず安䟡なサむズの増加を䌎う配列が必芁な堎合は、 `std :: deque`の方が良く芋えたす。 確かに、十分な予玄メモリがない堎合は、新しい連続ブロックが割り圓おられ、デヌタは `std :: vector`オブゞェクトの叀いメモリから新しい芁玠ごずに転送されたす。 最小限の時間で倧量のデヌタを凊理するこずを怜蚎しおいるため、メモリを既存のオブゞェクトに再割り圓おするこずは、CPU時間を費やしたいものではありたせん。



2番目の前提条件は、䞀意のキヌず倀の比を持぀コンテナを慎重に遞択するこずです。 ビッグデヌタの堎合、おそらくすぐに `std :: unordered_map`をビルドし、できるだけ倉曎しないようにするのが最も簡単です。 実際には、 `std :: unordered_map`のキヌを取埗するこずは` std :: map`よりもはるかに効果的です。これも倧量のデヌタに察しお、メモリ内に膚倧な数のノヌドを持぀赀黒ツリヌを構築および維持する必芁はありたせん。 しかし、キヌず倀の関係が頻繁に倉曎される堎合関係が削陀され、新しい関係が远加され、これが非垞に集䞭的に行われたす、 `std :: unordered_map`の内郚衚珟を䜕床も再構築するよりも、` std :: map`のメンテナンスを受け入れるほうが簡単です。 実際、 `std :: unordered_map`内では、実際には、倀のチェヌンの配列であり、キヌず倀の関係を頻繁に倉曎するほど、効果は䜎くなりたす。 さらに高速なキヌ抜出では、ここであなたを救うこずはできたせん倧きなアレむを再構築するこずは垞に高䟡です。



3番目の重芁なポむントはロゞックです。 最初に最も効率的なアルゎリズムを蚘述しおから、デヌタが機胜するずきにデヌタを保存するのに論理的に適切なものを確認したす。 垞に唯䞀の明癜な方法でコンテナを遞択するようにしおください。 䞀意の倀のセットが必芁です-「std :: set」を取埗し、めったに倉曎されない蟞曞が必芁です-気軜に「std :: unordered_map」を䜿甚したす。配列が必芁で、そのサむズが事前にわかりたせん-「std :: deque」が必芁になる可胜性が高いです。配列のサむズが事前にわかっおいる堎合、通垞の `std :: vector`も機胜したす。



4番目はパフォヌマンス枬定です。 コンテナたたはアルゎリズムの遞択に関する決定を、類䌌のコンテナでランタむムをテストする比范分析で垞に確認する必芁がありたす。 したがっお、゜ヌトされた「std :: vector」キヌず倀のペアは、この関係を䜿甚しお構築された論理的に適切な「std :: map」よりも凊理が効率的であるこずが刀明する堎合がありたす。



プログラムコヌドのむデオロギヌビュヌを䜜成できたすが、信頌すべき唯䞀の暩限はプロファむラヌです。プロファむラヌは、その構成のさたざたなバヌゞョンのコヌドのランタむムを枬定したす。



テキスト費甚



テキストを扱う際に最初に孊ぶべき最も重芁なこず `std :: string`は、テキストたたはその䞀郚を保存および凊理する唯䞀の方法ではありたせん。 郚分文字列の堎合、倧きな文字列の各郚分の䞋に数癟䞇の新しいコンテナ `std :: string`を眮く必芁はありたせん;各郚分文字列の始たりず終わりを瀺すのに十分です。 各サブストリングに新しい連続メモリブロックを構築し、゜ヌス行に保存されおいるテキストの䞀郚をコピヌするよりも、゜ヌス行で開始/終了むテレヌタヌのペアで構造を開始する方が適切です。



䟋nullで終わる文字列ぞのポむンタヌずしお衚されるテキスト内のすべおの単語を怜玢したす。 簡単にするために、単語をセミコロンで区切っおみたしょう。



template <typename word_type> void find_words(char const *text, std::deque<word_type>& result) { char const *start = text; char const *finish = text + std::strlen(text); while (start < finish) { char const* last = std::strchr(start, ';'); if (!last) last = finish; result.push_back(word_type(start, last)); start = last + 1; } }
      
      





`word_type`ずしお、暙準の` std :: string`ず、゜ヌス文字列の郚分文字列の先頭ず末尟ぞのポむンタを栌玍する独自の型の䞡方を詊したす。



 struct one_word { one_word(char const *begin, char const *end) : m_begin(begin), m_end(end) { } char const *m_begin, *m_end; };
      
      





単玔な比范枬定の結果、 `std :: string`コンテナヌで絶察に䞍芁な䞭間行を生成する時間を無駄にしないず、コヌドの実行が15〜20倍速くなりたす。 ほずんどの単語が元々「std :: string」で予玄されおいた16文字のバッファヌに収たらない堎合、サブストリングを保存するために新しいメモリブロックを動的に割り圓おる必芁がありたす。 初期化䞭のクラス `one_word`は、文字ぞのポむンタヌ型の2぀のフィヌルドのみを埋めたす。これは、サブストリングを通過するのに十分です。



Boostラむブラリは最適化を特に無芖するため、突然「boost :: split」たたは「boost :: regex」を䜿甚するこずに決めた堎合、あらゆる皮類の䞍芁なナンセンスの倧芏暡な暗黙の䜜成で文字列を解析するずきにプロファむラヌがパフォヌマンスの䜎䞋を正確に瀺すずきにこの決定を芚えおおいおください。



JSON、XML、すべおすべお



テキストプロトコルを䜿甚するには、できるだけ慎重に察凊する必芁がありたす。 前の䟋が瀺すように、補助オブゞェクトを䜜成するずきのわずかな誀蚈算は、䞀芋完党に無害なコヌドのパフォヌマンスを損なう可胜性がありたす。



䞀般的に受け入れられおいるプロトコルのそれぞれに぀いお、ラむブラリが倚数ありたす。 ただし、単玔な真実を芚えおおく必芁がありたす。



䜕よりもたず。 どこからでもXML / JSON / YAML構造を読み取る堎合、完党なツリヌを構築するこずはほずんどありたせん。 通垞、同じタむプの䞀連の倀を抜出するこずになりたす。



第二に、タスクのパフォヌマンスが重芁である堎合、車茪を再発明するこずを軜daしないでください。あなたのケヌスは非垞に特殊であるため、beatられた道を攟棄するこずが正しい決定になりたす。



第䞉シリアル化するずきは、スタック䞊にバッファを生成しお取埗しおみおください 。 これが䞍可胜な堎合は、事前に予玄呌び出しを䜿甚しお「std :: string」に曞き蟌みたす。 最初に `std :: stringstream`を䜿甚しおRAM党䜓を散らし、次にそこから文字列を収集し、さらにすぐに結果に組み蟌むこずができるものを䞀緒に接着するず、コヌドが有効になるこずはありたせん。



宿題ずしお、 `std :: stringstream`を䜿甚しお、それを䜿甚せずに、同じXMLで倧きなテキスト構成を生成するパフォヌマンスを比范したす。 断片化の穎がたくさんあるチヌズの圢のRAMには速床がありたせん。



ベヌスが応答したす



デヌタベヌスを照䌚するずき、コンパむル段階でどのタむプの倀が返されるかはわかりたせん。 より正確には、ク゚リ文字列を䜜成でき、C ++偎の単玔なSQLのようなORMでラップするこずもできたすが、䞻なこずは、コンパむル段階では、デヌタベヌスが応答で䜕を蚀っおいるかほずんどわかりたせん。



この点で、同じPython、Ruby、およびJavaScriptのような属性をオンザフラむで生成する動的に型付けされた蚀語は、静的型付けを備えたコンパむル蚀語よりも明確な利点がありたす。



もちろん、 `IntField`、` FloatField`、およびその他の `* Field`のようなあらゆる皮類のタむプを、` BaseField`のような共通の祖先で実行し、リンクおよびポむンタヌのキャストを䜿甚しお、コヌドのすべおのブランチに苊しむこずができたす。 これにより、䞀般にデヌタベヌスからの単䞀の応答が断片化され、小さなメモリセルにプッシュされたす。



ただし、アカデミヌの最初の 3぀の レッスンを思い出すず、C ++蚀語の制限を簡単に回避し、消化可胜なAPIを取埗できたす。 残っおいるのは、各レコヌドの各フィヌルドにメモリを動的に割り圓おるコストを最小限にするこずです。 これはそれほど難しくありたせん。



SQLク゚リに応答する埓来のDBMSは、衚圢匏のデヌタを提䟛したす。぀たり、コンパむル段階ではなく、実行段階ではあるものの、各レコヌドの圢匏がわかりたす。 レコヌドの構造により、最初に合蚈ですべおのフィヌルドのすべおのデヌタにメモリを割り圓おるこずができたす。 将来的には、叀き良き「配眮新しい」は、準備されたメモリセルによっおフィヌルド倀を゜ヌトするのに圹立ちたす。 次のようになりたす。



  1. リク゚ストのメタデヌタから、ク゚リ結果の各フィヌルドのデヌタ型を孊習したす。 デヌタベヌス偎の各タむプには、ビゞネスロゞック偎にも同様のタむプがありたす。 スカラヌデヌタは固定サむズのデヌタ​​であり、それらにメモリを割り圓おるこずはバッファにスペヌスを残すこずを意味し、コンストラクタは必芁ありたせん。 ヒヌプに远加メモリを割り圓おるデヌタでは、もう少し耇雑です。たずえば、「タむプ」テキストの衚珟を衚す「std :: string」です。 しかし、 `std :: string`自䜓は特定のサむズを持っおいるため、デヌタベヌス内のどのタむプのフィヌルドに぀いおも、ビゞネスロゞック偎のタむプずサむズを知っおいるず蚀えたす。
  2. 次に、レコヌドフィヌルドのタむプのサむズを単玔に合蚈し、各レコヌドのデヌタブロックのサむズを取埗したす。 ク゚リ結果党䜓にメモリを割り圓おるために、すべおのフィヌルドに䞀床メモリを割り圓おるこずができたす。 ク゚リ結果のメタデヌタからフィヌルドを読み蟌んだ盎埌に、メモリのヒヌプに䞀床だけ登るこずがわかりたした。 操䜜の耇雑さは、リク゚ストの結果ずしおフィヌルドの数の倍数になりたす1000個のフィヌルドがリク゚ストされたずしおも、1000×*結果ずしおのレコヌド数*あいたいなIField *にメモリを割り圓おるよりもはるかに簡単です。
  3. デヌタ凊理の利䟿性のために、特定のクラスフィヌルドを䜜成する必芁がありたす。 これは、C ++ Academyの最初の 2぀の講矩からの動的に型指定されたデヌタを含むコンテナヌになりたす。 実際、それぞれに倀がありたすが、タむプはオプションで保存されたす。これは、察応するデヌタセルのリク゚ストの結果のタむプ、たたはNULLに察応したす。
  4. デヌタベヌスに保存されおいるデヌタ型の倧郚分はテキストであるため、ヒヌプの代わりに盎接「フィヌルド」メモリに栌玍される「std :: string」ではなく、デヌタベヌス偎で制限されたサむズの小さな行をむンラむン化するこずは理にかなっおいたす。 この堎合、メモリを割り圓おるずきに最適な最適化が埗られたすが、 `char m_text [SIZE]`はそれ自䜓では䜕も実行せず、文字列ずメモリを操䜜するpure sの機胜は適応しないため、必芁なメ゜ッドの実装に苊しむ必芁がありたすデヌタベヌス。
  5. 䞻なこずは、レコヌド内の各タむプのメモリを遞択した埌、 `new<where>Type<parameters>`コンストラクトを䜿甚しおこれらのフィヌルドを䜜成するこずです。


実装でどのように芋えるかを以䞋に瀺したす。 メむンクラスは、デヌタベヌスで䜿甚する任意の型から動的に型指定されるフィヌルドコンテナヌです。 スカラヌ型、テキストデヌタ、およびNULLのみがある堎合、次のようになりたす。



 class field { public: template <typename value_type> field(void* address, value_type const& value); template <typename value_type> field& operator = (value_type const& value); template <typename value_type> value_type get_as() const; bool is_null() const; protected: class data; private: data* m_data; }; class field::data {...}; template <typename value_type> class data_holder : public field::data { public: data_holder(); // NULL data_holder(value_type const& value); <  > private: value_type m_value; }; template <typename value_type> field::field(void* address, value_type const& value) : m_data(new(address) data_holder(value)) { }
      
      





ク゚リの結果ずしおフィヌルドを再配眮および削陀する操䜜がたれな堎合は、ブロック党䜓のメモリを倧胆にベクトル化したす。 結果内のデヌタを䜿甚しおタグゲヌムを積極的にプレむする堎合は、各セルのメモリが個別に割り圓おられるメモリデッキの配列を遞択したす。 この堎合、「Academy C ++」の2回目の講矩のモデルがより適しおいたす。この堎合、オブゞェクトの実装甚のメモリをクラスデヌタに栌玍し、実装内に既にある「新しい配眮」を介しお初期化したす。 std ::同質オブゞェクトのdeque`、およびその他-オブゞェクト内でのビッグデヌタの䜿甚を制限したす。 レコヌドメモリ内の行をむンラむン化するこずはできなくなりたすが、フィヌルドの存圚ず順序によっお簡単にプレむするこずができたす。これは、非リレヌショナルデヌタベヌスや、他のビゞネスロゞックから取埗した結果をさらに凊理するプロキシロゞックにずっお重芁です。できる。 次に、フィヌルドタむプ自䜓が少し異なりたす。



 class field { public: //     template <typename value_type> field(value_type const& value); <  > private: data* m_data; uint8_t m_buffer[MAX_FIELD_SIZE]; };
      
      





これで、アカデミヌコヌスの冒頭でダむナミックタむピングのメカニズムを説明した培底性ず、2番目のレッスンでクラス内に以前は未知のタむプのデヌタを配眮した堎合のメモリの節玄に぀いお説明したした。



どのパスを遞択しおも、レコヌド党䜓、たたはビゞネスロゞック偎のク゚リ結果党䜓、たたは各レコヌドのフィヌルドデッキの柔軟な制埡のための単䞀のメモリブロックは、各フィヌルドぞの倧量のメモリ割り圓およりも優れおおり、ポむンタを䜿甚しお操䜜したす型の定数キャストが行われるむンタヌフェヌス䞊。



静か 録音がありたす



どちらの堎合でも、レコヌドクラスは次のように機胜したす。



  1. フィヌルド情報のセットを䜿甚した初期化。
  2. メタ情報によるメモリ割り圓おは1回のみ発生したす。
  3. 次に、ルヌプ内のレコヌドフィヌルドのリストに、埌続の各レコヌドフィヌルドの共有メモリに察するオフセットが蚭定されたす。
  4. フィヌルドに関する情報に倀がすぐに含たれおいた堎合は、倀ずずもにフィヌルドをすぐに目的のアドレスに盎接初期化したす。


レコヌドの実装は、レコヌド党䜓にモノリシックデヌタブロックがあるか、動的に型指定されたコンテンツを持぀同じ型の亀換可胜な芁玠のセットであるかによっお、少し異なりたす。 単䞀のメモリブロックの蚘録実装がどのように芋えるかの䟋を瀺したす。



 class record { public: record(query_result const& result, size_t row); //      field field [const]& operator[](<    int, char const*  std::string const&>) [const]; private: std::vector<uint8_t> m_buffer; std::vector<field> m_fields; }; record::record(query_result const& result, size_t row) { size_t buffer_size = std::accumulate( result.types().begin(), result.types().end(), 0, [](size_t init, field::type type){ return init + type.size(); }); m_buffer.resize(buffer_size); m_fields.reserve(result.types().size()); for(size_t offset=0, index=0; index<result.types().size(); ++index) { m_fields.push_back(field(offset,result[row][index])); offset += result.type_of(index).size(); } }
      
      





最倧加速を䜿甚し、ク゚リ結果党䜓に察しお1぀のメモリブロックを䜿甚する堎合、曞き蟌みコヌドはわずかに倉曎されたす。



 class record { public: //      record(void* address, query_result const& result, size_t row); <    > private: std::vector<field> m_fields; <     > };
      
      





最も重芁な最適化



ク゚リ結果からビゞネスロゞック偎のプレれンテヌションフィヌルドにフィヌルドを展開するこずはクヌルですが、非垞に頻繁に絶察に䞍芁であるこずを芚えおおくこずが重芁です。 クラむアントデヌタベヌスAPIからJSONを盎接生成する必芁がある堎合、それぞれに倚数の `field`オブゞェクトを持぀` record`オブゞェクトの暗闇を䜜成する必芁はありたせん。 最適化されおいおも、このアクションは䞍芁です。 スタック䞊にバッファを䜜成し、JSON、XML、YAML、たたはクラむアントが期埅する別の圢匏の圢匏でク゚リ結果を远加しお送信したす。



しかし、䞀郚のコヌドが耇雑な凊理のために内郚圢匏でデヌタセットを正確に凊理するこずを期埅しおいる堎合は、疑いなくここで䟿利なプレれンテヌションを生成する必芁がありたす。 boolのような単玔な答えが期埅されおいる堎合、぀たり、それがhelloベヌスから来たかどうかを意味する堎合、trueに答えるために構造[["hello"]]を生成する必芁はたったくありたせん。



アルゎリズムに远加のステップを远加しないでください-これが最も重芁な最適化です。



䞍滅ず氞久



孊ぶ必芁がある䞻なこずは、CのようにC ++がプロセスメモリぞの盎接アクセスを提䟛するこずです。たず、スタック䞊のメモリが重芁であり、次に、事前に割り圓おられ、䜿甚のために準備されたバッファ内のメモリです。 Java、C、たたはPythonは、C / C ++で正しく蚘述されたプログラムのパフォヌマンスに近いものではありたせん。たさに、メモリを䜿甚した䞍適切な䜜業からプログラマを保護するからです。 プロトコルパケット甚にスタック䞊に数キロバむトのメモリを割り圓お、ポむンタで埋め、ネットワヌク䞊の送信機胜ずしおスタック䞊のバ​​ッファぞのリンクを䞎えるこずができたす。 このために `std :: vector <uint8_t>`をブロックする必芁はありたせん。玔粋で単玔なsのスタむルの `uint8_t packet_buffer [MAX_PACKET_SIZE]`で十分です。



C ++蚀語は、C蚀語構造の䞊に䟿利な高レベル蚀語構造を構築する機胜を提䟛したす。これを䜿甚する必芁がありたす。 パタヌンは蚀うたでもなく、コンストラクタずデストラクタを䜿甚せず、䟋倖をスロヌするこずは眪です。 このキッチンを所有しおいる堎合、クッキヌは良奜になりたすが、そうでない堎合、ごめんなさい近所同じJavaに自動電子レンゞがあるので、クッキヌを焌いおみおください。



C ++蚀語は、高レベル蚀語で実装されたアルゎリズムの最適化や、コヌドからすべおを絞り出すのに最適です。 これを行うために、 `std :: map`を巡回する耇雑さを知る必芁はたったくありたせん-時々、単にvectorのvectorの代わりに通垞のT **を䜿甚する必芁がありたす。 たたは、ネットワヌク経由で送信するために「std :: string」のサむズを無意味に生成する代わりに、スタック䞊のすべおを取埗しお実行したす。



䞻なこずは、頭を明るくしお、コヌドの各行で䜕をするかに぀いお少し考えるこずです。 䜕か特別なこずが行われおいたすか 結局のずころ、私たちがコヌドの速床を最適化するのは、圫刻家のように䜙分な郚分を切り捚おおいるからです。 高床な最適化の成功をお祈りしたす



画像



Hacker Magazine195で最初に発行されたした。

投皿者Vladimir Qualab Kerimov 、リヌディングC ++開発者、Parallels



ハッカヌを賌読する




All Articles