बढ़ावा :: फ्लैट_मैप और इसका प्रदर्शन नक्शे और unordered_map की तुलना में


103

यह प्रोग्रामिंग में सामान्य ज्ञान है कि स्मृति स्थानीयता कैश हिट के कारण प्रदर्शन में बहुत सुधार करती है। मुझे हाल ही में पता चला है boost::flat_mapकि नक्शे का वेक्टर आधारित कार्यान्वयन कौन सा है। ऐसा लगता है कि यह लगभग उतना ही लोकप्रिय नहीं है जितना कि आपके विशिष्ट map/ unordered_mapमैं किसी भी प्रदर्शन तुलना को खोजने में सक्षम नहीं हूं। यह कैसे तुलना करता है और इसके लिए सबसे अच्छे उपयोग के मामले क्या हैं?

धन्यवाद!


यह ध्यान रखना महत्वपूर्ण है कि boost.org/doc/libs/1_70_0/doc/html/boost/container/… यह दावा करता है कि यादृच्छिक सम्मिलन लॉगरिदमिक समय लेता है, एक पॉपुलेशन को बढ़ावा देने का अर्थ है :: flat_map (n यादृच्छिक तत्वों को सम्मिलित करके) O (n log n) लेता है ) समय। यह झूठ है, जैसा कि नीचे दिए गए @ v.oddou के उत्तर में रेखांकन से स्पष्ट है: रैंडम इन्सर्ट O (n) है, और उनमें से n में O (n ^ 2) समय लगता है।
डॉन हैच

@DonHatch यहां इसकी रिपोर्ट कैसे करें: github.com/boostorg/container/issues ? (यह तुलना की संख्या की एक गिनती दे रहा हो सकता है, लेकिन यह वास्तव में भ्रामक है अगर चाल की संख्या के साथ नहीं है)
मार्क ग्लिससे

जवाबों:


188

मैंने हाल ही में अपनी कंपनी में विभिन्न डेटा संरचनाओं पर एक बेंचमार्क चलाया है, इसलिए मुझे लगता है कि मुझे एक शब्द छोड़ने की जरूरत है। किसी चीज़ को सही तरीके से बेंचमार्क करना बहुत जटिल है।

बेंचमार्किंग

वेब पर हम शायद ही कभी (अगर कभी) एक अच्छी तरह से इंजीनियर बेंचमार्क पाते हैं। आज तक मुझे केवल ऐसे बेंचमार्क मिले जो पत्रकार तरीके से किए गए (बहुत जल्दी और कालीन के नीचे दर्जनों वेरिएबल को व्यापक रूप से)।

1) आपको कैश वार्मिंग के बारे में विचार करना होगा

बेंचमार्क चलाने वाले अधिकांश लोग टाइमर की विसंगति से डरते हैं, इसलिए वे हजारों बार अपना सामान चलाते हैं और पूरे समय लेते हैं, वे बस हर ऑपरेशन के लिए एक ही हजार बार लेने के लिए सावधान हैं, और फिर उस तुलनीय पर विचार करें।

सच्चाई यह है कि वास्तविक दुनिया में, यह बहुत कम समझ में आता है, क्योंकि आपका कैश गर्म नहीं होगा, और आपके ऑपरेशन को केवल एक बार कॉल किया जाएगा। इसलिए आपको आरडीटीसीटी का उपयोग करके बेंचमार्क करने की आवश्यकता है, और समय सामान उन्हें केवल एक बार कॉल करना है। इंटेल एक कागज बना दिया है का वर्णन कैसे RDTSC उपयोग करने के लिए (कार्यक्रम यह स्थिर करने के लिए की शुरुआत में एक सीपीयूआईडी निर्देश का उपयोग कर पाइप लाइन फ्लश करने के लिए, और यह बुला कम से कम 3 बार)।

2) आरडीटीसीटी सटीकता माप

मैं यह करने की सलाह भी देता हूं:

u64 g_correctionFactor;  // number of clocks to offset after each measurement to remove the overhead of the measurer itself.
u64 g_accuracy;

static u64 const errormeasure = ~((u64)0);

#ifdef _MSC_VER
#pragma intrinsic(__rdtsc)
inline u64 GetRDTSC()
{
    int a[4];
    __cpuid(a, 0x80000000);  // flush OOO instruction pipeline
    return __rdtsc();
}

