2015-11-17 8 views
41

मैं स्विफ्ट में कुछ प्रदर्शन-महत्वपूर्ण कोड लिख रहा हूं। सभी ऑप्टिमाइज़ेशन को लागू करने के बाद, मैं इंस्ट्रूमेंट्स में एप्लिकेशन के बारे में सोच सकता हूं और प्रोफाइलिंग कर सकता हूं, मुझे एहसास हुआ कि सीपीयू चक्रों का विशाल बहुमत फ्लोट के सरणी पर map() और reduce() संचालन करने में व्यतीत होता है। तो, यह देखने के लिए कि क्या होगा, मैंने map और reduce के सभी उदाहरणों को अच्छी पुरानी for लूप के साथ बदल दिया। और मेरे आश्चर्य के लिए ... for लूप बहुत तेज थे!स्विफ्ट प्रदर्शन: लूप के लिए मानचित्र() और कम() बनाम

इससे थोड़ा परेशान, मैंने कुछ मोटे बेंचमार्क करने का फैसला किया।

// Populate array with 1,000,000,000 random numbers 
var array = [Float](count: 1_000_000_000, repeatedValue: 0) 
for i in 0..<array.count { 
    array[i] = Float(random()) 
} 
let start = NSDate() 
// Construct a new array, with each element from the original multiplied by 5 
let output = array.map({ (element) -> Float in 
    return element * 5 
}) 
// Log the elapsed time 
let elapsed = NSDate().timeIntervalSinceDate(start) 
print(elapsed) 

और बराबर for पाश कार्यान्वयन: 20.1 सेकंड:

var output = [Float]() 
for element in array { 
    output.append(element * 5) 
} 

map के लिए औसत निष्पादन समय एक परीक्षण में, मैं map वापसी तो जैसे कुछ साधारण अंकगणित प्रदर्शन के बाद मंगाई की एक सरणी था। for लूप के लिए औसत निष्पादन समय: 11.2 सेकंड। परिणाम फ़्लोट्स के बजाय इंटेजर्स का उपयोग कर समान थे।

मैंने स्विफ्ट के reduce के प्रदर्शन का परीक्षण करने के लिए एक समान बेंचमार्क बनाया। इस बार, reduce और for लूप्स ने एक बड़े सरणी के तत्वों को संक्षेप में लगभग समान प्रदर्शन प्राप्त किया। लेकिन जब मैं पाश परीक्षण 100,000 बार इस तरह:

// Populate array with 1,000,000 random numbers 
var array = [Float](count: 1_000_000, repeatedValue: 0) 
for i in 0..<array.count { 
    array[i] = Float(random()) 
} 
let start = NSDate() 
// Perform operation 100,000 times 
for _ in 0..<100_000 { 
    let sum = array.reduce(0, combine: {$0 + $1}) 
} 
// Log the elapsed time 
let elapsed = NSDate().timeIntervalSinceDate(start) 
print(elapsed) 

बनाम:

for _ in 0..<100_000 { 
    var sum: Float = 0 
    for element in array { 
     sum += element 
    } 
} 

जबकि for पाश (जाहिरा तौर पर) 0.000003 सेकंड लेता है reduce विधि 29 सेकंड लेता है।

स्वाभाविक रूप से मैं एक कंपाइलर अनुकूलन के परिणामस्वरूप उस अंतिम परीक्षण को नजरअंदाज करने के लिए तैयार हूं, लेकिन मुझे लगता है कि यह कुछ अंतर्दृष्टि दे सकता है कि कंपाइलर स्विफ्ट के अंतर्निर्मित सरणी विधियों बनाम लूप के लिए अलग-अलग कैसे अनुकूलित करता है। ध्यान दें कि सभी परीक्षणों को 2.5 गीगाहर्ट्ज i7 मैकबुक प्रो पर ऑप्टिमाइज़ेशन के साथ किया गया था। परिणाम सरणी आकार और पुनरावृत्तियों की संख्या के आधार पर भिन्न होते हैं, लेकिन for लूप हमेशा कम से कम 1.5x, अन्यथा 10x तक अन्य तरीकों से बेहतर प्रदर्शन करते हैं।

मैं यहां स्विफ्ट के प्रदर्शन के बारे में थोड़ा परेशान हूं। क्या इस तरह के परिचालन करने के लिए अंतर्निहित ऐरे विधियों को निष्क्रिय दृष्टिकोण से तेज नहीं होना चाहिए? हो सकता है कि कोई व्यक्ति निम्न स्तर के ज्ञान के साथ स्थिति पर कुछ प्रकाश डाल सके।

+1

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

+0

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

+0

शायद यह आलेख फॉर-लूप के बीच प्रदर्शन अंतरों पर कुछ अंतर्दृष्टि दे सकता है और कम हो जाता है: http://airspeedvelocity.net/2015/08/03/arrays-linked-lists-and-performance/ – JDS

उत्तर

24

