कार्यात्मक प्रोग्रामिंग के साथ दक्षता में सुधार कैसे करें?


20

मैं हाल ही में लर्न यू हास्केल फॉर ग्रेट गुड गाइड के माध्यम से जा रहा हूं और अभ्यास के रूप में मैं इसके साथ प्रोजेक्ट यूलर समस्या 5 को हल करना चाहता था , जो निर्दिष्ट करता है:

1 से 20 तक की सभी संख्याओं में समान रूप से विभाज्य सबसे छोटी धनात्मक संख्या क्या है?

मैंने पहले यह निर्धारित करने के लिए एक फ़ंक्शन लिखने का फैसला किया कि क्या किसी संख्या को इन संख्याओं द्वारा विभाजित किया गया है:

divisable x = all (\y -> x `mod` y == 0)[1..20]

तब मैंने सबसे छोटे का उपयोग करके गणना की head:

sm = head [x | x <- [1..], divisable x]

और अंत में परिणाम प्रदर्शित करने के लिए लाइन लिखी:

main = putStrLn $ show $ sm

दुर्भाग्य से इसे समाप्त होने में लगभग 30 सेकंड का समय लगा। 1 से 10 की संख्या के साथ एक ही काम करने से परिणाम लगभग तुरंत मिलता है, लेकिन फिर 1 से 20 के लिए समाधान की तुलना में फिर से परिणाम बहुत छोटा है।

मैंने इसे पहले सी में हल किया था और 1 से 20 के लिए परिणाम भी लगभग तुरंत गणना की गई थी। यह मुझे विश्वास दिलाता है कि मैं गलत समझ रहा हूं कि हास्केल के लिए इस समस्या की व्याख्या कैसे करें। मैंने अन्य लोगों के समाधानों को देखा और पाया:

main = putStrLn $ show $ foldl1 lcm [1..20]

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


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

जवाबों:


25

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

हालाँकि, ध्यान दें कि अन्य समाधान अधिक तेज़ नहीं है क्योंकि यह एक अंतर्निहित फ़ंक्शन का उपयोग करता है, लेकिन केवल इसलिए कि यह बहुत तेज़ एल्गोरिथम का उपयोग करता है : संख्याओं के एक सेट के कम से कम सामान्य एकाधिक को खोजने के लिए आपको केवल कुछ जीसीडी खोजने की आवश्यकता है। अपने समाधान के साथ इसकी तुलना करें, जो 1 से लेकर सभी संख्याओं के माध्यम से चक्र करता है foldl lcm [1..20]। यदि आप 30 के साथ कोशिश करते हैं, तो रनटाइम्स के बीच का अंतर और भी अधिक होगा।

जटिलताओं पर एक नज़र डालें: आपके एल्गोरिथ्म में O(ans*N)रनटाइम है, जहां ansउत्तर है और Nवह संख्या है जिस पर आप विभाजन के लिए जाँच कर रहे हैं (आपके मामले में 20)। हालांकि ,
अन्य एल्गोरिथ्म Nकई बार निष्पादित होते हैं , और जीसीडी में जटिलता होती है । इसलिए दूसरे एल्गोरिथ्म में जटिलता है । आप अपने लिए न्याय कर सकते हैं जो तेज है।lcmlcm(a,b) = a*b/gcd(a,b)O(log(max(a,b)))O(N*log(ans))

इसलिए, संक्षेप करने के लिए:
आपकी समस्या आपका एल्गोरिथ्म है, न कि भाषा।

ध्यान दें कि विशेष भाषाएं हैं जो गणित-भारी कार्यक्रमों पर केंद्रित और केंद्रित हैं, जैसे गणितज्ञ, जो गणित-केंद्रित समस्याओं के लिए संभवत: लगभग किसी भी चीज की तुलना में तेज है। इसमें कार्यों का एक बहुत ही अनुकूलित पुस्तकालय है, और यह कार्यात्मक प्रतिमान का समर्थन करता है (मूलतः यह भी अनिवार्य प्रोग्रामिंग का समर्थन करता है)।