inline void WarmupRDTSC()
{
    int a[4];
    __cpuid(a, 0x80000000);  // warmup cpuid.
    __cpuid(a, 0x80000000);
    __cpuid(a, 0x80000000);

    // measure the measurer overhead with the measurer (crazy he..)
    u64 minDiff = LLONG_MAX;
    u64 maxDiff = 0;   // this is going to help calculate our PRECISION ERROR MARGIN
    for (int i = 0; i < 80; ++i)
    {
        u64 tick1 = GetRDTSC();
        u64 tick2 = GetRDTSC();
        minDiff = std::min(minDiff, tick2 - tick1);   // make many takes, take the smallest that ever come.
        maxDiff = std::max(maxDiff, tick2 - tick1);
    }
    g_correctionFactor = minDiff;

    printf("Correction factor %llu clocks\n", g_correctionFactor);

    g_accuracy = maxDiff - minDiff;
    printf("Measurement Accuracy (in clocks) : %llu\n", g_accuracy);
}
#endif

यह एक विसंगति मापक है, और यह समय-समय पर -10 ** 18 (64 बिट्स पहले नकारात्मक मूल्यों) से बचने के लिए, सभी मापा मूल्यों में से न्यूनतम लेगा।

आंतरिक और न इनलाइन विधानसभा के उपयोग पर ध्यान दें। पहली इनलाइन असेंबली आजकल कंपाइलरों द्वारा शायद ही कभी समर्थन की जाती है, लेकिन सभी के बहुत खराब होने पर, कंपाइलर इनलाइन असेंबली के चारों ओर एक फुल ऑर्डरिंग बैरियर बनाता है, क्योंकि यह अंदर के स्टैटिक का विश्लेषण नहीं कर सकता है, इसलिए यह वास्तविक दुनिया के सामान को बेंचमार्क करने के लिए एक समस्या है, खासकर जब सामान को कॉल करना एक बार। तो एक आंतरिक यहाँ अनुकूल है, क्योंकि यह संकलक फ्री-री-निर्देशों के आदेश को नहीं तोड़ता है।

3) पैरामीटर

अंतिम समस्या यह है कि लोग आमतौर पर परिदृश्य के बहुत कम बदलावों के लिए परीक्षण करते हैं। एक कंटेनर प्रदर्शन इससे प्रभावित होता है:

  1. संभाजक
  2. निहित प्रकार का आकार
  3. निहित प्रकार के कॉपी ऑपरेशन, असाइनमेंट ऑपरेशन, मूव ऑपरेशन, कंस्ट्रक्शन ऑपरेशन के कार्यान्वयन की लागत।
  4. कंटेनर में तत्वों की संख्या (समस्या का आकार)
  5. प्रकार में तुच्छ 3. संचालन हैं
  6. प्रकार POD है

बिंदु 1 महत्वपूर्ण है क्योंकि कंटेनर समय-समय पर आवंटित करते हैं, और यह बहुत मायने रखता है यदि वे सीआरटी "नया" या कुछ उपयोगकर्ता परिभाषित ऑपरेशन का उपयोग कर आवंटित करते हैं, जैसे कि पूल आवंटन या फ्रीलास्ट या अन्य ...

( pt 1 के बारे में रुचि रखने वाले लोगों के लिए, सिस्टम एलिमिनेटर प्रदर्शन प्रभाव के बारे में gamedev पर रहस्य सूत्र में शामिल हों )

प्वाइंट 2 इसलिए है क्योंकि कुछ कंटेनरों (ए) का कहना है कि चारों ओर सामान की प्रतिलिपि बनाने में समय खो जाएगा, और बड़े प्रकार का ओवरहेड जितना बड़ा होगा। समस्या यह है कि जब दूसरे कंटेनर बी से तुलना की जाती है, तो ए छोटे प्रकारों के लिए बी पर जीत सकता है, और बड़े प्रकारों के लिए खो सकता है।

अंक 3, बिंदु 2 के समान है, सिवाय इसके कि कुछ भार कारक द्वारा लागत को गुणा करता है।

