एक राय है कि C की तुलना में C ++ में ध्यान देने योग्य ओवरहेड है और इसलिए यह धीमा है। इसके अतिरिक्त, ऐसे लेख भी हैं जिनमें जावा और सी # जैसी जस्ट-इन-टाइम संकलन (जेआईटी) भाषाओं की गति का लाभ दिखाया गया है। हम उन लोगों को अंतिम छोड़ देंगे जो उन्हें उपवास मानते हैं, लेकिन हम बताएंगे कि ऐसा क्यों नहीं है। और हम C और C ++ की तुलना डाटा सर्च टास्क के उदाहरण से करते हैं।
डेटा खोजने का कार्य अक्सर इसमें पाया जाता है: वेब सेवाएँ, डेटाबेस प्रबंधन प्रणाली (DBMS), भू-खोज और विश्लेषण।
सबसे पहले, स्पष्टीकरण की सादगी के लिए, हम 10,000,000 तत्वों (संरचनाओं) की एक सरणी के माध्यम से तत्वों की खोज करने की समस्या को उठाते हैं, जिसमें मानों की श्रेणी के साथ 5 फ़ील्ड होते हैं: amount_of_money (0-1000000), लिंग (0-1), आयु (0-100), कोड (0-1000000), ऊंचाई (0-300)। और अगले लेखों में हम समाधान में सूचकांक खोज जोड़ेंगे।
हम MSVC11 (MSVS2012) और GCC 4.7.2 के तहत क्रॉस-प्लेटफ़ॉर्म लिखेंगे, और उनमें आंशिक रूप से लागू C ++ 11 मानक का उपयोग करेंगे।
1. सी में समाधान
C में इस समस्या का सबसे सरल समाधान है बिट फ़ील्ड्स की संरचना बनाना जो 8 बाइट्स लेता है (एक सामान्य नियम के रूप में,
#pragma pack(push,1)
निर्देश की अनुपस्थिति में, फ़ील्ड उनके मूल प्रकारों के आकार की सीमाओं को पार नहीं कर सकती हैं, हमारे मामले में - 32 बिट्स)
enum T_field_enum { amount_of_money_e, gender_e, age_e, code_e, height_e, last_e }; struct T_cash_account_row {
इन तत्वों में से 10,000,000 के लिए मेमोरी आवंटित करें:
const size_t c_array_size = 10000000; struct T_cash_account_row *const array_ptr = ( struct T_cash_account_row *)calloc(c_array_size, sizeof(struct T_cash_account_row)); if (array_ptr == NULL) { printf ("calloc error\n"); exit(1); }
चक्र में, शर्त द्वारा निर्दिष्ट सीमाओं के भीतर यादृच्छिक मूल्यों के साथ भरें:
static inline struct T_cash_account_row generate_row() { struct T_cash_account_row cash_account_row; cash_account_row.age = rand() % 100; cash_account_row.amount_of_money = (rand() % 1000)*(rand() % 1000); cash_account_row.code = (rand() % 1000)*(rand() % 1000); cash_account_row.gender = rand() % 2; cash_account_row.height = rand() % 300; return cash_account_row; } for(i = 0; i < c_array_size; ++i) array_ptr[i] = generate_row();
खोज फ़िल्टर-फ़िल्टर संरचना बनाएँ, जहाँ
last_e
एक स्ट्रिंग में फ़ील्ड्स की संख्या के साथ एक गणना है
(C ++ 03 7.2 गणन घोषणाएँ) :
struct T_range_filters { struct T_cash_account_row begin, end; unsigned char use_filter[last_e]; };
यहां
use_filter[]
उपयोग यह बताने के लिए किया जाता है कि किसी दिए गए फील्ड की स्थिति को फ़िल्टर करना है या नहीं।
और दिए गए फ़ील्ड को लूप में सरणी के सभी तत्वों से गुजरते हुए चेक करके खोजें:
static inline unsigned char test_predicate(struct T_cash_account_row const*const row, struct T_range_filters const*const range_filters) { return (!range_filters->use_filter[amount_of_money_e] || (row->amount_of_money >= range_filters->begin.amount_of_money && row->amount_of_money <= range_filters->end.amount_of_money)) && (!range_filters->use_filter[gender_e] || (row->gender >= range_filters->begin.gender && row->gender <= range_filters->end.gender)) && (!range_filters->use_filter[age_e] || (row->age >= range_filters->begin.age && row->age <= range_filters->end.age)) && (!range_filters->use_filter[code_e] || (row->code >= range_filters->begin.code && row->code <= range_filters->end.code)) && (!range_filters->use_filter[height_e] || (row->height >= range_filters->begin.height && row->height <= range_filters->end.height)); } static inline size_t search(struct T_cash_account_row const*const array_ptr, const size_t c_array_size, struct T_cash_account_row *const result_ptr, struct T_range_filters const*const range_filters) { size_t result_size = 0; size_t i; for(i = 0; i < c_array_size; ++i) { if(test_predicate(array_ptr + i, range_filters)) result_ptr[result_size] = array_ptr[i], ++result_size; } return result_size; }
GitHub.com पर पूरे कामकाजी कोड से लिंक करें
ऐसा लगता है कि यहां आप अभी भी गति कर सकते हैं और अनुक्रमणिका के बिना पूर्ण पास के साथ अनुकूलन कर सकते हैं?
- लूप स्थिति के साथ तुलना की संख्या को कम करने के लिए लूप का विस्तार करें और एक पुनरावृत्ति में कई फ़िल्टरिंग test_predicate करें? - यह हमारे लाइन की 5 से 15 तुलनाओं की तुलना में बहुत छोटा है, जो टेस्ट किए गए फ़ंक्शन में हमारी पहुंच की तुलना में है और रैम तक पहुंचने की तुलना में है।
- प्रीफेटिंग-कैश करते हैं? - यह C और C ++ दोनों में संभव है, लेकिन हमारे कार्य के ढांचे में, यह बहुत कम करेगा, क्योंकि एलएलसी (L3) में जितनी हो सकेगी उतनी खोज करेंगे, और 80 एमबी की पूरी सरणी वैसे भी नहीं हो पाएगी।
- SSE: CMPSS, COMISS, UCOMISS से वेक्टर तुलना कमांड का उपयोग करें? - यह C और C ++ दोनों में संभव है। लेकिन यह अनुकूलन x86 / x64 के अलावा प्रोसेसर पर पोर्टेबल नहीं है, जैसे ARM या पावर [PC]।
- आप C और C ++ में कंपाइलर और PGO ऑप्टिमाइज़ेशन कुंजी का उपयोग कर सकते हैं। पीजीओ किसी भी मामले में एक समझौता है, जैसा कि अनुकूलित कोड प्रोग्राम निष्पादन के अन्य तरीकों के एक तरीके के लिए बनाया गया है, अर्थात। कुछ इनपुट के साथ यह तेज़ होगा, और कुछ धीमे के साथ। मैं आपको दिखाऊंगा कि प्रत्येक संभावित निष्पादन पथ के लिए अनुकूलित कोड कैसे बनाया जाए, और केवल गति के लिए कार्यक्रम के सबसे महत्वपूर्ण हिस्से में।
सी-एंड में यह निम्न-स्तरीय (गैर-वास्तुशिल्प) अनुकूलन, और इनमें से कोई भी अनुकूलन C ++ में लागू होता है।
2. C और C ++ में एक और अनुकूलन कैसा दिखता है
- सबसे पहले, सी में ऊपर दिए गए समाधान आसानी से बिना किसी बदलाव के सी ++ कंपाइलर पर संकलित करते हैं, क्योंकि ज्यादातर मामलों में, पिछड़ी संगतता होती है।
C: ideone.com पर एक ऑनलाइन कंपाइलर में परिणाम
एक ऑनलाइन C ++ 11 संकलक में परिणाम: ideone.com
मैंने यादृच्छिक बीज गणना पर टिप्पणी की
विभिन्न प्रोग्राम लॉन्च के परिणामों की तुलना करने के लिए। लेकिन आप इसे अनसुना कर सकते हैं और सुनिश्चित कर सकते हैं कि निरपेक्ष क्रम परिवर्तन हो, लेकिन मेरी आशाओं से सापेक्ष त्वरण लगभग समान है। - दूसरे, विशेष रूप से चालाक सी-डेवलपर्स एक और अनुकूलन की पेशकश कर सकते हैं - खोज स्थितियों की संख्या के प्रत्येक संस्करण के लिए test_predicate / खोज फ़ंक्शंस के 2 ^ 5 = 32 वेरिएंट बनाएं, एक सरणी में उन्हें पॉइंटर्स सहेजें और रनटाइम के लिए आवश्यक रूपांतरों का चयन करें खोज करते हैं। यह तुलनाओं की संख्या को काफी कम कर देगा।
मान लीजिए 5 और आयु के 2 क्षेत्रों में एक खोज की स्थिति आ गई है: कोड और। तब हम
search_12()
फ़ंक्शन को कॉल करते हैं:
static inline unsigned char test_predicate_12(struct T_cash_account_row const*const __restrict row, struct T_range_filters const*const __restrict range_filters) { return (row->age >= range_filters->begin.age && row->age <= range_filters->end.age) && (row->code >= range_filters->begin.code && row->code <= range_filters->end.code); } static inline size_t search_12(struct T_cash_account_row const*const __restrict array_ptr, const size_t c_array_size, struct T_cash_account_row *const __restrict result_ptr, struct T_range_filters const*const __restrict range_filters) { size_t result_size = 0; size_t i; for(i = 0; i < c_array_size; ++i) { if(test_predicate_12(array_ptr + i, range_filters)) result_ptr[result_size] = array_ptr[i], ++result_size; } return result_size; }
हालांकि इस समारोह में परिस्थितियों की संख्या 15 प्रारंभिक लोगों से घटकर 4 हो गई है। वास्तव में, सी तुलना पर समाधान संस्करण में, 15 में से केवल 9 को निष्पादित किया गया था, 2 क्षेत्रों में खोज
test_predicate
लिए, यह
test_predicate
फ़ंक्शन:
github.com के disassembler में देखा जा सकता है। ऐसा इसलिए हुआ क्योंकि प्रत्येक तुलना के उपयोग के
use_filter[] == false
अगले क्षेत्र में तुलना करने के लिए एक सशर्त संक्रमण था। यानी इन 4 तुलनाओं के अलावा,
use_filter[]
साथ केवल 5 और तुलनाएँ की गईं।
यहां वर्णित समाधान, उदाहरण के लिए, 5 के 2 क्षेत्रों में खोज के लिए, 1.3 गुना का त्वरण देगा। बुरा नहीं है, लेकिन एक छोटी सी समस्या है - सी-डेवलपर्स को इन 32 कार्यों को मैन्युअल रूप से बनाना होगा, कहीं भी सभी विकल्पों की कोशिश करने और नंबर और फ़ील्ड नामों में किसी भी परिवर्तन के लिए आवश्यक कार्यों को बदलने की गलती करना। ठीक है, अगर आपको 10 फ़ील्ड वाली तालिका के लिए समान समाधान की आवश्यकता है, तो आपको 1024 फ़ंक्शन बनाने होंगे। सामान्य तौर पर, कोड को अंतिम रूप देते समय एक परी कथा!
इसके अतिरिक्त, गैर-तुच्छ तुलनाओं के साथ प्रकार फ़ील्ड जोड़ते समय भ्रम पैदा किया जाता है, जैसे कि स्ट्रिंग
char[]
strcmp()
माध्यम से तुलना की
char[]
। C ++ में, यह एक अधिभार तुलना ऑपरेटर के साथ एक कस्टम प्रकार बनाकर हल किया जाता है। (C ++ में
मौलिक प्रकारों के लिए ऑपरेटरों को अधिभारित नहीं किया जा सकता है - ऑपरेटर मापदंडों में से एक उपयोगकर्ता वर्ग होना चाहिए।)
और C ++ में स्वचालित रूप से अनुकूलित कार्यों की आवश्यक संख्या बनाने का कार्य आसानी से टेम्पलेट के अनियंत्रित होने से हल हो जाता है।
यह किसी को लग सकता है कि यह पूरी तरह से सी और रन-टाइम में हल किया जा सकता है, और संकलन-समय में अनुकूलित 32 - 1024 कार्यों को ब्लॉक करने की आवश्यकता नहीं है। हमारे मामले में 5 की संख्या के बराबर संख्या वाले फ़ंक्शन के लिए व्यूअर की एक सरणी बनाएं, और प्रत्येक खोज के लिए इस खोज क्वेरी के लिए उपयोग की जाने वाली शर्तों के साथ केवल उन फ़ंक्शन के साथ प्रत्येक एरे को भरें। और अंत में एक फ़ंक्शन को एक पॉइंटर जोड़ें जो 1 (सच्चा) देता है। और इनमें से प्रत्येक फ़ंक्शन को उसी प्रकार के फ़ंक्शन के एक सरणी के लिए एक पॉइंटर प्राप्त होता है, और अगले फ़ंक्शन का सूचकांक। मैं निराशाजनक हूं, लेकिन इस मामले में फ़ंक्शन एम्बेडेड नहीं होंगे (इनलाइन), और उन्हें कॉल करना सशर्त शाखाओं के साथ तुलना की तुलना में तेज़ नहीं है।
यहाँ C:
GitHub.com में इस रन-टाइम समाधान का एक कार्यशील संस्करण है
जैसा कि आप MSVC में देख सकते हैं, गति 74 ms से 84 ms तक गिर गई। और जीसीसी में और भी अधिक - 117ms तक। सी में, ऐसा अनुकूलन संभव नहीं है, लेकिन बड़ी संख्या में कार्यों के निर्माण के माध्यम से केवल अनुकूलन संभव है।
3. C ++ में समाधान
टेम्प्लेट का प्रचार एक अन्य टेम्पलेट द्वारा एक टेम्प्लेट का एक उदाहरण (तात्कालिकता) बनाकर किया जाता है, जिसमें टेम्प्लेट पैरामीटर का मूल्य निर्माता की तुलना में एक कम होता है। और 0 के पैरामीटर मान वाले टेम्पलेट के लिए, हम एक खाली विशेषज्ञता बनाते हैं जो कुछ भी नहीं करता है। नतीजतन, पैरामीटर एन के साथ टेम्पलेट के प्रचार को त्वरित करते हुए, हमें एन - अनवांटेड टेम्पलेट के उदाहरण मिलते हैं, जिनमें से प्रत्येक में अगले टेम्पलेट उदाहरण को कॉल करने का निर्माणकर्ता या
inline
कथन कहा जाता है। इस प्रचार में, टेम्पलेट फ़ंक्शंस और टेम्प्लेट क्लास दोनों भाग ले सकते हैं।
टेम्प्लेट के तर्क से खुद को बढ़ावा देने के लिए, हम एक टेम्प्लेट प्रमोशन क्लास बनाएंगे। एक पैरामीटर के साथ यह उस नंबर को लेगा जिसके द्वारा इसे अनवांट करना आवश्यक है, और दूसरे पैरामीटर के साथ यह टेम्प्लेट को लेगा, जिसे अनवांटेड करने की आवश्यकता है:
अब एक आधार सार खोज वर्ग बनाएँ। हम इसमें से एक टेम्पलेट चाइल्ड क्लास इनहेरिट करेंगे, जो कि टेम्प्लेट पैरामीटर के साथ 32-बिट
unsigned int
मान लेता है, जिनमें से प्रत्येक का अर्थ होगा कि संबंधित फ़िल्टर का उपयोग करना है या नहीं:
क्योंकि टेम्पलेट पैरामीटर
index_pred
और enumerations
amount_of_money_e, gender_e …
संकलन चरण में जाना जाता है, संकलक हमेशा की तरह कुछ शर्तों को पूरा करेगा। वास्तव में, हम कंपाइलर को हमारे कार्यक्रम को अनुकूलित करने में मदद कर रहे हैं। इसमें सबसे महत्वपूर्ण निर्णय है!
अब दिखाते हैं कि यह टेम्प्लेट चाइल्ड क्लास
template<unsigned index_pred> struct T_custom_filter
32 कक्षाओं में कैसे
template<unsigned index_pred> struct T_custom_filter
। आइए उनमें से प्रत्येक की 32 वस्तुओं को बनाएं और उन पर आधार प्रकार के
std::array<>
स्थिर सरणी
std::array<>
स्टोर करें। और रनटाइम के दौरान, हम खोज स्थितियों के आधार पर बहुधा रूप से इच्छित वस्तु का उल्लेख करेंगे:
class T_optimized_search {
यहां,
unsigned get_index_pred((T_range_filters const*const __restrict range_filters)
दिए गए
range_filters
खोज स्थिति डेटा के लिए आवश्यक खोज ऑब्जेक्ट की सूचकांक संख्या देता है।
सी में समाधान के रूप में उसी तरह से उपयोग किया जाता है:
T_optimized_search optimized_search;
यहाँ C में दो
test_predicate
कार्यों के disassembler कोड की तुलना की गई है और C ++ में अनुकूलित किया गया है जो MSVC11 (MSVS 2012) पर संकलित है, मेरी टिप्पणियों के साथ - यह अंतर स्पष्ट रूप से दिखाई देता है जब
TortoiseDiff के माध्यम से देखा जाता है।
GitHub.com के लिए लिंक लिंक करें
हम देखते हैं कि 15 तुलनाओं में से 9 को हमारी खोज स्थितियों के तहत निष्पादित किया जाता है, केवल 4 तुलनाएं शेष हैं - सीएमपी कोडांतरक कमांड।
"मेरी टिप्पणियों के साथ कछुआ से छूट की एक तस्वीर"

वास्तव में, टेम्प्लेट की सहायता से, हमने प्रत्येक फिल्टर का उपयोग करने के लिए लूप से बाहर की परीक्षा में लिया। और लूप के अंदर हमें संकलन-समय में उपयोग किए जाने वाले
use_filter[]
फिल्टर का मान मिला, जिसने कंपाइलर को अनुकूलन के दौरान उन्हें बाहर करने की अनुमति दी। यानी यह अनुकूलन चक्र से बाहर तक की गणना या चेक को हटाने के सभी समान मामलों पर लागू होता है।
सी ++ उदाहरण में, मैंने *const
पॉइंटर का उपयोग करके फ़ंक्शन को पास करने के सी-स्टाइल तरीके का उपयोग किया, ताकि सी और सी ++ के बीच के अंतर में, मैं उन परिवर्तनों को देखता हूं जो केवल चर्चा के तहत अनुकूलन को प्रभावित करते हैं। हालाँकि, C ++ - शैली में इंटरफेस का उपयोग करते हुए, फ़ंक्शन & लिंक के माध्यम से पैरामीटर ले सकता है, जो * के बाद भूल जाने की संभावना को समाप्त करता है और यह कुछ हद तक कम है। लेकिन Google C ++ स्टाइल गाइड , const&
निरंतर लिंक द्वारा अपरिवर्तनीय मापदंडों को पारित करने की सलाह देता है, और निरंतर पॉइंटर *const
कॉन्स्टेबल द्वारा *const
। यदि कोड इस शैली में लिखा गया है, तो आपके पास किसी अन्य फ़ंक्शन में पारित किए गए अपने चरों के परिवर्तन (या परिवर्तन नहीं) पर पूर्ण नियंत्रण है - अर्थात यदि आप मूल्य से गुजरते हैं
void func(int const& a, int *b) {}
तब कंपाइलर एक त्रुटि यह कहते हुए फेंक देगा कि फ़ंक्शन आपके पैरामीटर को बदलना चाहता है b। टीडीडी परीक्षण के माध्यम से विकसित करते समय यह विशेष रूप से महत्वपूर्ण है, जब बाहरी परीक्षण कॉल कठोरता से इंटरफ़ेस प्रारूप सेट करते हैं, और इस मामले में, बाहरी परीक्षणों में ऐसी कॉल फ़ंक्शन डेवलपर को बताएगी कि बी - बदला नहीं जा सकता।
और अगर हम पॉइंटर से गुजरते हैं (या पता लेकर):
void func(int const& a, int *b) {}
यह त्रुटियों के बिना संकलित है। और यहां तक कि फ़ंक्शन कॉल से यह हमारे लिए स्पष्ट है कि फ़ंक्शन द्वारा चर में परिवर्तन नहीं होता है, और चर b
बदलता है। और टीडीडी के मामले में, हम इस तथ्य के बारे में बात कर रहे हैं कि डेवलपर को पॉइंटर द्वारा b
लेना चाहिए, और इसलिए इसे बदलना होगा। और उसे निरंतर संदर्भ या मूल्य से स्वीकार करना होगा, और इसके बाहरी मूल्य को बदलने में सक्षम नहीं होगा।
लेकिन सी में, जहां लिंक नहीं हैं, यह दृष्टिकोण संभव नहीं है, क्योंकि यदि फ़ंक्शन हमेशा केवल पॉइंटर द्वारा स्वीकार करता है, तो कॉलर की ओर से यह गारंटी देना असंभव है कि उन्हें संशोधित नहीं किया जा सकता है, और उपयोगकर्ता प्रकार चर के मूल्य से गुजरने से महत्वपूर्ण ओवरहेड हो सकता है।
4. निष्कर्ष
यहां C ++:
GitHub.com में इस समाधान का पूरी तरह से काम करने वाला संस्करण है
मेरे पास GCC4.7.2 पर -O3 -march = देशी, CPUCore i5 K750 कीज़ और एक्सके फ़ाइल साइज़ 74K है जिसका परिणाम यह है:
उत्पन्न पंक्तियाँ: 10,000,000
C ++ - खोज रहा है ...
C ++ - अनुकूलित खोज में 0.061000 सेकंड लगे।
मिली पंक्तियाँ: 38
सी-सर्चिंग ...
सी-सर्च में 0.089000 सेकंड लगे।
C ++ की तुलना में C ++ तेज: 1.459016 बार
मिली पंक्तियाँ: 38
और चाबियाँ / O2 / Ob2 / Oi, CPU Core i5 K750 और 138K के exe फ़ाइल के आकार के साथ MSVC11 (MSVS2012) पर, परिणाम है:
उत्पन्न पंक्तियाँ: 10,000,000
C ++ - खोज रहा है ...
C ++ - अनुकूलित खोज में 0.056000 सेकंड लगे।
मिली पंक्तियाँ: 38
सी-सर्चिंग ...
सी-सर्च में 0.074000 सेकंड लगे।
C ++ की तुलना में C ++ तेज: 1.321429 बार
मिली पंक्तियाँ: 38
जैसा कि हम देख सकते हैं, निष्पादन का समय 74ms से 56ms तक गिर गया, अर्थात। गति
1.3 गुना बढ़ गई। सिद्धांत रूप में, बुरा नहीं है।
सिर्फ
1.3 बार? और पूर्ण-पास की खोज के लिए
3.5 - 5.3 बार त्वरण के बारे में क्या कोई विचार है?
निष्कर्ष - संकलनकर्ता को संकलन के समय जितना अधिक पता होगा, उतना ही वह कार्यक्रम का अनुकूलन करने में सक्षम होगा। और इस में, टेम्पलेट उसे और कुछ नहीं की तरह मदद करते हैं।
वैसे, यह अनुकूलन जावा और सी # में लागू नहीं है, क्योंकि जेनेरिक में, एक मान के साथ एक पैरामीटर का उपयोग करना संभव नहीं है, न कि एक प्रकार।
अगले लेख में, 3.5 - 5.3 और फिर भी अनुक्रमणिका के बिना त्वरण के साथ
एक कट्टर समाधान। लेकिन समाधान का उपयोग इंडेक्स खोज में आगे किया जाएगा।