एक और स्टैक ओवरफ्लो प्रश्न का उत्तर देना ( यह एक ) मैं एक दिलचस्प उप-समस्या पर ठोकर खाई। 6 पूर्णांक की एक सरणी को सॉर्ट करने का सबसे तेज़ तरीका क्या है?
चूंकि प्रश्न बहुत निम्न स्तर का है:
- हम यह नहीं मान सकते कि पुस्तकालय उपलब्ध हैं (और कॉल की अपनी लागत है), केवल सादे सी
- निर्देश पाइपलाइन (एक है कि खाली करने से बचने के लिए बहुत उच्च लागत) हम शायद, शाखाओं को कम करना चाहिए कूदता है, और नियंत्रण के हर दूसरे प्रकार तोड़ने प्रवाह (में अनुक्रम अंक पीछे छिपा उन जैसे
&&
या||
)। - कमरा विवश है और रजिस्टरों को कम करना और मेमोरी का उपयोग एक मुद्दा है, आदर्श रूप में जगह की तरह संभवतः सबसे अच्छा है।
वास्तव में यह प्रश्न एक प्रकार का गोल्फ है जहां लक्ष्य स्रोत की लंबाई को कम करना नहीं है बल्कि निष्पादन समय है। मैं माइकल ज़्राश और उसके सीक्वल द्वारा कोड ऑप्टिमाइज़ेशन की पुस्तक ज़ेन के शीर्षक में उपयोग के रूप में इसे 'ज़िंगन' कोड कहता हूं ।
जैसा कि क्यों यह दिलचस्प है, कई परतें हैं:
- उदाहरण सरल और समझने में आसान है, जिसमें सी कौशल शामिल नहीं है
- यह समस्या के लिए एक अच्छे एल्गोरिदम की पसंद के प्रभावों को दिखाता है, लेकिन कंपाइलर और अंतर्निहित हार्डवेयर के प्रभावों को भी दर्शाता है।
यहां मेरा संदर्भ (भोली, अनुकूलित नहीं) कार्यान्वयन और मेरा परीक्षण सेट है।
#include <stdio.h>
static __inline__ int sort6(int * d){
char j, i, imin;
int tmp;
for (j = 0 ; j < 5 ; j++){
imin = j;
for (i = j + 1; i < 6 ; i++){
if (d[i] < d[imin]){
imin = i;
}
}
tmp = d[j];
d[j] = d[imin];
d[imin] = tmp;
}
}
static __inline__ unsigned long long rdtsc(void)
{
unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
return x;
}
int main(int argc, char ** argv){
int i;
int d[6][5] = {
{1, 2, 3, 4, 5, 6},
{6, 5, 4, 3, 2, 1},
{100, 2, 300, 4, 500, 6},
{100, 2, 3, 4, 500, 6},
{1, 200, 3, 4, 5, 600},
{1, 1, 2, 1, 2, 1}
};
unsigned long long cycles = rdtsc();
for (i = 0; i < 6 ; i++){
sort6(d[i]);
/*
* printf("d%d : %d %d %d %d %d %d\n", i,
* d[i][0], d[i][6], d[i][7],
* d[i][8], d[i][9], d[i][10]);
*/
}
cycles = rdtsc() - cycles;
printf("Time is %d\n", (unsigned)cycles);
}
कच्चे परिणाम
जैसा कि कई प्रकार के संस्करण बड़े होते जा रहे हैं, मैंने उन सभी को एक परीक्षण सूट में इकट्ठा किया जो यहां पाया जा सकता है । उपयोग किए गए वास्तविक परीक्षण केविन स्टॉक की बदौलत ऊपर दिखाए गए लोगों की तुलना में थोड़े कम भोले हैं। आप इसे अपने परिवेश में संकलित और निष्पादित कर सकते हैं। मैं अलग-अलग लक्ष्य वास्तुकला / संकलक के व्यवहार से काफी दिलचस्पी रखता हूं। (ठीक है दोस्तों, इसे उत्तर में रखिए, मैं एक नए परिणाम के प्रत्येक योगदानकर्ता को +1 करूँगा)।
मैंने एक साल पहले डैनियल स्टुट्ज़बाक (गोल्फ के लिए) को जवाब दिया था क्योंकि वह उस समय सबसे तेज़ समाधान के स्रोत पर था (नेटवर्क को छांटना)।
लिनक्स 64 बिट्स, gcc 4.6.1 64 बिट्स, इंटेल कोर 2 डुओ E8400, -ओ 2
- Qsort लाइब्रेरी फ़ंक्शन के लिए सीधे कॉल: 689.38
- Naive कार्यान्वयन (प्रविष्टि प्रकार): 285.70
- निवेशन सॉर्ट (डैनियल स्टुट्ज़बेक): 142.12
- सम्मिलन क्रमबद्ध अनियंत्रित: 125.47
- रैंक ऑर्डर: 102.26
- रजिस्टरों के साथ रैंक ऑर्डर: 58.03
- सॉर्टिंग नेटवर्क (डैनियल स्टुट्ज़बेक): 111.68
- सॉर्टिंग नेटवर्क (पॉल आर): 66.36
- फास्ट स्वैप के साथ छंटनी नेटवर्क 12: 58.86
- सॉर्टिंग नेटवर्क 12 ने स्वैप्ट को पुनः व्यवस्थित किया: 53.74
- सॉर्टिंग नेटवर्क 12 ने सरल स्वैप को फिर से चलाया: 31.54
- पुन: क्रमबद्ध सॉर्टिंग नेटवर्क w / तेज स्वैप: 31.54
- पुन: क्रमबद्ध नेटवर्क W / तेज स्वैप V2: 33.63
- इनबिल्ड बबल सॉर्ट (पाओलो बोन्जिनी): 48.85
- अनियंत्रित प्रविष्टि सॉर्ट (पाओलो बोन्जिनी): 75.30
लिनक्स 64 बिट्स, gcc 4.6.1 64 बिट्स, इंटेल कोर 2 डुओ E8400, -ओ 1
- Qsort लाइब्रेरी फ़ंक्शन के लिए सीधे कॉल: 705.93
- Naive कार्यान्वयन (प्रविष्टि प्रकार): 135.60
- निवेशन सॉर्ट (डैनियल स्टुट्ज़बेक): 142.11
- सम्मिलन क्रमबद्ध अनियंत्रित: 126.75
- रैंक क्रम: 46.42
- रजिस्टर आदेश के साथ रजिस्टर: 43.58
- सॉर्टिंग नेटवर्क (डैनियल स्टुट्ज़बेक): 115.57
- सॉर्टिंग नेटवर्क (पॉल आर): 64.44
- फास्ट स्वैप के साथ छंटनी नेटवर्क 12: 61.98
- सॉर्टिंग नेटवर्क 12 ने स्वैप्ट को फिर से शुरू किया: 54.67
- सॉर्टिंग नेटवर्क 12 ने सरल स्वैप को फिर से चलाया: 31.54
- पुन: क्रमबद्ध छँटाई नेटवर्क w / तेज स्वैप: 31.24
- सॉर्ट किए गए सॉर्टिंग नेटवर्क w / तेज स्वैप V2: 33.07
- इनबिल्ड बबल सॉर्ट (पाओलो बोन्जिनी): 45.79
- अनियंत्रित प्रविष्टि सॉर्ट (पाओलो बोन्जिनी): 80.15
मैंने -O1 और -O2 दोनों परिणामों को शामिल किया क्योंकि आश्चर्यजनक रूप से कई कार्यक्रमों के लिए O2 O1 से कम कुशल है। मुझे आश्चर्य है कि इस विशिष्ट अनुकूलन का क्या प्रभाव है?
प्रस्तावित समाधान पर टिप्पणियाँ
सम्मिलन क्रमबद्ध करें (डैनियल स्टुटज़बक)
जैसा कि अपेक्षित न्यूनतम शाखाओं वास्तव में एक अच्छा विचार है।
सॉर्टिंग नेटवर्क (डैनियल स्टुट्ज़बाक)
प्रविष्टि प्रकार से बेहतर है। मुझे आश्चर्य है कि बाहरी लूप से बचने से मुख्य प्रभाव नहीं मिला। मैंने इसे जांचने के लिए अनियंत्रित सम्मिलन के प्रकार की कोशिश की और वास्तव में हमें लगभग समान आंकड़े मिलते हैं (कोड यहाँ है )।
सॉर्टिंग नेटवर्क (पॉल आर)
अब तक का सबसे अच्छा। मैं जिस वास्तविक कोड का परीक्षण करता था वह यहाँ है । अभी तक नहीं पता कि यह अन्य छँटाई नेटवर्क कार्यान्वयन के रूप में लगभग दो गुना क्यों तेज है। पैरामीटर पासिंग? तेजी से अधिकतम?
फास्ट स्वैप के साथ छंटनी नेटवर्क 12 SWAP
जैसा कि डैनियल स्टुट्ज़बैक ने सुझाव दिया था, मैंने उनके 12 स्वैप छँटाई नेटवर्क को शाखा रहित तेज़ स्वैप (कोड यहाँ है ) के साथ जोड़ा । यह वास्तव में तेज़ है, एक छोटे से मार्जिन के साथ अब तक का सबसे अच्छा (लगभग 5%) जैसा कि 1 कम स्वैप का उपयोग करके उम्मीद की जा सकती है।
यह देखना भी दिलचस्प है कि अगर पीपीसी आर्किटेक्चर पर उपयोग करने वाले सरल की तुलना में शाखाहीन स्वैप बहुत अधिक (4 गुना) कम कुशल लगता है।
लाइब्रेरी क्यूसोर्ट कह रहे हैं
एक और संदर्भ बिंदु देने के लिए मैंने भी कोशिश की जैसा कि लाइब्रेरी कैसोर्ट को कॉल करने का सुझाव दिया गया था (कोड यहाँ है )। जैसा कि अपेक्षित था यह बहुत धीमा है: 10 से 30 गुना धीमा ... जैसा कि नए टेस्ट सूट के साथ स्पष्ट हो गया है, पहली कॉल के बाद मुख्य समस्या पुस्तकालय के शुरुआती भार की लगती है, और यह अन्य के साथ इतनी खराब तुलना नहीं करता है संस्करण। यह मेरे लिनक्स पर 3 से 20 गुना धीमा है। दूसरों द्वारा परीक्षण के लिए उपयोग किए जाने वाले कुछ आर्किटेक्चर पर यह और भी तेज लगता है (मैं वास्तव में उस एक से आश्चर्यचकित हूं, जैसा कि लाइब्रेरी क्युसर्ट एक अधिक महत्वपूर्ण एपीआई का उपयोग करता है)।
रैंक आदेश
रेक्स केर ने एक और पूरी तरह से अलग विधि प्रस्तावित की: सरणी के प्रत्येक आइटम के लिए सीधे अपनी अंतिम स्थिति की गणना करें। यह कुशल है क्योंकि कंप्यूटिंग रैंक ऑर्डर को शाखा की आवश्यकता नहीं है। इस पद्धति का दोष यह है कि यह सरणी की मेमोरी की तीन गुना (रैंक ऑर्डर को संग्रहीत करने के लिए सरणी और चर की एक प्रति) लेता है। प्रदर्शन के परिणाम बहुत ही आश्चर्यजनक (और दिलचस्प) हैं। 32 बिट्स ओएस और इंटेल कोर 2 क्वाड E8300 के साथ मेरे संदर्भ आर्किटेक्चर पर, साइकिल की गिनती 1000 से थोड़ी कम थी (जैसे कि ब्रांचिंग स्वैप के साथ नेटवर्क सॉर्ट करना)। लेकिन जब मेरे 64 बिट्स बॉक्स (Intel Core2 Duo) को संकलित और निष्पादित किया गया तो इसने बहुत बेहतर प्रदर्शन किया: यह अब तक का सबसे तेज़ बना। मुझे आखिरकार सही वजह का पता चला। मेरा 32 बिट्स बॉक्स gcc 4.4.1 और मेरे 64 बिट्स बॉक्स gcc 4.4 का उपयोग करता है।
अपडेट :
जैसा कि ऊपर प्रकाशित आंकड़ों से पता चलता है कि यह प्रभाव अभी भी बाद के संस्करणों के द्वारा बढ़ाया गया था और रैंक ऑर्डर किसी भी अन्य विकल्प की तुलना में लगातार दोगुना हो गया।
सॉर्ट किए गए नेटवर्क 12 को पुन: क्रमबद्ध स्वैप के साथ
4.4.3 gcc के साथ रेक्स केर प्रस्ताव की अद्भुत दक्षता ने मुझे आश्चर्यचकित कर दिया: कैसे 3 बार के साथ एक प्रोग्राम हो सकता है जितना कि मेमोरी उपयोग ब्रांचलेस सॉर्टिंग नेटवर्क की तुलना में तेज हो? मेरी परिकल्पना यह थी कि इसमें लिखने के बाद पढ़ी जाने वाली तरह की निर्भरता कम थी, जिससे x86 के सुपरसक्लेर इंस्ट्रक्शन शेड्यूलर के बेहतर उपयोग की अनुमति मिली। इससे मुझे अंदाजा हो गया: लिखने की निर्भरता के बाद रीड को छोटा करने के लिए रिकॉर्डर स्वैप होता है। और अधिक सीधे शब्दों में कहें: जब आप करते SWAP(1, 2); SWAP(0, 2);
हैं तो आपको पहले स्वैप के लिए दूसरा एक प्रदर्शन करने से पहले समाप्त होने का इंतजार करना होगा क्योंकि दोनों एक आम मेमोरी सेल तक पहुंचते हैं। जब आप SWAP(1, 2); SWAP(4, 5);
प्रोसेसर करते हैं तो दोनों समानांतर में निष्पादित हो सकते हैं। मैंने इसकी कोशिश की और यह उम्मीद के मुताबिक काम करता है, सॉर्टिंग नेटवर्क लगभग 10% तेजी से चल रहा है।
सरल स्वैप के साथ नेटवर्क 12 छंटनी
मूल पद के एक साल बाद स्टीनर एच। गुंडरसन ने सुझाव दिया, कि हमें संकलक को बाहर करने और स्वदेशी कोड को सरल रखने की कोशिश नहीं करनी चाहिए। यह वास्तव में एक अच्छा विचार है क्योंकि परिणामस्वरूप कोड लगभग 40% तेज है! उन्होंने x86 इनलाइन असेंबली कोड का उपयोग करके हाथ से अनुकूलित एक स्वैप भी प्रस्तावित किया जो अभी भी कुछ और चक्रों को छोड़ सकता है। सबसे आश्चर्यजनक (यह प्रोग्रामर के मनोविज्ञान पर संस्करणों का कहना है) यह है कि एक साल पहले स्वैप के उस संस्करण का कोई भी उपयोग नहीं किया था। मैं परीक्षण करने के लिए इस्तेमाल किया कोड यहाँ है । अन्य लोगों ने सी फास्ट स्वैप लिखने के अन्य तरीके सुझाए, लेकिन यह समान प्रदर्शन करता है जो एक सभ्य संकलक के साथ सरल है।
"सर्वश्रेष्ठ" कोड अब इस प्रकार है:
static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x)
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
const int b = max(d[x], d[y]); \
d[x] = a; d[y] = b; }
SWAP(1, 2);
SWAP(4, 5);
SWAP(0, 2);
SWAP(3, 5);
SWAP(0, 1);
SWAP(3, 4);
SWAP(1, 4);
SWAP(0, 3);
SWAP(2, 5);
SWAP(1, 3);
SWAP(2, 4);
SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}
यदि हम मानते हैं कि हमारा परीक्षण सेट (और, हाँ, यह काफी घटिया है, तो यह मात्र लाभ है, जो हम माप रहे हैं, उसे समझना सरल और आसान है), एक प्रकार के लिए परिणामी कोड के चक्रों की औसत संख्या 40 चक्रों से कम है ( 6 परीक्षण निष्पादित किए जाते हैं)। प्रत्येक स्वैप को औसतन 4 चक्रों में रखा जाता है। मैं उस आश्चर्यजनक रूप से तेज कहता हूं। कोई अन्य सुधार संभव है?
__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
क्योंकि rdtsc EDX: EAX में उत्तर डालता है जबकि GCC इसे 64-बिट रजिस्टर में रखता है। आप -O3 पर संकलन करके बग को देख सकते हैं। इसके अलावा पॉल तेज के बारे में मेरी टिप्पणी के नीचे एक तेज स्वैप देखें।
CMP EAX, EBX; SBB EAX, EAX
0 या 0xFFFFFFFF के EAX
आधार पर EAX
बड़ा या छोटा होने पर निर्भर करेगा EBX
। SBB
"उधार के साथ घटाना" है, का प्रतिरूप ADC
("कैरी के साथ जोड़ें"); स्थिति थोड़ा तुम उल्लेख करने के लिए है कैरी बिट। फिर से, मुझे याद है कि ADC
और SBB
पेंटियम 4 बनाम ADD
और पर भयानक विलंबता और थ्रूपुट था SUB
, और अभी भी कोर सीपीयू पर दो बार धीमा था। 80386 के बाद से SETcc
सशर्त-स्टोर और CMOVcc
सशर्त-चाल निर्देश भी हैं, लेकिन वे भी धीमी हैं।
x-y
औरx+y
अतिप्रवाह या अतिप्रवाह का कारण नहीं होगा?