बिंदु 4 कैश मुद्दों के साथ मिश्रित बड़े ओ का प्रश्न है। कुछ खराब-जटिलता वाले कंटेनर कम संख्या के प्रकारों (जैसे mapबनाम vector, के लिए कम-जटिलता वाले कंटेनरों को बेहतर ढंग से पछाड़ सकते हैं , क्योंकि उनका कैश लोकलिटी अच्छा है, लेकिन mapमेमोरी को टुकड़े करता है)। और फिर कुछ क्रॉसिंग बिंदु पर, वे खो देंगे, क्योंकि निहित समग्र आकार मुख्य मेमोरी को "लीक" करना शुरू कर देता है और कैश मिस हो जाता है, इसके अलावा तथ्य यह है कि स्पर्शोन्मुख जटिलता महसूस की जा सकती है।

प्वाइंट 5 कंपाइलर्स के बारे में है कि वे सामान को खाली करने में सक्षम हैं जो संकलन समय पर खाली या तुच्छ हैं। यह बहुत कुछ संचालन को अनुकूलित कर सकता है, क्योंकि कंटेनरों को विस्थापित किया जाता है, इसलिए प्रत्येक प्रकार का अपना प्रदर्शन प्रोफ़ाइल होगा।

बिंदु 5 के समान बिंदु 6, PODs इस तथ्य से लाभ उठा सकते हैं कि कॉपी निर्माण सिर्फ एक यादगार है, और कुछ कंटेनरों में इन मामलों के लिए एक विशिष्ट कार्यान्वयन हो सकता है, टी के लक्षण के अनुसार एल्गोरिदम का चयन करने के लिए आंशिक टेम्पलेट विशेषज्ञता या SFINAE का उपयोग कर सकते हैं।

फ्लैट के नक्शे के बारे में

जाहिरा तौर पर फ्लैट का नक्शा लोकी एसोसावेक्टर की तरह एक सदिश वेक्टर आवरण है, लेकिन कुछ पूरक आधुनिकीकरण के साथ सी ++ 11 के साथ आ रहा है, एकल तत्वों के डालने और हटाने में तेजी लाने के लिए चाल शब्दार्थ का शोषण कर रहा है।

यह अभी भी एक ऑर्डर किया गया कंटेनर है। अधिकांश लोगों को आमतौर पर ऑर्डर देने वाले हिस्से की आवश्यकता नहीं होती है, इसलिए इसका अस्तित्व है unordered..

क्या आपने माना है कि शायद आपको इसकी आवश्यकता है flat_unorderedmap? जो कुछ ऐसा होगा google::sparse_mapया ऐसा कुछ होगा - एक खुला पता हैश मैप।

ओपन एड्रेस हैश मैप्स की समस्या यह है कि उस समय rehashउन्हें नए विस्तारित फ्लैट भूमि के चारों ओर सब कुछ कॉपी करना पड़ता है, जबकि एक मानक अनियंत्रित मानचित्र को सिर्फ हैश इंडेक्स को फिर से बनाना पड़ता है, जबकि आवंटित डेटा रहता है जहां यह है। बेशक नुकसान यह है कि स्मृति नरक की तरह खंडित है।

ओपन एड्रेस हैश मैप में रिहैस का मानदंड तब होता है जब क्षमता लोड फैक्टर द्वारा गुणा की गई बाल्टी वेक्टर के आकार से अधिक हो जाती है।

एक विशिष्ट भार कारक है 0.8; इसलिए, आपको इस बारे में ध्यान रखने की आवश्यकता है, यदि आप इसे भरने से पहले अपने हैश मानचित्र को पूर्व-आकार दे सकते हैं, तो हमेशा पूर्व-आकार: intended_filling * (1/0.8) + epsilonयह आपको गारंटी देगा कि भरने के दौरान कभी भी पूर्वाभास न करें और सब कुछ पुन: व्यवस्थित करें।

बंद पते के नक्शे ( std::unordered..) का लाभ यह है कि आपको उन मापदंडों की परवाह नहीं करनी है।

लेकिन boost::flat_mapएक आदेश दिया वेक्टर है; इसलिए, इसमें हमेशा एक लॉग (N) स्पर्शोन्मुख जटिलता होती है, जो खुले पते हैश मैप (अमूर्त निरंतर समय) से कम अच्छी होती है। आपको उस पर भी विचार करना चाहिए।

बेंचमार्क परिणाम

यह एक परीक्षण है जिसमें विभिन्न मानचित्र ( intकुंजी और __int64/ somestructमान के रूप में) और std::vector

परीक्षण प्रकार की जानकारी:

typeid=__int64 .  sizeof=8 . ispod=yes
typeid=struct MediumTypePod .  sizeof=184 . ispod=yes

निवेशन

संपादित करें:

