2012-12-24 85 views
222

मैं 500000 यादृच्छिक रूप से जनरेट Tuple<long,long,string> वस्तुओं की एक सूची है जिस पर मैं खोज "के बीच" एक साधारण प्रदर्शन कर रहा है:एक सॉर्ट किए गए सरणी की तुलना में एक क्रमबद्ध सरणी को धीमा क्यों कर रहा है?

var data = new List<Tuple<long,long,string>>(500000); 
... 
var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x); 

जब मैं अपने यादृच्छिक सरणी पैदा करते हैं और x के 100 बेतरतीब ढंग से उत्पन्न मूल्यों के लिए मेरे खोज चलाने के लिए, खोज लगभग चार सेकंड में पूर्ण हो जाती है। Item1 से तो Item2 द्वारा Item3 से पहले, और अंत में - - great wonders that sorting does to searching के जानने के बाद, हालांकि, मैं अपने डेटा को सॉर्ट करने का निर्णय लिया मेरी 100 खोजों को चलाने से पहले। मैं शाखा भविष्यवाणी की वजह से तेजी से एक छोटे से प्रदर्शन करने के लिए क्रमबद्ध संस्करण की उम्मीद: मेरी सोच रहा है कि एक बार हम बिंदु है जहां Item1 == x, t.Item1 <= x के सभी आगे के चेक को सही ढंग से शाखा भविष्यवाणी के रूप में "नहीं ले", पूंछ भाग को तेज करने के लिए मिलता है खोज का मेरे आश्चर्य करने के लिए बहुत, खोजें एक क्रमबद्ध सरणी पर जब तक दो बार ले ली!

मैं जिस क्रम में मैं अपने प्रयोगों भाग गया, और यादृच्छिक संख्या जनरेटर के लिए विभिन्न बीज इस्तेमाल किया आसपास स्विचन की कोशिश की है, लेकिन प्रभाव ही कर दिया गया है: खोज एक अवर्गीकृत सरणी में खोजों में के रूप में लगभग दो बार के रूप में तेजी से भाग गया एक ही सरणी, लेकिन क्रमबद्ध!

किसी को भी इस अजीब प्रभाव का एक अच्छा विवरण है? मेरे परीक्षणों का स्रोत कोड निम्नानुसार है; मैं .NET 4.0 का उपयोग कर रहा हूँ।


private const int TotalCount = 500000; 
private const int TotalQueries = 100; 
private static long NextLong(Random r) { 
    var data = new byte[8]; 
    r.NextBytes(data); 
    return BitConverter.ToInt64(data, 0); 
} 
private class TupleComparer : IComparer<Tuple<long,long,string>> { 
    public int Compare(Tuple<long,long,string> x, Tuple<long,long,string> y) { 
     var res = x.Item1.CompareTo(y.Item1); 
     if (res != 0) return res; 
     res = x.Item2.CompareTo(y.Item2); 
     return (res != 0) ? res : String.CompareOrdinal(x.Item3, y.Item3); 
    } 
} 
static void Test(bool doSort) { 
    var data = new List<Tuple<long,long,string>>(TotalCount); 
    var random = new Random(1000000007); 
    var sw = new Stopwatch(); 
    sw.Start(); 
    for (var i = 0 ; i != TotalCount ; i++) { 
     var a = NextLong(random); 
     var b = NextLong(random); 
     if (a > b) { 
      var tmp = a; 
      a = b; 
      b = tmp; 
     } 
     var s = string.Format("{0}-{1}", a, b); 
     data.Add(Tuple.Create(a, b, s)); 
    } 
    sw.Stop(); 
    if (doSort) { 
     data.Sort(new TupleComparer()); 
    } 
    Console.WriteLine("Populated in {0}", sw.Elapsed); 
    sw.Reset(); 
    var total = 0L; 
    sw.Start(); 
    for (var i = 0 ; i != TotalQueries ; i++) { 
     var x = NextLong(random); 
     var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x); 
     total += cnt; 
    } 
    sw.Stop(); 
    Console.WriteLine("Found {0} matches in {1} ({2})", total, sw.Elapsed, doSort ? "Sorted" : "Unsorted"); 
} 
static void Main() { 
    Test(false); 
    Test(true); 
    Test(false); 
    Test(true); 
} 

