std :: function vs template


161

C ++ 11 के लिए धन्यवाद, हमने std::functionफ़ंक्शनल रैपर के परिवार को प्राप्त किया । दुर्भाग्य से, मैं इन नए परिवर्धन के बारे में केवल बुरी बातें ही सुनता रहता हूं। सबसे लोकप्रिय यह है कि वे बहुत धीमी गति से हैं। मैंने इसका परीक्षण किया और वे वास्तव में टेम्प्लेट की तुलना में चूसते हैं।

#include <iostream>
#include <functional>
#include <string>
#include <chrono>

template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }

float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }

int main() {
    using namespace std::chrono;

    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        calc1([](float arg){ return arg * 0.5f; });
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    return 0;
}

111 एमएस बनाम 1241 एमएस। मुझे लगता है यह है क्योंकि टेम्पलेट्स अच्छी तरह से इनलेट किया जा सकता है, जबकि functionआभासी कॉल के माध्यम से आंतरिक कवर।

जैसा कि मैंने उन्हें देखा है जाहिर तौर पर टेम्प्लेट के अपने मुद्दे हैं:

  • उन्हें हेडर के रूप में प्रदान किया जाना है, जो कुछ ऐसा नहीं है जिसे आप अपनी लाइब्रेरी को बंद कोड के रूप में जारी करते समय करना नहीं चाहते,
  • जब तक समान extern templateनीति पेश नहीं की जाती है, वे संकलन समय को बहुत लंबा कर सकते हैं ,
  • टेम्पलेट की आवश्यकताओं (अवधारणाओं, किसी को?) का प्रतिनिधित्व करने का कोई स्पष्ट तरीका नहीं है (कम से कम मेरे लिए ज्ञात है), किसी भी तरह के फ़नकार से क्या अपेक्षा की जाती है, यह वर्णन करते हुए एक टिप्पणी करें।

क्या मैं यह मान सकता हूं कि functionएस को गुजरने वाले फंक्शंस के वास्तविक मानक के रूप में इस्तेमाल किया जा सकता है , और उन स्थानों पर जहां उच्च प्रदर्शन की उम्मीद है, टेम्पलेट्स का उपयोग किया जाना चाहिए?


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

मेरा कंपाइलर CTP के बिना विजुअल स्टूडियो 2012 है ।


16
उपयोग करें std::functionऔर केवल अगर आपको वास्तव में कॉल करने योग्य वस्तुओं के एक विषम संग्रह की आवश्यकता है (यानी रनटाइम में कोई और भेदभावपूर्ण जानकारी उपलब्ध नहीं है)।
केरेके एसबी

30
आप गलत चीजों की तुलना कर रहे हैं। टेम्पलेट्स का उपयोग दोनों मामलों में किया जाता है - यह " std::functionया टेम्पलेट" नहीं है। मुझे लगता है कि यहाँ मुद्दा केवल एक लंबोदर को std::functionलपेटने में है बनाम एक मेमने को लपेटने में नहीं std::function। फिलहाल आपका सवाल यह पूछने जैसा है कि "क्या मुझे एक सेब, या एक कटोरा पसंद करना चाहिए?"
को ऑर्बिट में

7
चाहे 1ns या 10ns, दोनों कुछ भी नहीं है।
आईपीसी

23
@ipc: 1000% हालांकि कुछ भी नहीं है। जैसे ही ओपी की पहचान होती है, आप जो भी व्यावहारिक उद्देश्य के लिए मापनीयता में आते हैं, उसकी देखभाल करना शुरू करते हैं।
लाइटवेट दौड़ ऑर्बिट में

18
@ipc यह 10 गुना धीमा है, जो बहुत बड़ा है। बेसलाइन की तुलना में गति की आवश्यकता है; यह सोचने के लिए धोखा है कि यह सिर्फ इसलिए नहीं है क्योंकि यह नैनोसेकंड है।
पॉल मंटा

जवाबों:


170

सामान्य तौर पर, यदि आप एक डिजाइन स्थिति का सामना कर रहे हैं जो आपको एक विकल्प देता है, तो टेम्पलेट्स का उपयोग करें । मैंने शब्द डिज़ाइन पर बल दिया क्योंकि मुझे लगता है कि आपको जिस चीज़ पर ध्यान केंद्रित करने की आवश्यकता है वह उपयोग के मामलों std::functionऔर टेम्पलेट्स के बीच का अंतर है, जो बहुत अलग हैं।

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

इसके अलावा, जैसा कि आपने सही ढंग से बताया है, टेम्प्लेट फ़ंक्शंस के लिए कॉल स्टेटिक रूप से हल किए जाते हैं (यानी संकलन के समय), इसलिए कंपाइलर को कोड को ऑप्टिमाइज़ करने और संभवतः इनलाइन करने के लिए सभी आवश्यक जानकारी होती है (जो कॉल के माध्यम से किए जाने पर संभव नहीं होगा। vtable)।