मेरे पिछले परिणामों में एक बग शामिल था: उन्होंने वास्तव में आदेशित सम्मिलन का परीक्षण किया था, जिसने फ्लैट मानचित्रों के लिए बहुत तेज़ व्यवहार प्रदर्शित किया था।
मैंने उन परिणामों को बाद में इस पृष्ठ पर छोड़ दिया क्योंकि वे दिलचस्प हैं।
यह सही परीक्षण है: यादृच्छिक डालें 100

यादृच्छिक डालने 10000

मैंने क्रियान्वयन की जाँच की है, यहाँ सपाट नक्शों में लागू किया गया कोई ऐसा काम नहीं है। प्रत्येक सम्मिलन मक्खी पर होता है, इसलिए यह मानदंड स्पर्शोन्मुख प्रवृत्ति प्रदर्शित करता है:

नक्शा: O (N * log (N))
हैशमैप: O (N)
वेक्टर और फ़्लैटमैप: O (N * N)

चेतावनी : के लिए इसके बाद 2 परीक्षण std::mapऔर दोनों flat_mapरों रहे हैं गाड़ी है और वास्तव में परीक्षण का आदेश दिया प्रविष्टि (बनाम अन्य कंटेनरों के लिए यादृच्छिक प्रविष्टि हाँ यह खेद भ्रामक है।):
आरक्षण के बिना 100 तत्वों का मिश्रित सम्मिलित

हम देख सकते हैं कि आदेश दिया गया सम्मिलन, पीछे धकेलने का परिणाम है, और बहुत तेज़ है। हालांकि, मेरे बेंचमार्क के गैर-चार्टेड परिणामों से, मैं यह भी कह सकता हूं कि यह बैक-इंसर्शन के लिए पूर्ण अनुकूलता के पास नहीं है। 10k तत्वों पर, पूर्व-आरक्षित वेक्टर पर सही बैक-इंसर्शन इष्टतमता प्राप्त की जाती है। जो हमें 3Million चक्र देता है; हम यहां दिए गए सम्मिलन के लिए 4.8M निरीक्षण करते हैं flat_map(इसलिए इष्टतम का 160%)।

बिना आरक्षण के 10000 तत्वों की मिश्रित प्रविष्टि विश्लेषण: याद रखें कि यह वेक्टर के लिए 'रैंडम इंसर्ट' है, इसलिए बड़े पैमाने पर 1 बिलियन साइकिल प्रत्येक इंसर्शन पर डेटा को ऊपर (एक तत्व द्वारा एक तत्व) में आधा शिफ्ट करने के लिए आता है।

3 तत्वों की यादृच्छिक खोज (1 के लिए क्रमबद्ध घड़ियां)

आकार में = 100

100 तत्वों के कंटेनर के भीतर रैंड सर्च

आकार में = 10000

10000 तत्वों के कंटेनर के भीतर रैंड सर्च

यात्रा

आकार 100 से अधिक (केवल मध्यम प्रकार)

100 से अधिक मध्यम फली में परिवर्तन

आकार 10000 से अधिक (केवल मध्यम प्रकार)

10000 से अधिक मध्यम फली में परिवर्तन

नमक का अंतिम अनाज

अंत में मैं "बेंचमार्किंग 13 Pt1" (सिस्टम आवंटनकर्ता) पर वापस आना चाहता था। हाल ही में किए गए एक प्रयोग में मैं अपने द्वारा विकसित किए गए एक खुले पते के मानचित्र के प्रदर्शन के आसपास कर रहा हूं , मैंने कुछ std::unordered_mapउपयोग मामलों पर विंडोज 7 और विंडोज 8 के बीच 3000% से अधिक के प्रदर्शन अंतर को मापा ( यहां चर्चा की गई )।
जो मुझे उपरोक्त परिणामों के बारे में पाठक को चेतावनी देना चाहता है (वे Win7 पर किए गए थे): आपका लाभ भिन्न हो सकता है।

सादर


1
ओह, उस मामले में यह समझ में आता है। वेक्टर का निरंतर परिशोधन समय केवल तभी लागू होता है जब वह अंत में सम्मिलित होता है। रैंडम पोजिशन्स में इंसर्टिंग औसत O (n) प्रति इंसर्ट करना चाहिए क्योंकि इनसर्शन पॉइंट के बाद सब कुछ आगे बढ़ाना चाहिए। इसलिए हम आपके बेंचमार्क में द्विघात व्यवहार की अपेक्षा करेंगे, जो कि छोटे N के लिए भी काफी तेज गति से चलता है। AssocVector शैली के कार्यान्वयन संभवत: तब तक छंट रहे हैं जब तक कि प्रत्येक लुकअप के बाद छँटाई की बजाय उदाहरण के लिए आवश्यक है। अपने बेंचमार्क को देखे बिना कहना मुश्किल है।
बिली ओनेल