Populated in 00:00:01.3176257 
Found 15614281 matches in 00:00:04.2463478 (Unsorted) 
Populated in 00:00:01.3345087 
Found 15614281 matches in 00:00:08.5393730 (Sorted) 
Populated in 00:00:01.3665681 
Found 15614281 matches in 00:00:04.1796578 (Unsorted) 
Populated in 00:00:01.3326378 
Found 15614281 matches in 00:00:08.6027886 (Sorted) 
+14

शाखा भविष्यवाणी के कारण: पी –

+8

@jalf मुझे उम्मीद है कि क्रमबद्ध संस्करण शाखा भविष्यवाणी के कारण थोड़ा तेज़ प्रदर्शन करेगा। मेरी सोच यह थी कि एक बार जब हम उस बिंदु पर पहुंच जाते हैं जहां 'Item1 == x',' t.Item1 <= x' की सभी और जांच शाखा की भविष्यवाणी को "नहीं लेती" के रूप में सही ढंग से भविष्यवाणी करती है, जो खोज के पूंछ हिस्से को तेज करती है। जाहिर है, कठोर वास्तविकता द्वारा सोच की उस पंक्ति को गलत साबित कर दिया गया है :) – dasblinkenlight

+0

दिलचस्प बात यह है कि 'कुल राशि' के लिए '10,000' या उससे कम के लिए, क्रमबद्ध संस्करण तेजी से प्रदर्शन करता है (बेशक, उन छोटी संख्याओं में तेज़ी से तेज़) (एफवाईआई, आपके कोड में 'var डेटा सूची = नई सूची > (500000) 'प्रारंभिक आकार की क्षमता को हार्ड कोडिंग के बजाय' टोटलकाउंट' के विरुद्ध बाध्य होना चाहिए) –

उत्तर

257

आप अवर्गीकृत सूची सभी tuples स्मृति-आदेश में पहुंचा जा सकता है का उपयोग कर रहे हैं। उन्हें रैम में लगातार आवंटित किया गया है। सीपीयू क्रमशः स्मृति तक पहुंचने से प्यार करते हैं क्योंकि वे अनुमानित रूप से अगली कैश लाइन का अनुरोध कर सकते हैं ताकि जब आवश्यक हो तो यह हमेशा उपस्थित रहेगा।

जब आप सूची को सॉर्ट कर रहे हैं तो आप इसे यादृच्छिक क्रम में डाल दें क्योंकि आपकी सॉर्ट कुंजी यादृच्छिक रूप से जेनरेट की जाती हैं। इसका मतलब है कि स्मृति सदस्यों को स्मृति तक पहुंच अप्रत्याशित है। सीपीयू मेमोरी को प्रीफेच नहीं कर सकता है और ट्यूपल में लगभग हर एक्सेस कैश मिस है।

यह जीसी स्मृति प्रबंधन की एक विशिष्ट लाभ के लिए एक अच्छी उदाहरण है: डाटा संरचनाओं जो एक साथ आवंटित किया गया है और एक साथ इस्तेमाल कर रहे हैं बहुत अच्छी तरह से प्रदर्शन करते हैं। उनके पास संदर्भ का क्षेत्र है।

कैश से जुर्माना इस मामले में सहेजी गई शाखा भविष्यवाणी दंड से अधिक है।

एक struct -tuple स्विच करके देखें। यह प्रदर्शन को पुनर्स्थापित करेगा क्योंकि ट्यूपल सदस्यों तक पहुंचने के लिए रनटाइम पर कोई पॉइंटर-डिफरेंस होने की आवश्यकता नहीं है।

