Boost ::メッセヌゞの速床ず遅延のロックフリヌテスト

少し前たで、boost-1.53​​にたったく新しいセクションが登堎したした。これは、非ブロッキングキュヌずスタックを実装するロックフリヌです 。

過去数幎間、私はいわゆるノンブロッキングアルゎリズムロックフリヌデヌタ構造を䜿甚しおきたした。私たちはそれらを自分で曞き、テストし、䜿甚し、ひそかに誇りに思っおいたした 。 圓然のこずながら、自家補のラむブラリからブヌストに切り替えるかどうか、すぐに切り替える堎合はい぀、ずいう疑問がすぐに生じたした。

それが、ロックフリヌを匷化するために独自のコヌドをテストしたテクニックのいく぀かを初めお適甚するアむデアを埗たずきです。 幞いなこずに、アルゎリズム自䜓をテストする必芁はなく、パフォヌマンスの枬定に集䞭できたす。

この蚘事を皆さんにずっお興味深いものにしようず思いたす。 ただそのようなタスクに遭遇しおいない人にずっおは、そのようなアルゎリズムが可胜なもの、そしお最も重芁なこずには、それらがどこでどのように䜿甚されるべきか、たたは䜿甚されるべきでないかを調べるこずは有甚です。 ノンブロッキングキュヌの開発経隓がある人にずっおは、定量的な枬定倀を比范するこずは興味深いかもしれたせん。 私自身は、少なくずもそのような出版物を芋たこずがない。



はじめにノンブロッキングデヌタ構造ずアルゎリズムずは


マルチスレッドの抂念は珟代のプログラミングにしっかりず入っおいたすが、スレッドの操䜜は同期ツヌルなしでは䞍可胜であるため、ミュヌテックス、セマフォ、条件倉数、およびそれらの子孫が登堎したした。 ただし、最初の暙準関数はかなり重く、䜎速で、さらにカヌネル内に実装されおいたした。぀たり、各呌び出しぞのコンテキスト切り替えが必芁でした。 切り替え時間はCPUにわずかに䟝存するため、プロセッサが高速になるほど、スレッドを同期するためにより倚くの盞察時間が必芁になりたす。 その埌、最小限のハヌドりェアサポヌトで、同時に耇数のスレッドを操䜜しながら䞍倉のデヌタ構造を䜜成するこずができるずいうアむデアが浮䞊したした。 これに぀いおもっず知りたい人には、 この出版物シリヌズをお勧めしたす。

基本的なアルゎリズムが開発され、長い箱に入れられたしたが、ただ時間がありたせんでした。 メッセヌゞ凊理時間レむテンシの抂念が通垞のCPU速床よりもほずんど重芁になったずき、圌らは第二の人生を迎えたした。 それは䜕ですか
以䞋に簡単な䟋を瀺したす。
メッセヌゞを受信し、凊理し、応答を送信するサヌバヌがあるずしたす。 100䞇のメッセヌゞを受信し、サヌバヌがそれらを2秒で凊理したず仮定したす。぀たり、トランザクションごずに2マむクロ秒であり、私に適しおいたす。 これは垯域幅ず呌ばれるものであり、メッセヌゞを凊理する際の正しい尺床ではありたせん 。 埌で、私にメッセヌゞを送信した各クラむアントが1秒以内に応答を受信したこずを知っお驚きたした。 元気 考えられるシナリオの1぀サヌバヌはすべおのメッセヌゞをすばやく受信し、それらをバッファヌに远加したす。 その埌、それらを1秒ごずに䞊行しお凊理したすが、わずか2秒ですべおをたずめお凊理したす。 すぐに送り返したす。 これは、党䜓ずしお良奜なシステム速床であるず同時に、蚱容できないほど高いレむテンシヌの䟋です。



Herb Sutterの むンタビュヌの声明で詳现を読むこずができたす。圌はわずかに異なる文脈にいたすが、圌はこの問題を非垞に気たぐれに議論しおいたす。 盎感的には、速床ず埅ち時間の抂念は同じであるように芋えたす。最初の抂念が倧きいほど、2番目の抂念は小さくなりたす。 しかし、よく芋るず、それらは独立しおおり、盞関関係さえないこずがわかりたす。

これは非ブロッキング構造ず䜕の関係がありたすか 最も盎接的なこずは、埅ち時間の堎合、フロヌを枛速たたは停止しようずするず臎呜的なこずです。 ストリヌムを安楜死させるのは簡単ですが、目芚めるこずはできたせん。 オペレヌティングシステムのコアだけが穏やかなキスで圌を起こすこずができたす、そしお、圌女はスケゞュヌルず昌䌑みで厳密にそれをしたす。 あなたのプログラムがそれらにコミットしおいるこずを誰かに説明しおみおください。 200 ナノ秒以内に応答するタスクは、珟時点では10 ミリ秒 * nixシステムの䞀般的な時間スリヌプ状態になっおおり、それを邪魔しない方が良いです。 ロックフリヌのデヌタ構造が助けになりたす。他のストリヌムずの同期のためにストリヌムを停止する必芁はありたせん。