1
@ बिलियोनियल: आह, हमने एक सहकर्मी के साथ कोड का निरीक्षण किया, और दोषी पाया, मेरी "यादृच्छिक" प्रविष्टि का आदेश दिया गया था क्योंकि मैं एक std का उपयोग करता था :: यह सुनिश्चित करने के लिए कि सम्मिलित कुंजियाँ अद्वितीय थीं। यह एक सादा असंतुलन है, लेकिन मैंने तय किया कि random_shuffle के साथ, मैं अब पुनर्निर्माण कर रहा हूं और कुछ नए परिणाम जल्द ही एक संपादन के रूप में दिखाई देंगे। तो अपने वर्तमान स्थिति में परीक्षण मानता है कि "आदेशित सम्मिलन" बहुत जल्दी खराब है।
v.oddou

3
"इंटेल का एक पेपर है" "और यहाँ यह है
isomorphismes

5
शायद मुझे कुछ स्पष्ट याद आ रहा है, लेकिन मुझे समझ में नहीं आता है कि यादृच्छिक खोज की flat_mapतुलना में धीमी क्यों है std::map- क्या कोई इस परिणाम की व्याख्या करने में सक्षम है?
बॉयसी

1
मैं इसे इस समय के बढ़ावा कार्यान्वयन के एक विशिष्ट ओवरहेड के रूप में समझाऊंगा, न flat_mapकि एक कंटेनर के आंतरिक चरित्र के रूप में। क्योंकि Aska::संस्करण std::mapलुकअप से तेज है । यह साबित करना कि अनुकूलन के लिए जगह है। अपेक्षित प्रदर्शन समान रूप से समान है, लेकिन शायद कैश स्थानीयता के लिए थोड़ा बेहतर धन्यवाद। उच्च आकार के सेट के साथ उन्हें अभिसरण करना चाहिए।
v.oddou

6

डॉक्स से ऐसा लगता है कि यह वह अनुरूप है Loki::AssocVectorजिससे मैं काफी भारी उपयोगकर्ता हूं। चूंकि यह एक वेक्टर पर आधारित है, इसलिए इसमें एक वेक्टर की विशेषताएं हैं, जो कहना है:

  • जब भी इससे sizeआगे बढ़ता है Iterators अमान्य हो जाता है capacity
  • जब यह परे बढ़ता है तो capacityइसे पुन: प्राप्त करने और वस्तुओं को स्थानांतरित करने की आवश्यकता होती है , अर्थात endजब डालने पर विशेष मामले को छोड़कर निरंतर समय की गारंटी नहीं होती हैcapacity > size
  • लुक से अधिक तेजी से है std::mapकैश इलाके, एक द्विआधारी खोज जो रूप में एक ही प्रदर्शन लक्षण है की वजह से std::mapअन्यथा
  • कम मेमोरी का उपयोग करता है क्योंकि यह एक लिंक बाइनरी ट्री नहीं है
  • यह कभी नहीं सिकुड़ता है जब तक कि आप इसे जबरन नहीं बताते (क्योंकि इससे वास्तविक वसूली शुरू होती है)

सबसे अच्छा उपयोग तब होता है जब आप पहले से ही तत्वों की संख्या जानते हैं (इसलिए आप reserveअग्रिम कर सकते हैं ), या जब सम्मिलन / निष्कासन दुर्लभ है, लेकिन लुकअप अक्सर होता है। Iterator अमान्य इसे कुछ उपयोग के मामलों में थोड़ा बोझिल बनाता है ताकि वे कार्यक्रम शुद्धता के मामले में विनिमेय न हों।


1
गलत :) शो मैप के ऊपर माप माप कार्यों के लिए फ्लैट_मैप की तुलना में तेज़ है, मुझे लगता है कि कार्यान्वयन को ठीक करने के लिए बढ़ावा पीपीएल की आवश्यकता है, लेकिन सिद्धांत रूप में आप सही हैं।
NoSenseEtAl
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.