हां, यह सच है कि टेम्पलेट समर्थन सही नहीं है, और C ++ 11 में अभी भी अवधारणाओं के लिए समर्थन की कमी है; हालाँकि, मैं यह नहीं देखता कि std::functionआप उस संबंध में कैसे बचेंगे। std::functionटेम्प्लेट का विकल्प नहीं है, बल्कि डिज़ाइन स्थितियों के लिए एक उपकरण है जहाँ टेम्प्लेट का उपयोग नहीं किया जा सकता है।

एक ऐसा उपयोग मामला तब उत्पन्न होता है जब आपको कॉल - योग्य ऑब्जेक्ट को कॉल करके रन-टाइम पर हल करने की आवश्यकता होती है जो एक विशिष्ट हस्ताक्षर का पालन करता है, लेकिन जिसका ठोस प्रकार संकलन-समय पर अज्ञात है। यह आमतौर पर मामला है जब आपके पास संभावित विभिन्न प्रकार के कॉलबैक का संग्रह होता है , लेकिन जिसे आपको समान रूप से आह्वान करने की आवश्यकता होती है ; पंजीकृत कॉलबैक का प्रकार और संख्या आपके प्रोग्राम की स्थिति और एप्लिकेशन लॉजिक के आधार पर रन-टाइम पर निर्धारित की जाती है। उन कॉलबैक में से कुछ फंक्शनल हो सकते हैं, कुछ सादे कार्य हो सकते हैं, कुछ अन्य कार्यों को कुछ तर्कों से बांधने का परिणाम हो सकते हैं।

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

अंत में, ऐसी अन्य स्थितियां हैं std::functionजो अपरिहार्य हैं, उदाहरण के लिए यदि आप पुनरावर्ती लंबदा लिखना चाहते हैं ; हालाँकि, इन प्रतिबंधों को मेरे द्वारा मान्यता प्राप्त वैचारिक अंतरों की तुलना में तकनीकी सीमाओं से अधिक निर्धारित किया गया है।

योग करने के लिए, डिजाइन पर ध्यान केंद्रित करें और समझने की कोशिश करें कि इन दोनों निर्माणों के लिए वैचारिक उपयोग के मामले क्या हैं। यदि आप उन्हें आपके द्वारा किए गए तरीके की तुलना में डालते हैं, तो आप उन्हें एक ऐसे क्षेत्र में मजबूर कर रहे हैं जिसकी वे संभावना नहीं रखते हैं।


23
मुझे लगता है कि "यह आमतौर पर तब होता है जब आपके पास संभावित विभिन्न प्रकारों के कॉलबैक का संग्रह होता है, लेकिन आपको इसे लागू करने की आवश्यकता होती है;" महत्वपूर्ण बिट है। मेरे अंगूठे का नियम है: " std::functionस्टोरेज अंत और Funइंटरफ़ेस पर टेम्पलेट को प्राथमिकता दें "।
आर। मार्टिनो फर्नांडिस

2
नोट: कंक्रीट के प्रकारों को छिपाने की तकनीक को प्रकार की इरेज़र कहा जाता है (प्रबंधित भाषाओं में टाइप इरेज़र के साथ भ्रमित नहीं होना)। यह अक्सर गतिशील बहुरूपता के संदर्भ में लागू किया जाता है, लेकिन अधिक शक्तिशाली होता है (जैसे unique_ptr<void>कि आभासी विनाश के बिना भी उपयुक्त विध्वंसकों को बुलाना)।
एकमात्रा

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

2
@ecatmur: तो एक तरह से गतिशील बहुरूपता वैचारिक पैटर्न है, जबकि टाइप इरेज़र एक ऐसी तकनीक है जो इसे साकार करने की अनुमति देती है।
एंडी प्रोल

2
@Downvoter: मुझे यह सुनने के लिए उत्सुक होना चाहिए कि आपने इस उत्तर में क्या गलत पाया।
एंडी प्रोल

89

एंडी प्रॉल में अच्छी तरह से डिजाइन के मुद्दे हैं। यह, निश्चित रूप से, बहुत महत्वपूर्ण है, लेकिन मेरा मानना ​​है कि मूल प्रश्न से संबंधित अधिक प्रदर्शन मुद्दों की चिंता है std::function

सबसे पहले, माप तकनीक पर एक त्वरित टिप्पणी: के लिए प्राप्त 11ms calc1का कोई मतलब नहीं है। वास्तव में, उत्पन्न विधानसभा (या विधानसभा कोड डिबगिंग) को देखते हुए, कोई यह देख सकता है कि VS2012 का ऑप्टिमाइज़र यह समझने में काफी चतुर है कि कॉलिंग calc1का परिणाम पुनरावृत्ति से स्वतंत्र है और कॉल को लूप से बाहर ले जाता है:

for (int i = 0; i < 1e8; ++i) {
}
calc1([](float arg){ return arg * 0.5f; });

इसके अलावा, यह पता चलता है कि कॉलिंग calc1का कोई प्रभाव नहीं है और कॉल को पूरी तरह से छोड़ देता है। इसलिए, 111ms वह समय है जो खाली लूप को चलाने के लिए लेता है। (मुझे आश्चर्य है कि आशावादी ने लूप को रखा है।) इसलिए, लूप में समय माप के साथ सावधान रहें। यह उतना सरल नहीं है जितना यह लग सकता है।

जैसा कि यह बताया गया है, ऑप्टिमाइज़र को समझने में अधिक परेशानी होती है std::functionऔर लूप से कॉल को स्थानांतरित नहीं करता है। तो 1241ms के लिए एक उचित माप है calc2

ध्यान दें कि, std::functionविभिन्न प्रकार की कॉल करने योग्य वस्तुओं को संग्रहीत करने में सक्षम है। इसलिए, इसे स्टोरेज के लिए कुछ टाइप-इरेज़र मैजिक करना चाहिए। आम तौर पर, इसका तात्पर्य एक गतिशील मेमोरी आवंटन (डिफ़ॉल्ट रूप से कॉल के माध्यम से new) से है। यह सर्वविदित है कि यह काफी महंगा ऑपरेशन है।

मानक (20.8.11.2.1 / 5) छोटी वस्तुओं के लिए गतिशील मेमोरी आवंटन से बचने के लिए कार्यान्वयन को बढ़ावा देता है, जो, शुक्र है, VS2012 (विशेष रूप से, मूल कोड के लिए) करता है।

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

float a, b, c; // never mind the values
// ...
calc2([a,b,c](float arg){ return arg * 0.5f; });

इस संस्करण के लिए, समय लगभग 16000ms (मूल कोड के लिए 1241ms की तुलना में) है।

अंत में, ध्यान दें कि लैम्ब्डा के जीवनकाल को इसका पता चलता है std::function। इस मामले में, मेमने की एक प्रति संग्रहीत करने के बजाय, std::functionइसे "संदर्भ" संग्रहीत कर सकता है। "संदर्भ" से मेरा मतलब है std::reference_wrapperकि जो आसानी से कार्य करता है std::refऔर std::cref। अधिक सटीक, का उपयोग करके:

auto func = [a,b,c](float arg){ return arg * 0.5f; };
calc2(std::cref(func));

समय लगभग 1860ms तक घट जाता है।

मैंने लिखा है कि कुछ समय पहले:

http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

जैसा कि मैंने लेख में कहा है, तर्क वितर्क C0 11 के खराब समर्थन के कारण VS2010 के लिए लागू नहीं होते हैं। लेखन के समय, VS2012 का केवल एक बीटा संस्करण उपलब्ध था लेकिन C ++ 11 के लिए इसका समर्थन पहले से ही इस मामले के लिए पर्याप्त था।


मुझे वास्तव में यह दिलचस्प लगता है, खिलौना उदाहरणों का उपयोग करके एक कोड गति का प्रमाण बनाना चाहते हैं जो संकलक द्वारा अनुकूलित हो जाते हैं क्योंकि उनके कोई दुष्प्रभाव नहीं होते हैं। मैं कहूंगा कि कुछ वास्तविक / उत्पादन कोड के बिना, इस प्रकार के माप पर शायद ही कोई शर्त लगा सकता है।
गीता

@ गीता: इस उदाहरण में, कोड को दूर करने से रोकने के लिए, calc1एक floatतर्क ले सकता है जो पिछले पुनरावृत्ति का परिणाम होगा। कुछ इस तरह x = calc1(x, [](float arg){ return arg * 0.5f; });। इसके अतिरिक्त, हमें यह सुनिश्चित करना चाहिए कि calc1उपयोग करता है x। लेकिन, यह अभी तक पर्याप्त नहीं है। हमें एक साइड इफेक्ट बनाने की जरूरत है। उदाहरण के लिए, माप के बाद, xस्क्रीन पर मुद्रण । हालांकि, मैं मानता हूं कि टाइमिमग माप के लिए खिलौना कोड का उपयोग हमेशा वास्तविक / उत्पादन कोड के साथ होने वाला सही संकेत नहीं दे सकता है।
कैसियो नेरी