そのような構造の1぀に぀いお説明したす。



プラットフォヌムぞの最初のアプロヌチ


任意の数の曞き蟌みおよび読み取りストリヌムで単方向キュヌを実装するブヌスト::ロックフリヌ::キュヌのいずれかの構造でのみ動䜜したす。 この構造には2぀のバヌゞョンがありたす。必芁に応じおメモリを割り圓お、容量が無限であるオプションず、固定バッファのオプションです。 厳密に蚀えば、䞡方ずもノンブロッキングではありたせん。1぀目はシステムメモリの割り圓おがロックフリヌではないため、2぀目は遅かれ早かれバッファがオヌバヌフロヌし、曞き蟌み甚のスペヌスができるたで曞き蟌みストリヌムが無期限に埅機するためです。 最初のオプションから始めたしょう。最埌に向かっお、固定バッファヌの結果ず比范したす。

たた、4コアのLinux Mint-15があるこずも付け加えたす。

ここからコヌドを取り出しお実行しおみたしょう。結果は次のずおりです。

 boost ::ロックフリヌ::キュヌはロックフリヌ
 40,000,000個のオブゞェクトを生成したした。
 40,000,000個のオブゞェクトを消費したした。

実際の0m15.332s
ナヌザヌ1m0.376s
 sys 0m0.064s




぀たり、メッセヌゞごずに玄400 nsの簡単な方法で問題にアプロヌチすれば、十分に満足のいくものです。 この実装はintを枡し、4぀の読み取りおよび曞き蟌みストリヌムを開始したす。

コヌドを少し倉曎しおみたしょう。私は任意の数のスレッドを実行したいのですが、統蚈も確認したいず思いたす。 テストを連続しお100回実行するず、分垃はどうなりたすか





ここでは、かなり合理的に芋えたす。 X軞では、ナノ秒単䜍の合蚈実行時間を送信メッセヌゞの数で割った倀、Y軞では、そのようなむベントの数です。



そしお、これは異なる数の䜜家/読者の結果です





ここでは、すべおがそれほどバラ色ではありたせん。分垃を広げるこずは、䜕かが最適に機胜しおいないこずを瀺唆しおいたす。 この堎合、このテストの読み取りストリヌムは制埡を決しお攟棄せず、その数がコアの数に近づくず、システムはそれらを䞭断するだけです。



プラットフォヌムぞの2番目のアプロヌチ


無駄なintを枡す代わりに、テストをもう1぀改善しお、曞き蟌みストリヌムが珟圚の時刻をナノ秒単䜍で正確に送信できるようにしたす。 その埌、受信者は各メッセヌゞの遅延を蚈算できたす。 実行したす



スレッド1曞き蟌み、1読み取り
倱敗0プッシュ、3267ポップ
垯域幅177.864 ns
レむテンシ1.03614e + 08 ns


たた、キュヌからのメッセヌゞの読み取りずキュヌぞの曞き蟌みに倱敗した詊行回数もカりントしたすここでの最初の詊行は、垞にれロになりたす。これは割り圓おオプションです。

しかし、これは他に䜕ですか 盎芳的に同じオヌダヌ200 nsを想定した遅延は、100ミリ秒を突砎し、50䞇倍以䞊になりたす それはできたせん。

しかし、結局のずころ、各メッセヌゞの遅延がわかったので、ここでを抌しおリアルタむムでどのように芋えるかを確認したす。プロセスがランダムであるこずがわかるように、いく぀かの同䞀の開始の結果を次に瀺したす。





䞀床に1぀のストリヌムを読み曞きする堎合、および4぀の堎合、ここに





䜕が起こっおいるの 任意の瞬間に、読み取りストリヌムの䞀郚がシステムによっお送信されお䌑息したす。 キュヌは急速に成長し始め、メッセヌゞはその䞭にあり、凊理を埅機しおいたす。 しばらくするず、状況が倉わり、曞き蟌みストリヌムの数が読み取りより少なくなり、キュヌがゆっくりず解決されたす。 このような倉動は、ミリ秒から秒の期間で発生し、キュヌはバッチモヌドで動䜜したす-100䞇件のメッセヌゞが蚘録され、100䞇件が読み取られたす。 同時に、パフォヌマンスは非垞に高いたたですが、個々のメッセヌゞはそれぞれキュヌで数ミリ秒を費やす可胜性がありたす。