क्या ऐसे ऑपरेशन करने के लिए अंतर्निहित ऐरे विधियों को निष्क्रिय दृष्टिकोण से तेज नहीं होना चाहिए? हो सकता है कि कोई व्यक्ति निम्न स्तर के ज्ञान के साथ स्थिति पर कुछ प्रकाश डाल सके।

मैं बस के साथ एक "जरूरी नहीं कि" (मेरी ओर से स्विफ्ट अनुकूलक की प्रकृति की समझ कम) के साथ वैचारिक स्तर से सवाल और अधिक के इस भाग को संबोधित करने के प्रयास करने के लिए चाहते हैं। स्विफ्ट के अनुकूलक की प्रकृति के गहरे जड़ वाले ज्ञान की तुलना में यह कंपाइलर डिज़ाइन और कंप्यूटर आर्किटेक्चर में पृष्ठभूमि से अधिक आ रहा है।

ओवरहेड

तरह map और reduce इनपुट के रूप में स्वीकार करने कार्यों कार्यों के साथ कॉलिंग, यह एक तरह से डाल करने के लिए अनुकूलक पर ज्यादा तनाव देता है। कुछ बहुत ही आक्रामक अनुकूलन के इस तरह के मामले संक्षेप में प्राकृतिक प्रलोभन लगातार आगे और पीछे की, कहते हैं, map क्रियान्वयन, और बंद आपके द्वारा दी गई के बीच शाखा, और इसी तरह संचारित करने के लिए (रजिस्टर और ढेर के माध्यम से कोड के इन अलग-अलग शाखाओं में डेटा है , आमतौर पर)।

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

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

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

स्टैंडर्ड कार्य

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

कृत्रिम मानक

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

+3

यह वास्तव में जानकारीपूर्ण है - मैंने हमेशा पढ़ा था कि लूप को समानांतर करके उच्च-आदेश फ़ंक्शंस तेज़ होना चाहिए, लेकिन अब मैं देखता हूं कि यह हमेशा ऐसा क्यों नहीं हो सकता है। एक संबंधित प्रश्न के रूप में - क्या आप स्विफ्ट कोड को अनुकूलित करने के लिए किसी भी अच्छे संसाधन के बारे में जानते हैं? मुझे अपने अनुप्रयोगों को बेहतर बनाने के लिए अधिक निम्न स्तर के ज्ञान प्राप्त करने में दिलचस्पी है। – hundley

+3

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

12

मैं आपके पहले टेस्ट (map() बनाम append() लूप में) के बारे में ज्यादा कुछ नहीं कह सकता हूं लेकिन मैं आपके परिणामों की पुष्टि कर सकता हूं। संलग्न पाश भी तेजी से करता है, तो आप सरणी निर्माण के बाद

output.reserveCapacity(array.count) 

जोड़ने हो जाता है। ऐसा लगता है कि ऐप्पल पर चीजों को बेहतर बना सकता है और आप एक बग रिपोर्ट दर्ज कर सकते हैं।

for _ in 0..<100_000 { 
    var sum: Float = 0 
    for element in array { 
     sum += element 
    } 
} 

संकलक (शायद) में पूरे पाश क्योंकि परिकलित परिणामों को बिल्कुल भी उपयोग नहीं कर रहे निकाल देता है। मैं केवल अटकलें कर सकते हैं क्यों एक ऐसी ही अनुकूलन

for _ in 0..<100_000 { 
    let sum = array.reduce(0, combine: {$0 + $1}) 
} 

में ऐसा नहीं होता है, लेकिन अगर बंद के साथ reduce() कॉल करने वाले किसी दुष्प्रभाव है या नहीं यह तय करने के लिए और अधिक कठिन होगा।

परीक्षण कोड को हल्के से बदल रहा है, तो की गणना और प्रिंट कुल राशि

do { 
    var total = Float(0.0) 
    let start = NSDate() 
    for _ in 0..<100_000 { 
     total += array.reduce(0, combine: {$0 + $1}) 
    } 
    let elapsed = NSDate().timeIntervalSinceDate(start) 
    print("sum with reduce:", elapsed) 
    print(total) 
} 

do { 
    var total = Float(0.0) 
    let start = NSDate() 
    for _ in 0..<100_000 { 
     var sum = Float(0.0) 
     for element in array { 
      sum += element 
     } 
     total += sum 
    } 
    let elapsed = NSDate().timeIntervalSinceDate(start) 
    print("sum with loop:", elapsed) 
    print(total) 
} 

को तो दोनों वेरिएंट अपने परीक्षण में के बारे में 10 सेकंड का समय।

+4

+1 रिजर्व कैपेसिटी को इंगित करने के लिए +1() - जो गति को लगभग 3x तक बढ़ाता है। इन परीक्षणों से ऐसा लगता है कि मानचित्र() से थोड़ा अधिक अनुकूलित हो सकता है। – hundley

संबंधित मुद्दे