3
मुझे हाल ही में एक हास्केल कार्यक्रम के साथ एक प्रदर्शन समस्या थी और तब मुझे एहसास हुआ कि मैंने अनुकूलन बंद कर दिया था। लगभग 10 गुना बढ़ाए गए प्रदर्शन पर अनुकूलन स्विच करना। तो सी में लिखा गया एक ही कार्यक्रम अभी भी तेज था, लेकिन हास्केल बहुत धीमा नहीं था (लगभग 2, 3 गुना धीमा, जो मुझे लगता है कि एक अच्छा प्रदर्शन है, यह भी विचार करते हुए कि मैंने हास्केल कोड को और सुधारने की कोशिश नहीं की थी)। निचला रेखा: रूपरेखा और अनुकूलन एक अच्छा सुझाव है। +1
जियोर्जियो

3
ईमानदारी से सोचें कि आप पहले दो पैराग्राफ को हटा सकते हैं, वे वास्तव में सवाल का जवाब नहीं देते हैं और शायद गलत हैं (वे निश्चित रूप से शब्दावली के साथ तेज और ढीली खेलते हैं, भाषाओं की एक गति है)
jk।

1
आप एक विरोधाभासी जवाब दे रहे हैं। एक ओर, आप ओपी को "कुछ गलत नहीं समझा" का दावा करते हैं, और यह कि धीमेपन हास्केल में निहित है। दूसरी ओर, आप एल्गोरिथ्म की पसंद को मायने रखता है! यदि आपका उत्तर पहले दो पैराग्राफ को छोड़ देता है, तो आपका उत्तर बेहतर होगा, जो बाकी उत्तर के साथ कुछ विरोधाभासी हैं।
एंड्रेस एफ।

2
Andres F. और jk से प्रतिक्रिया लेना। मैंने पहले दो पैराग्राफ को कुछ वाक्यों को कम करने का फैसला किया है। टिप्पणियों के लिए धन्यवाद
केटी।

5

मेरा पहला विचार यह था कि सभी संख्याओं से विभाज्य केवल संख्याएँ = 20 से कम संख्याओं से विभाज्य होगी। इसलिए आपको केवल उन संख्याओं पर विचार करने की आवश्यकता है जो 2 * 3 * 5 * 7 * 11 * 13 * 17 * 19 के गुणक हैं। । इस तरह के एक समाधान के रूप में 1 / 9,699,690 के रूप में कई संख्या के रूप में जानवर बल दृष्टिकोण की जाँच करता है। लेकिन आपका फास्ट-हास्केल समाधान इससे बेहतर करता है।

अगर मैं "फास्ट हास्केल" समाधान को समझता हूं, तो यह 1 से 20 तक की संख्या की सूची में lcm (कम से कम सामान्य एकाधिक) फ़ंक्शन को लागू करने के लिए foldl1 का उपयोग करता है। इसलिए यह lcm 1 2, उपज 2 को लागू करेगा। फिर lcm 2 3 क्षेत्ररक्षण 6 इसके बाद 6 6 4 पैदावार 12, और इतने पर। इस तरह, lcm फ़ंक्शन को आपके उत्तर का उत्पादन करने के लिए केवल 19 बार कहा जाता है। बिग ओ नोटेशन में, समाधान पर पहुंचने के लिए ओ (एन -1) ऑपरेशन है।

आपका धीमा-हास्केल समाधान 1 से आपके समाधान के लिए प्रत्येक संख्या के लिए 1-20 की संख्या से गुजरता है। यदि हम समाधान एस कहते हैं, तो धीमी-हास्केल समाधान ओ (एस * एन) संचालन करता है। हम पहले से ही जानते हैं कि एस 9 मिलियन से अधिक है, इसलिए शायद धीमेपन की व्याख्या करता है। यहां तक ​​कि अगर सभी शॉर्टकट और 1-20 की संख्या की सूची के माध्यम से औसतन आधा हो जाता है, तो अभी भी केवल ओ (s * n / 2) है।

कॉलिंग headआपको इन गणनाओं को करने से नहीं बचाती है, उन्हें पहले समाधान की गणना करने के लिए किया जाना चाहिए।