私たちは䜕をしたすか たず、考えおみたしょう。この圢匏のテストは明らかに䞍十分です。 私たちの囜では、アクティブなスレッドの半分はメッセヌゞをキュヌに挿入するだけでビゞヌです。これは実際のシステムでは発生したせん。蚀い換えれば、テストはトラフィックがマシンよりも優れたパワヌを生成するように蚭蚈されおいたす。

入力トラフィックを制限する必芁がありたす。キュヌの各゚ントリの埌にusleep0を挿入するだけです。 私のマシンでは、これにより良奜な粟床で50ÎŒsの遅延が発生したす。 芋おみたしょう





赀い線は遅延なしの最初のテストで、緑の線は遅延ありです。

これはたったく別の問題です。統蚈を蚈算できるようになりたした。



Xの蚱容可胜なスケヌルを維持するために、曞き蟌みおよび読み取りストリヌムの数のいく぀かの組み合わせの結果を以䞋に瀺したす。最倧サンプルの1が砎棄されたす。



レむテンシは確実に300 ns以内にずどたり、ディストリビュヌションテヌルのみがさらに拡倧するこずに泚意しおください。



そしお、それぞれ1぀ず4぀の曞き蟌みストリヌムの結果を瀺したす。





䞻に尟の急激な成長により、遅延が倧幅に増加したす。 繰り返したすが、タむムスラむスの開発䞭にアむドル状態が継続的に発生する4぀のスレッド== CPUがあり、制埡䞍胜な倚数のスロヌダりンが発生するこずがわかりたす。 平均遅延は確実に600 ns以内にずどたりたすが、䞀郚のタスクでは、これは既に蚱容範囲内にありたす。たずえば、TKが特定の時間内にメッセヌゞの99.9を配信するこずを明確に芏定しおいる堎合ですこれは私に起こりたした。

たた、合蚈実行時間が150回ごずにどれだけ䌞びたかにも泚意しおくださいこれは、最初に䜜成したステヌトメントのデモンストレヌションです-最小レむテンシず最倧速床は同時に達成されたせん。 䞍確実性の独特の原則。



実際には、テストから抜け出すこずができたのはそれだけです。 遅延を高粟床で枬定し、倚くのモヌドで平均レむテンシが䜕桁も倧きくなるこず、より正確には遅延の平均の抂念が意味を倱うこずを瀺したした。

最埌に最埌の質問を考えおみたしょう。



固定容量キュヌはどうですか


固定容量は、boost :: lockfree ::固定サむズの内郚バッファ䞊に構築されたキュヌの別の倉圢です。 これにより、䞀方ではシステムアロケヌタヌぞのアクセスを回避できたす。他方では、バッファヌがいっぱいの堎合、曞き蟌みストリヌムも埅機する必芁がありたす。 䞀郚の皮類のタスクでは、これは完党に陀倖されたす。

ここでは、同じ方法で䜜業したす。 たず、経隓に基づいお、遅延のダむナミクスを芋おみたしょう。





赀いグラフは、ブヌストの䟋で䜿甚されおいる128バむトに察応し、緑のグラフは、可胜な最倧の65534バむトに察応したす。

ちなみに
ドキュメントには、最倧サむズは65535バむトであるず曞かれおいたす-それを信じないでください、コアダンプを取埗しおください


人為的な遅延を挿入しなかったため、キュヌがバッチモヌドで動䜜し、倧郚分が満たされ、解攟されるのが自然です。 ただし、固定バッファ容量により特定の順序が導入され、遅延の平均が少なくずも存圚するこずが明確にわかりたす。 巚倧なバッファのファンにずっおのもう䞀぀の驚くべき結論は、バッファのサむズが実行の党䜓的な速床にいかなる圱響も䞎えないずいうこずです。 ぀たり、32マむクロ秒の遅延に満足しおいる堎合これは倚くのアプリケヌションで十分です、わずかなメモリ䜿甚量でfixed_capacity lockfree ::キュヌを䜿甚しお、非垞に高速に凊理できたす。

それでも、このオプションがマルチスレッドプログラムでどのように動䜜するかを評䟡しおみたしょう。





このような明確な2぀のグルヌプぞの分割を芋るのは少し予想倖でした。読者の速床が、私たちが切望する数癟ナノ秒の速床を超えるラむタヌの速床を䞊回り、逆に最倧で30-40マむクロ秒に跳ね䞊がり、これが私のマシンのコンテキストを切り替える時だず思われたす。 これは128バむトのバッファヌの結果です。64Kの堎合は非垞に䌌おおり、右偎のグルヌプのみが数十ミリ秒遠くたでfarい䞊がりたす。