क्रिस सिंक्लेयर टिप्पणियों में नोट करता है कि "10,000 या उससे कम कुल टोटल के लिए, क्रमबद्ध संस्करण तेजी से प्रदर्शन करता है"। इसका कारण यह है एक छोटी सूची सीपीयू कैश में पूरी तरह से फिट बैठता है। मेमोरी एक्सेस अप्रत्याशित हो सकती है लेकिन लक्ष्य हमेशा कैश में होता है। मेरा मानना ​​है कि अभी भी एक छोटा जुर्माना है क्योंकि कैश से भी भार कुछ चक्र लेता है।लेकिन ऐसा कोई समस्या नहीं है क्योंकि सीपीयू एकाधिक बकाया भार को जोड़ सकता है, जिससे थ्रूपुट बढ़ रहा है। जब भी सीपीयू मेमोरी की प्रतीक्षा करता है तो यह अभी भी निर्देश स्ट्रीम में आगे बढ़ेगा जितना कि यह कर सकता है उतने मेमोरी ऑपरेशंस कतार में। इस तकनीक का उपयोग विलंबता को छिपाने के लिए किया जाता है।

इस तरह के व्यवहार से पता चलता है कि आधुनिक CPUs पर प्रदर्शन की भविष्यवाणी करना कितना मुश्किल है। तथ्य यह है कि हम केवल 2x धीमी अनुक्रमिक से यादृच्छिक स्मृति पहुंच में जाने पर मुझे बताते हैं कि स्मृति विलंबता को छिपाने के लिए कवर के तहत कितना चल रहा है। एक स्मृति पहुंच CPU को 50-200 चक्रों के लिए रोक सकती है। यह देखते हुए कि नंबर एक यादृच्छिक मेमोरी एक्सेस पेश करते समय कार्यक्रम को 10x धीमा होने की उम्मीद कर सकता है।

+5

अच्छा कारण सी/सी ++ में जो कुछ भी आप सीखते हैं, वह सी # जैसी भाषा में वर्बैटिम लागू नहीं करता है! – Mehrdad

+33

आप नई सूची का परीक्षण करने से पहले सॉर्ट किए गए डेटा को मैन्युअल रूप से 'नई सूची <टुपल <लम्बी, लंबी, स्ट्रिंग >> (500000)' में एक-एक करके कॉपी करके इस व्यवहार की पुष्टि कर सकते हैं। इस परिदृश्य में, क्रमबद्ध परीक्षण उतना ही तेज़ है जितना अनचाहे वाला, जो इस उत्तर पर तर्क के साथ मेल खाता है। – Bobson

+3

बढ़िया, बहुत बहुत धन्यवाद!मैंने समकक्ष 'टुपल 'संरचना बनाई, और कार्यक्रम ने जिस तरह से भविष्यवाणी की थी, उसका व्यवहार करना शुरू किया: क्रमबद्ध संस्करण थोड़ा तेज़ था। इसके अलावा, अपरिवर्तित संस्करण दो गुना तेजी से बन गया! तो 'स्ट्रक्चर' वाली संख्या 2s अनसुलझा बनाम 1.9 एस क्रमबद्ध हैं। – dasblinkenlight

3

LINQ नहीं जानता कि आपकी सूची सॉर्ट की गई है या नहीं।

चूंकि predicate पैरामीटर के साथ गणना सभी IENumerables के लिए एक्सटेंशन विधि है, मुझे लगता है कि यह यह भी नहीं जानता कि यह कुशल यादृच्छिक पहुंच के साथ संग्रह पर चल रहा है या नहीं। इसलिए, यह बस प्रत्येक तत्व की जांच करता है और उपयोगकर्ता समझाया गया कि प्रदर्शन क्यों कम हो गया।

क्रमबद्ध सरणी (जैसे बाइनरी खोज) के प्रदर्शन लाभ का फायदा उठाने के लिए, आपको थोड़ा और कोडिंग करना होगा।

+5

मुझे लगता है कि आप सवाल गलत समझा: निश्चित रूप से मैं उम्मीद कर रहा था कि 'Count' या' Where' "किसी भी तरह" ऊपर विचार है कि अपने डेटा सॉर्ट हो जाता है पर लेने होता है, और एक सादे के बजाय एक द्विआधारी खोज चलाने "सब कुछ जाँच " खोज। सभी मैं के लिए उम्मीद कर रही थी बेहतर शाखा भविष्यवाणी (मेरे सवाल के अंदर लिंक देखें) की वजह से कुछ सुधार था, लेकिन के रूप में यह पता चला है, संदर्भ के इलाके शाखा भविष्यवाणी बड़ा समय श्रेष्ठ माना जाता। – dasblinkenlight