धन्यवाद, यह एक दिलचस्प सवाल था। इसने वास्तव में मेरे हास्केल ज्ञान को बढ़ाया। अगर मैं एल्गोरिदम का आखिरी बार अध्ययन नहीं किया था तो मैं इसका जवाब नहीं दे पाऊंगा।


वास्तव में आप 2 * 3 * 5 * 7 * 11 * 13 * 17 * 19 के साथ जिस दृष्टिकोण को प्राप्त कर रहे थे वह संभवतः कम से कम एलसीएम आधारित समाधान के रूप में उपवास है। आपको विशेष रूप से 2 ^ 4 * 3 ^ 2 * 5 * 7 * 11 * 13 * 17 * 19. क्योंकि 2 ^ 4 में 2 की सबसे बड़ी शक्ति 20 से कम या बराबर है, और 3 ^ 2 सबसे बड़ी शक्ति है 3 से कम या 20 के बराबर, और इसी तरह।
अर्धविराम

@semicolon जब निश्चित रूप से चर्चा की गई अन्य विकल्पों की तुलना में तेज़ है, तो इस दृष्टिकोण को भी इनपुट पैरामीटर की तुलना में, पूर्व-गणना की सूची की आवश्यकता होती है। यदि हम कारक है कि रनटाइम में (और, अधिक महत्वपूर्ण बात, स्मृति पदचिह्न में), यह दृष्टिकोण दुर्भाग्य से कम आकर्षक हो जाता है
K.Steff

@ K.Steff क्या आप मुझसे मजाक कर रहे हैं ... आपको कंप्यूटर को 19 तक का समय देना है ... जो एक दूसरे के छोटे से हिस्से को लेता है। आपका कथन बिल्कुल शून्य समझ में आता है, मेरे दृष्टिकोण का कुल क्रम प्रधानमंत्री पीढ़ी के साथ अविश्वसनीय रूप से छोटा है। मैंने प्रोफाइलिंग को सक्षम किया और मेरा दृष्टिकोण (हास्केल में) मिला total time = 0.00 secs (0 ticks @ 1000 us, 1 processor)और total alloc = 51,504 bytes। रनटाइम एक दूसरे के नगण्य पर्याप्त अनुपात है जो कि प्रोफाइलर पर रजिस्टर भी नहीं है।
अर्धविराम

@semicolon मुझे अपनी टिप्पणी योग्य चाहिए, इसके बारे में खेद है। मेरा कथन एन तक सभी अपराधों की गणना की छिपी हुई कीमत से संबंधित था - भोले इरेटोस्थनीज हे (एन * लॉग (एन) * लॉग (एन (एन))) संचालन और ओ (एन) मेमोरी है जिसका अर्थ है कि यह पहला है एल्गोरिथ्म का घटक जो स्मृति से बाहर चलेगा या यदि एन वास्तव में बड़ा है। यह एटकिन की छलनी से बहुत बेहतर नहीं होता है, इसलिए मैंने निष्कर्ष निकाला कि एल्गोरिथ्म की तुलना में कम आकर्षक होगा foldl lcm [1..N], जिसे लगातार कई बड़े संकेतों की आवश्यकता होती है।
21

@ K.Steff खैर मैंने दोनों एल्गोरिदम का परीक्षण किया। मेरे प्रमुख आधारित एल्गोरिथ्म के लिए प्रोफाइलर ने मुझे (n = 100,000 के लिए) दिया: total time = 0.04 secsऔर total alloc = 108,327,328 bytes। अन्य lcm आधारित एल्गोरिथ्म के लिए प्रोफाइलर ने मुझे दिया: total time = 0.67 secsऔर total alloc = 1,975,550,160 bytes। N = 1,000,000 के लिए मैं प्राइम बेस्ड के लिए मिला: total time = 1.21 secsऔर total alloc = 8,846,768,456 bytes, और एलसीएम बेस्ड के लिए: total time = 61.12 secsऔर total alloc = 200,846,380,808 bytes। तो दूसरे शब्दों में, आप गलत हैं, प्राइम बेस्ड ज्यादा बेहतर है।
सेमीकॉलन

1