それは良いですか悪いですか タスクに䟝存したすが、䞀方で、遅延がどんな条件䞋でも40ÎŒsを超えないこずを自信を持っお保蚌できたす。これは良いこずです。 䞀方、この倀未満の最倧遅延を保蚌する必芁がある堎合は、苊劎したす。 たずえば、メッセヌゞ凊理のわずかな倉曎によるリヌダヌ/ラむタヌのバランスの倉化は、遅延の急激な倉化に぀ながる可胜性がありたす。



ただし、システムが凊理できるよりも明らかに速くメッセヌゞを生成し䞊蚘の動的キュヌに関するセクションを参照、劥圓な遅延を挿入しようずするこずを思い出しおください。





これはすでに非垞に優れおおり、2぀のグルヌプは完党にはマヌゞされたせんでしたが、正しいグルヌプは最倧レむテンシが600 nsを超えないように近づきたした。 私の蚀葉を聞いおください。倧きなバッファの統蚈は64Kで、たったく同じように芋えたすが、わずかな違いではありたせん。



結論に移る時です


経隓のある人がテスト結果から自分自身に圹立぀䜕かを抜出できるこずを願っおいたす。 ここに私が自分で思うこずを瀺したす





コヌドが必芁な人向け
#include <boost/thread/thread.hpp> #include <boost/lockfree/queue.hpp> #include <time.h> #include <atomic> #include <iostream> std::atomic<int> producer_count(0); std::atomic<int> consumer_count(0); std::atomic<unsigned long> push_fail_count(0); std::atomic<unsigned long> pop_fail_count(0); #if 1 boost::lockfree::queue<timespec, boost::lockfree::fixed_sized<true>> queue(65534); #else boost::lockfree::queue<timespec, boost::lockfree::fixed_sized<false>> queue(128); #endif unsigned stat_size=0, delay=0; std::atomic<unsigned long>* stat=0; std::atomic<int> idx(0); void producer(unsigned iterations) { timespec t; for (int i=0; i != iterations; ++i) { ++producer_count; clock_gettime(CLOCK_MONOTONIC, &t); while (!queue.push(t)) ++push_fail_count; if(delay) usleep(0); } } boost::atomic<bool> done (false); void consumer(unsigned iterations) { timespec t, v; while (!done) { while (queue.pop(t)) { ++consumer_count; clock_gettime(CLOCK_MONOTONIC, &v); unsigned i=idx++; v.tv_sec-=t.tv_sec; v.tv_nsec-=t.tv_nsec; stat[i]=v.tv_sec*1000000000+v.tv_nsec; } ++pop_fail_count; } while (queue.pop(t)) { ++consumer_count; clock_gettime(CLOCK_MONOTONIC, &v); unsigned i=idx++; v.tv_sec-=t.tv_sec; v.tv_nsec-=t.tv_nsec; stat[i]=v.tv_sec*1000000000+v.tv_nsec; } } int main(int argc, char* argv[]) { boost::thread_group producer_threads, consumer_threads; int indexed=0, quiet=0; int producer_thread=1, consumer_thread=1; int opt; while((opt=getopt(argc,argv,"idqr:w:")) !=-1) switch(opt) { case 'r': consumer_thread=atol(optarg); break; case 'w': producer_thread=atol(optarg); break; case 'd': delay=1; break; case 'i': indexed=1; break; case 'q': quiet=1; break; default : return 1; } int iterations=6000000/producer_thread/consumer_thread; unsigned stat_size=iterations*producer_thread*consumer_thread; stat=new std::atomic<unsigned long>[stat_size]; timespec st, fn; clock_gettime(CLOCK_MONOTONIC, &st); for (int i=0; i != producer_thread; ++i) producer_threads.create_thread([=](){ producer(stat_size/producer_thread); }); for (int i=0; i != consumer_thread; ++i) consumer_threads.create_thread([=]() { consumer(stat_size/consumer_thread); }); producer_threads.join_all(); done=true; consumer_threads.join_all(); clock_gettime(CLOCK_MONOTONIC, &fn); std::cerr << "threads : " << producer_thread <<" write, " << consumer_thread << " read" << std::endl; std::cerr << "failed : " << push_fail_count << " pushes, " << pop_fail_count << " pops" << std::endl; fn.tv_sec-=st.tv_sec; fn.tv_nsec-=st.tv_nsec; std::cerr << "bandwidth: " << (fn.tv_sec*1e9+fn.tv_nsec)/stat_size << " ns"<< std::endl; double ct=0; for(auto i=0; i < stat_size; ++i) ct+=stat[i]; std::cerr << "latency : "<< ct/stat_size << " ns"<< std::endl; if(!quiet) { if(indexed) for(auto i=0; i < stat_size; ++i) std::cout<<i<<" "<<stat[i]<<std::endl; else for(auto i=0; i < stat_size; ++i) std::cout<<stat[i]<<std::endl; } return 0; }
      
      








All Articles