यह मुझे भी लगता है कि बेंचमार्क std :: function object को लूप के अंदर बनाता है, और calc2 को लूप में बुलाता है। इसके बावजूद कि कंपाइलर इसे ऑप्टिमाइज़ कर सकता है या नहीं कर सकता है, (और यह कि कंस्ट्रक्टर vptr को स्टोर करना जितना आसान हो सकता है), मैं ऐसे मामले में अधिक दिलचस्पी लूंगा जहाँ फ़ंक्शन एक बार निर्मित हो, और किसी अन्य फ़ंक्शन को कॉल किया जाए यह एक पाश में है। यानी निर्माण समय (और 'f' और 'calc2 का नहीं) के बजाय कॉल ओवरहेड करें। यदि लूप में (कॉल cal2 में) कॉल किया जाता है, तो एक बार के बजाय, किसी भी उत्थापन से लाभ होगा।
ग्रोगो

बहुत बढ़िया जवाब। 2 चीजें: std::reference_wrapper(मोटे तौर पर टेम्पलेट्स के लिए एक वैध उपयोग का अच्छा उदाहरण ; यह सिर्फ सामान्य भंडारण के लिए नहीं है), और वीएस के आशावादी को एक खाली लूप को छोड़ने में असफल देखना मजेदार है ... जैसा कि मैंने इस जीसीसी बग री केvolatile साथ देखा ।
अंडरस्कोर_ड

37

क्लैंग के साथ दोनों के बीच कोई प्रदर्शन अंतर नहीं है

क्लैंग (3.2, ट्रंक 166872) (-ओ 2 लिनक्स पर) का उपयोग करते हुए, दो मामलों के बायनेरिज़ वास्तव में समान हैं

-मैं पोस्ट के अंत में वापस आऊँगा। लेकिन पहले, gcc 4.7.2:

पहले से ही बहुत सारी जानकारी चल रही है, लेकिन मैं यह बताना चाहता हूं कि calc1 और calc2 की गणना के परिणाम समान नहीं हैं, इन-लाइनिंग आदि के कारण उदाहरण के लिए सभी परिणामों के योग की तुलना करें:

float result=0;
for (int i = 0; i < 1e8; ++i) {
  result+=calc2([](float arg){ return arg * 0.5f; });
}

calc2 के साथ जो बन जाता है

1.71799e+10, time spent 0.14 sec

जबकि calc1 के साथ यह बन जाता है

6.6435e+10, time spent 5.772 sec

यह गति अंतर में ~ 40 का एक कारक है, और मूल्यों में ~ 4 का कारक है। ओपी ने जो पोस्ट किया है उससे बहुत बड़ा अंतर है (दृश्य स्टूडियो का उपयोग करके)। वास्तव में मूल्य का प्रिंट आउट एक अच्छा परिणाम नहीं (जैसा कि नियम) के साथ कोड को हटाने के लिए संकलक को रोकने के लिए एक अच्छा विचार भी है। कैसियो नेरी ने पहले ही अपने जवाब में यह बात कही। ध्यान दें कि परिणाम कितने भिन्न हैं - अलग-अलग गणना करने वाले कोड के गति कारकों की तुलना करते समय किसी को सावधान रहना चाहिए।

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

अगर मैं calc1 और 2 के लिए एक यूजर सप्लाई वैल्यू तर्क जोड़ता हूं, तो calc1 और calc2 के बीच का स्पीड फैक्टर 40 से 5 के कारक पर आ जाता है! दृश्य स्टूडियो के साथ अंतर 2 के एक कारक के करीब है, और क्लैंग के साथ कोई अंतर नहीं है (नीचे देखें)।

इसके अलावा, जैसा कि गुणन तेज हैं, धीमी गति के कारकों के बारे में बात करना अक्सर दिलचस्प नहीं होता है। एक और दिलचस्प सवाल यह है कि आपके कार्य कितने छोटे हैं, और क्या ये वास्तविक कार्यक्रम में अड़चन हैं?

बजना:

क्लैंग (मैंने 3.2 का इस्तेमाल किया) वास्तव में समान बायनेरिज़ का उत्पादन किया जब मैं उदाहरण कोड के लिए calc1 और calc2 के बीच फ्लिप करता हूं (नीचे पोस्ट किया गया)। प्रश्न में पोस्ट किए गए मूल उदाहरण के साथ भी दोनों समान हैं, लेकिन बिना किसी समय के हैं (लूप्स को पूरी तरह से हटा दिया गया है जैसा कि ऊपर वर्णित है)। मेरे संशोधित उदाहरण के साथ, -O2:

निष्पादित करने के लिए सेकंड की संख्या (सर्वश्रेष्ठ 3):

clang:        calc1:           1.4 seconds
clang:        calc2:           1.4 seconds (identical binary)

gcc 4.7.2:    calc1:           1.1 seconds
gcc 4.7.2:    calc2:           6.0 seconds

VS2012 CTPNov calc1:           0.8 seconds 
VS2012 CTPNov calc2:           2.0 seconds 

VS2015 (14.0.23.107) calc1:    1.1 seconds 
VS2015 (14.0.23.107) calc2:    1.5 seconds 

MinGW (4.7.2) calc1:           0.9 seconds
MinGW (4.7.2) calc2:          20.5 seconds 

सभी बायनेरिज़ की गणना के परिणाम समान हैं, और सभी परीक्षणों को एक ही मशीन पर निष्पादित किया गया था। यह दिलचस्प होगा यदि कोई गहरा क्लैंग या वीएस ज्ञान वाला व्यक्ति टिप्पणी कर सकता है कि क्या अनुकूलन किए जा सकते हैं।

मेरा संशोधित परीक्षण कोड:

#include <functional>
#include <chrono>
#include <iostream>

template <typename F>
float calc1(F f, float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

float calc2(std::function<float(float)> f,float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

int main() {
    using namespace std::chrono;

    const auto tp1 = high_resolution_clock::now();

    float result=0;
    for (int i = 0; i < 1e8; ++i) {
      result=calc1([](float arg){ 
          return arg * 0.5f; 
        },result);
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    std::cout << result<< std::endl;
    return 0;
}

अपडेट करें:

Vs2015 जोड़ा गया। मैंने यह भी देखा कि calc1, calc2 में डबल-> फ्लोट रूपांतरण हैं। उन्हें हटाने से दृश्य स्टूडियो के लिए निष्कर्ष नहीं बदलता है (दोनों बहुत तेज हैं लेकिन अनुपात समान है)।


8
जो यकीनन सिर्फ बेंचमार्क को गलत दिखाता है। IMHO का दिलचस्प उपयोग मामला वह है जहां कॉलिंग कोड कहीं और से एक फ़ंक्शन ऑब्जेक्ट प्राप्त करता है, इसलिए कंपाइलर को कॉल का संकलन करते समय std :: फ़ंक्शन की उत्पत्ति का पता नहीं चलता है। यहां, कंपाइलर मुख्य रूप से calc2 इनलाइन का विस्तार करके, इसे कॉल करते समय std :: फ़ंक्शन की संरचना को जानता है। Sep में calc2 'एक्सटर्नल' बनाकर आसानी से तय किया गया। मूल फाइल। फिर आप सेब w / संतरे की तुलना कर रहे हैं; calc2 कुछ कर रहा है calc1 नहीं कर सकता। और, लूप कैल्क के अंदर हो सकता है (एफ के लिए कई कॉल); फ़ंक्शन ऑब्जेक्ट के ctor के आसपास नहीं।
ग्रोगो

1
जब मैं एक उपयुक्त संकलक के पास पहुँच सकता हूँ। अभी के लिए कह सकते हैं कि (क) वास्तविक एसटीडी के लिए ctor :: function कॉल 'new'; (बी) जब लक्ष्य मिलान वास्तविक कार्य होता है तो कॉल ही काफी दुबला होता है; (ग) बाइंडिंग के मामलों में, कोड का एक हिस्सा होता है जो अनुकूलन करता है, जिसे फ़ंक्शन obj में एक कोड ptr द्वारा चुना जाता है, और जो फ़ंक्शन obj (d) 'बाउंड' फ़ंक्शन से डेटा (बाउंड पैरा) उठाता है। उस एडॉप्टर में इनबिल्ट हो, अगर कंपाइलर उसे देख सके।
22 फरवरी को greggo

वर्णित सेटअप के साथ नया उत्तर जोड़ा गया।
ग्रैग्गो

3
BTW बेंचमार्क गलत नहीं है, प्रश्न ("std :: function vs template") केवल उसी कंपार्टमेंट यूनिट के दायरे में मान्य है। यदि आप फ़ंक्शन को किसी अन्य इकाई में स्थानांतरित करते हैं, तो टेम्पलेट अब संभव नहीं है, इसलिए तुलना करने के लिए कुछ भी नहीं है।
rustyx

13

अलग-अलग समान नहीं है।

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

void eval(const std::function<int(int)>& f) {
    std::cout << f(3);
}

int f1(int i) {
    return i;
}

float f2(double d) {
    return d;
}

int main() {
    std::function<int(int)> fun(f1);
    eval(fun);
    fun = f2;
    eval(fun);
    return 0;
}

ध्यान दें कि एक ही फ़ंक्शन ऑब्जेक्ट, funदोनों कॉल के लिए पास किया जा रहा है eval। यह दो अलग-अलग कार्य करता है।

यदि आपको ऐसा करने की आवश्यकता नहीं है, तो आपको उपयोग नहीं करना चाहिए std::function


2
केवल यह बताना चाहते हैं कि जब 'फन = एफ 2' किया जाता है, तो 'फन' ऑब्जेक्ट एक छिपे हुए फंक्शन की ओर इशारा करता है, जो इंट को डबल में कनवर्ट करता है, एफ 2 को कॉल करता है, और डबल रिजल्ट को वापस इंट में कन्वर्ट करता है। (वास्तविक उदाहरण में) , 'f2' उस फंक्शन में इनबिल्ट हो सकता है)। यदि आप एक std :: fun को बाँधते हैं, तो 'फन' ऑब्जेक्ट समाप्त हो सकता है, जिसमें बाध्य मापदंडों के लिए उपयोग किए जाने वाले मान शामिल होते हैं। इस लचीलेपन का समर्थन करने के लिए, 'फ़न' (या init) का एक असाइनमेंट मेमोरी को आवंटित / डील करने में शामिल कर सकता है, और यह वास्तविक कॉल ओवरहेड के बजाय अधिक समय तक ले सकता है।
ग्रोगो

8

आपके पास पहले से ही कुछ अच्छे उत्तर हैं, इसलिए मैं उन्हें विरोधाभास नहीं करने जा रहा हूं, संक्षेप में एसटीडी :: फ़ंक्शन को टेम्प्लेट करने के लिए वर्चुअल फ़ंक्शंस की तुलना करने की तरह है। आपको कभी भी फ़ंक्शंस के लिए वर्चुअल फ़ंक्शंस को "पसंद" नहीं करना चाहिए, बल्कि आप वर्चुअल फ़ंक्शंस का उपयोग करते हैं जब यह समस्या को फिट करता है, तो संकलन के समय से लेकर चलने के समय तक के फैसले। विचार यह है कि एक बिसपोक समाधान (जैसे एक जंप-टेबल) का उपयोग करके समस्या को हल करने के बजाय आप कुछ ऐसा उपयोग करते हैं जो संकलक को आपके लिए अनुकूलन का बेहतर मौका देता है। यदि आप एक मानक समाधान का उपयोग करते हैं, तो यह अन्य प्रोग्रामर को भी मदद करता है।


6

यह उत्तर मौजूदा जवाबों के सेट में योगदान करने के लिए है, जो मुझे लगता है कि std :: फ़ंक्शन कॉल की रनटाइम लागत के लिए अधिक सार्थक बेंचमार्क होना चाहिए।

एसटीडी :: फ़ंक्शन तंत्र को यह प्रदान करने के लिए पहचाना जाना चाहिए: किसी भी कॉल करने योग्य इकाई को स्टैड में बदला जा सकता है :: उपयुक्त हस्ताक्षर का कार्य। मान लें कि आपके पास एक लाइब्रेरी है जो z = f (x, y) द्वारा परिभाषित फ़ंक्शन पर एक सतह फिट बैठता है, तो आप इसे स्वीकार करने के लिए लिख सकते हैं std::function<double(double,double)>, और लाइब्रेरी का उपयोगकर्ता आसानी से किसी भी कॉल करने योग्य इकाई को बदल सकता है; यह एक सामान्य कार्य हो सकता है, एक वर्ग उदाहरण की एक विधि, या लंबोदर, या कुछ भी जो std :: bind द्वारा समर्थित है।

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

मेरे विचार से, यह std के लिए सबसे महत्वपूर्ण उपयोग मामला है: कार्य जहां तक ​​प्रदर्शन का संबंध है: मैं एक कॉल करने की लागत में दिलचस्पी रखता हूं :: एक बार निर्माण होने के बाद कई बार कार्य करता है, और इसकी आवश्यकता है ऐसी स्थिति हो जहां कंपाइलर वास्तव में कहे जा रहे फ़ंक्शन को जानकर कॉल को ऑप्टिमाइज़ करने में असमर्थ हो (यानी आपको एक उचित बेंचमार्क प्राप्त करने के लिए किसी अन्य स्रोत फ़ाइल में कार्यान्वयन को छिपाने की आवश्यकता है)।

मैंने नीचे परीक्षण किया, ओपी के समान; लेकिन मुख्य परिवर्तन हैं:

  1. प्रत्येक मामले में 1 बिलियन बार लूप होता है, लेकिन एसटीडी :: फ़ंक्शन ऑब्जेक्ट केवल एक बार निर्मित होते हैं। मैंने आउटपुट कोड को देखकर पाया है कि वास्तविक std :: function कॉल का निर्माण करते समय 'ऑपरेटर नया' कहा जाता है (शायद तब जब वे अनुकूलित नहीं होते हैं)।
  2. अवांछित अनुकूलन को रोकने के लिए परीक्षण को दो फ़ाइलों में विभाजित किया गया है
  3. मेरे मामले हैं: (ए) फ़ंक्शन इनबिल्ड है (बी) फ़ंक्शन को एक साधारण फ़ंक्शन पॉइंटर द्वारा पास किया जाता है (सी) फ़ंक्शन एक संगत फ़ंक्शन है जिसे एसटीडी के रूप में लपेटा जाता है :: फ़ंक्शन (डी) फ़ंक्शन एक असंगत फ़ंक्शन है जो एसटीडी के साथ संगत बनाया गया है :: बाँध, एसटीडी के रूप में लिपटे :: समारोह

मुझे मिलने वाले परिणाम हैं:

  • मामला (ए) (इनलाइन) 1.3 एनसेक

  • अन्य सभी मामले: 3.3 nsec।

केस (d) थोड़ा धीमा हो जाता है, लेकिन अंतर (लगभग 0.05 nsec) शोर में अवशोषित हो जाता है।

निष्कर्ष यह है कि std :: function तुलनीय ओवरहेड (कॉल टाइम पर) एक फ़ंक्शन पॉइंटर का उपयोग करने के लिए है, तब भी जब वास्तविक फ़ंक्शन के लिए सरल 'बाइंड' अनुकूलन हो। इनलाइन दूसरों की तुलना में 2 ns तेज है, लेकिन यह एक अपेक्षित ट्रेडऑफ है क्योंकि इनलाइन एकमात्र मामला है जो रन के समय में 'हार्ड-वायर्ड' है।

जब मैं एक ही मशीन पर johan-lundberg का कोड चलाता हूं, तो मुझे लगभग 39 nsec प्रति लूप दिखाई दे रहा है, लेकिन वहां के लूप में बहुत कुछ है, जिसमें std :: function का वास्तविक कंस्ट्रक्टर और डिस्ट्रॉक्टर भी शामिल है, जो संभवतः काफी अधिक है चूंकि इसमें एक नया और हटाना शामिल है।

-O2 gcc 4.8.1, x86_64 लक्ष्य (कोर i5) के लिए।

ध्यान दें, संकलक को दो फ़ाइलों में विभाजित किया गया है, ताकि संकलक को उन कार्यों के विस्तार से रोका जा सके जहां उन्हें बुलाया जाता है (एक मामले में जहां यह इरादा है) को छोड़कर।

----- पहला स्रोत फ़ाइल --------------

#include <functional>


// simple funct
float func_half( float x ) { return x * 0.5; }

// func we can bind
float mul_by( float x, float scale ) { return x * scale; }

//
// func to call another func a zillion times.
//
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with a function pointer
float test_funcptr( float (*func)(float), int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with inline function
float test_inline(  int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func_half(x);
    }
    return y;
}

----- दूसरा स्रोत फ़ाइल -------------

#include <iostream>
#include <functional>
#include <chrono>

extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline(  int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );

int main() {
    using namespace std::chrono;


    for(int icase = 0; icase < 4; icase ++ ){
        const auto tp1 = system_clock::now();

        float result;
        switch( icase ){
         case 0:
            result = test_inline( 1e9);
            break;
         case 1:
            result = test_funcptr( func_half, 1e9);
            break;
         case 2:
            result = test_stdfunc( func_half, 1e9);
            break;
         case 3:
            result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
            break;
        }
        const auto tp2 = high_resolution_clock::now();

        const auto d = duration_cast<milliseconds>(tp2 - tp1);  
        std::cout << d.count() << std::endl;
        std::cout << result<< std::endl;
    }
    return 0;
}

रुचि रखने वालों के लिए, यहाँ एडेप्टर 'mul_by' बनाने के लिए बनाया गया है जो फ्लोट (फ्लोट) की तरह दिखता है - इसे 'कहा जाता है' जब बाइंड (mul_by, _1,0.5) के रूप में बनाया जाता है:

movq    (%rdi), %rax                ; get the std::func data
movsd   8(%rax), %xmm1              ; get the bound value (0.5)
movq    (%rax), %rdx                ; get the function to call (mul_by)
cvtpd2ps    %xmm1, %xmm1        ; convert 0.5 to 0.5f
jmp *%rdx                       ; jump to the func

(इसलिए यह थोड़ा और तेज़ हो सकता है अगर मैं बाँध में 0.5f लिखा होता ...) ध्यान दें कि 'x' पैरामीटर% xmm0 में आता है और बस वहीं रहता है।

यहां उस क्षेत्र का कोड है जहां फ़ंक्शन का निर्माण किया जाता है, कॉल करने से पहले test_stdfunc - c ++ फ़ाइल के माध्यम से चलाएं:

movl    $16, %edi
movq    $0, 32(%rsp)
call    operator new(unsigned long)      ; get 16 bytes for std::function
movsd   .LC0(%rip), %xmm1                ; get 0.5
leaq    16(%rsp), %rdi                   ; (1st parm to test_stdfunc) 
movq    mul_by(float, float), (%rax)     ; store &mul_by  in std::function
movl    $1000000000, %esi                ; (2nd parm to test_stdfunc)
movsd   %xmm1, 8(%rax)                   ; store 0.5 in std::function
movq    %rax, 16(%rsp)                   ; save ptr to allocated mem

   ;; the next two ops store pointers to generated code related to the std::function.
   ;; the first one points to the adaptor I showed above.

movq    std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq    std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)


call    test_stdfunc(std::function<float (float)> const&, int)

1
क्लैंग 3.4.1 x64 के साथ परिणाम हैं: (ए) 1.0, (बी) 0.95, (सी) 2.0, (डी) 5.0।
Rustyx

4

मुझे आपके परिणाम बहुत दिलचस्प लगे इसलिए मैंने यह समझने के लिए थोड़ा सा खुदाई की कि क्या चल रहा है। सबसे पहले के रूप में कई अन्य लोगों ने कहा है कि संगणना प्रभाव के परिणाम कार्यक्रम के राज्य कंपाइलर सिर्फ इस दूर का अनुकूलन करेंगे। दूसरी बात यह है कि लगातार 3.3 कॉलबैक के लिए एक आयुध के रूप में दिया जाता है मुझे संदेह है कि अन्य अनुकूलन होंगे। इसे ध्यान में रखते हुए मैंने आपके बेंचमार्क कोड को थोड़ा बदल दिया है।

template <typename F>
float calc1(F f, float i) { return -1.0f * f(i) + 666.0f; }
float calc2(std::function<float(float)> f, float i) { return -1.0f * f(i) + 666.0f; }
int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc2([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

कोड में इस बदलाव को देखते हुए मैंने 4.8 -O3 के साथ संकलित किया और calc1 के लिए 330ms और calc2 के लिए 2702 का समय मिला। इसलिए टेम्पलेट का उपयोग 8 गुना तेज था, यह संख्या मुझे संदिग्ध लग रही थी, 8 की शक्ति की गति अक्सर संकेत देती है कि संकलक के पास कुछ वेक्टर है। जब मैंने टेम्प्लेट संस्करण के लिए उत्पन्न कोड को देखा तो यह स्पष्ट रूप से अस्वीकृत हो गया था

.L34:
cvtsi2ss        %edx, %xmm0
addl    $1, %edx
movaps  %xmm3, %xmm5
mulss   %xmm4, %xmm0
addss   %xmm1, %xmm0
subss   %xmm0, %xmm5
movaps  %xmm5, %xmm0
addss   %xmm1, %xmm0
cvtsi2sd        %edx, %xmm1
ucomisd %xmm1, %xmm2
ja      .L37
movss   %xmm0, 16(%rsp)

जहाँ std :: function संस्करण नहीं था। यह मेरे लिए समझ में आता है, क्योंकि टेम्पलेट के साथ कंपाइलर को पता है कि फ़ंक्शन पूरे लूप में कभी नहीं बदलेगा, लेकिन एसटीडी के साथ :: इसमें पास होने वाला फ़ंक्शन बदल सकता है, इसके लिए वेक्टर नहीं किया जा सकता है।

इससे मुझे यह देखने के लिए कुछ और प्रयास करना पड़ा कि क्या मैं std :: function संस्करण पर समान अनुकूलन करने के लिए कंपाइलर प्राप्त कर सकता हूं। एक फ़ंक्शन में पास होने के बजाय मैं एक वैश्विक संस्करण के रूप में एक std :: function बनाता हूं, और इसे कॉल किया जाता है।

float calc3(float i) {  return -1.0f * f2(i) + 666.0f; }
std::function<float(float)> f2 = [](float arg){ return arg * 0.5f; };

int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc3([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

इस संस्करण के साथ हम देखते हैं कि संकलक ने अब कोड को उसी तरह से वेक्टर किया है और मुझे समान बेंचमार्क परिणाम मिलते हैं।

  • साँचा: 330 मी
  • std :: function: 2702ms
  • वैश्विक एसटीडी :: समारोह: 330ms

तो मेरा निष्कर्ष एक std :: function बनाम टेम्पलेट टेम्पलेट की कच्ची गति बहुत अधिक है। हालाँकि यह आशावादी के काम को और अधिक कठिन बना देता है।


1
पूरे बिंदु एक पैरामीटर के रूप में एक फ़नकार को पारित करना है। आपका calc3मामला कोई मतलब नहीं है; calc3 को अब f2 कॉल करने के लिए हार्डकोड किया गया है। बेशक इसे अनुकूलित किया जा सकता है।
रस्टेक्स

वास्तव में, यह वही है जो मैं दिखाने की कोशिश कर रहा था। वह कैल्क 3 टेम्पलेट के बराबर है, और उस स्थिति में प्रभावी रूप से एक टेम्पलेट की तरह एक संकलन समय का निर्माण होता है।
जोशुआ रिटरमैन
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.