मैं शुरू में उत्तर लिखने की योजना नहीं बना रहा था। लेकिन बाद में मुझे बताया गया कि एक अन्य उपयोगकर्ता ने अजीब दावा किया कि बस पहले कुछ युगल को गुणा करना अधिक कम्प्यूटेशनल रूप से महंगा था और फिर बार-बार आवेदन करना lcm। तो यहाँ दो एल्गोरिदम हैं, और कुछ बेंचमार्क हैं:

मेरा एल्गोरिथ्म:

प्राइम जनरेशन एल्गोरिथ्म, मुझे अपराधों की एक अनंत सूची प्रदान करता है।

isPrime :: Int -> Bool
isPrime 1 = False
isPrime n = all ((/= 0) . mod n) (takeWhile ((<= n) . (^ 2)) primes)

toPrime :: Int -> Int
toPrime n 
    | isPrime n = n 
    | otherwise = toPrime (n + 1)

primes :: [Int]
primes = 2 : map (toPrime . (+ 1)) primes

अब कुछ के लिए परिणाम की गणना करने के लिए उस प्रमुख सूची का उपयोग करना N:

solvePrime :: Integer -> Integer
solvePrime n = foldl' (*) 1 $ takeWhile (<= n) (fromIntegral <$> primes)

अब अन्य lcm आधारित एल्गोरिथ्म, जो कि वास्तव में काफी संक्षिप्त है, ज्यादातर इसलिए कि मैंने प्राइम जेनरेशन को स्क्रैच से लागू किया है (और इसके खराब प्रदर्शन के कारण सुपर कॉनसेक लिस्ट कॉम्प्रिहेंशन एल्गोरिदम का उपयोग नहीं किया) जबकि lcmबस से आयात किया गया था Prelude

solveLcm :: Integer -> Integer
solveLcm n = foldl' (flip lcm) 1 [2 .. n]
-- Much slower without `flip` on `lcm`

अब बेंचमार्क के लिए, प्रत्येक के लिए मेरे द्वारा उपयोग किया जाने वाला कोड सरल था: ( -prof -fprof-auto -O2तब +RTS -p)

main :: IO ()
main = print $ solvePrime n
-- OR
main = print $ solveLcm n

के लिए n = 100,000, solvePrime:

total time = 0.04 secs
total alloc = 108,327,328 bytes

बनाम solveLcm:

total time = 0.12 secs
total alloc = 117,842,152 bytes

के लिए n = 1,000,000, solvePrime:

total time = 1.21 secs
total alloc = 8,846,768,456 bytes

बनाम solveLcm:

total time = 9.10 secs
total alloc = 8,963,508,416 bytes

के लिए n = 3,000,000, solvePrime:

total time = 8.99 secs
total alloc = 74,790,070,088 bytes

बनाम solveLcm:

total time = 86.42 secs
total alloc = 75,145,302,416 bytes

मुझे लगता है कि परिणाम अपने लिए बोलते हैं।

प्रोफाइलर इंगित करता है कि प्राइम पीढ़ी रन के समय का एक छोटा और छोटा प्रतिशत nबढ़ाती है। तो यह अड़चन नहीं है, इसलिए हम इसे अभी के लिए अनदेखा कर सकते हैं।

इसका मतलब है कि हम वास्तव में कॉलिंग की तुलना कर रहे हैं lcmजहां एक तर्क 1 से जाता है n, और दूसरा 1 से ज्यामितीय रूप से जाता है ans*एक ही स्थिति के साथ कॉल करने के लिए और प्रत्येक गैर-प्राइम नंबर को छोड़ने का अतिरिक्त लाभ (asymptotically मुफ्त में, अधिक महंगी प्रकृति के कारण *)।

और यह सर्वविदित है कि *की तुलना में तेजी से होता है lcm, के रूप में lcmदोहराया अनुप्रयोगों की आवश्यकता है mod, और modasymptotically धीमी ( O(n^2)बनाम ~O(n^1.5)) है।

इसलिए उपरोक्त परिणाम और संक्षिप्त एल्गोरिथ्म विश्लेषण को यह बहुत स्पष्ट करना चाहिए कि कौन सा एल्गोरिथ्म तेज है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.