जब किसी सूची के अंतिम लेकिन दूसरे तत्व को खोजा जाता है, तो इनमें से सबसे अंतिम का उपयोग क्यों किया जाता है?


10

नीचे दिए गए 3 फ़ंक्शन हैं जो एक सूची में अंतिम लेकिन दूसरा तत्व ढूंढते हैं। एक का उपयोग last . initबाकी की तुलना में बहुत तेज लगता है। मुझे पता नहीं क्यों लग सकता है।

परीक्षण के लिए, मैंने [1..100000000](100 मिलियन) की इनपुट सूची का उपयोग किया । अंतिम लगभग तुरंत चलता है जबकि अन्य को कई सेकंड लगते हैं।

-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"

-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse

-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init

5
initकई बार "अनपैकिंग" सूची से बचने के लिए अनुकूलित किया गया है।
विलेम वैन ओन्सेम

1
@WillemVanOnsem लेकिन myButLastइतना धीमा क्यों है ? ऐसा लगता है कि यह किसी भी सूची को खोलना नहीं है, लेकिन इसे केवल initफ़ंक्शन के रूप में ट्रैवर्स करना है ...
lsmor

1
@ आइसमोर: यह [x, y]कम समय के लिए है (x:(y:[])), इसलिए यह बाहरी कंस को अनपैक करता है , एक दूसरे कॉन्स को और दूसरे की पूंछ को चेक करता consहै []। इसके अलावा दूसरा खंड सूची को फिर से खोल देगा (x:xs)। हां यह कि अनपैकिंग काफी हद तक कुशल है, लेकिन निश्चित रूप से यदि यह बहुत बार होता है, तो यह प्रक्रिया को धीमा कर देगा।
विलेम वान ओन्सेम

1
Hackage.haskell.org/package/base-4.12.0.0/docs/src/… में देखते हुए , ऑप्टिमाइज़ेशन ऐसा लगता है कि initबार-बार चेक नहीं करता है कि इसका तर्क एक सिंगलटन सूची या खाली सूची है या नहीं। एक बार जब पुनरावृत्ति शुरू हो जाती है, तो यह मान लेता है कि पहले तत्व को पुनरावर्ती कॉल के परिणाम से निपटा जाएगा।
chepner 14

2
@WillemVanOnsem मुझे लगता है कि अनपैकिंग शायद यहाँ मुद्दा नहीं है: जीएचसी कॉल-पैटर्न विशेषज्ञता देता है जो आपको myButLastस्वचालित रूप से अनुकूलित संस्करण देना चाहिए । मुझे लगता है कि यह अधिक संभावना सूची संलयन है जो स्पीडअप के लिए दोषी है।
oisdk 14

जवाबों:


9

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

ऐसा कहे जाने के बाद...

बेंचमार्किंग हमारा दोस्त है।

criterionएक पैकेज है जो उन्नत बेंचमार्किंग उपकरण प्रदान करता है। मैंने जल्दी से इस तरह एक बेंचमार्क का मसौदा तैयार किया:

module Main where

import Criterion
import Criterion.Main

-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"

-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse

-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init

butLast2 :: [a] -> a
butLast2 (x :     _ : [ ] ) = x
butLast2 (_ : xs@(_ : _ ) ) = butLast2 xs
butLast2 _ = error "List too short"

setupEnv = do
  let xs = [1 .. 10^7] :: [Int]
  return xs

benches xs =
  [ bench "slow?"   $ nf myButLast   xs
  , bench "decent?" $ nf myButLast'  xs
  , bench "fast?"   $ nf myButLast'' xs
  , bench "match2"  $ nf butLast2    xs
  ]

main = defaultMain
    [ env setupEnv $ \ xs -> bgroup "main" $ let bs = benches xs in bs ++ reverse bs ]

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

% ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.6.5


% ghc -O2 -package criterion A.hs && ./A
benchmarking main/slow?
time                 54.83 ms   (54.75 ms .. 54.90 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 54.86 ms   (54.82 ms .. 54.93 ms)
std dev              94.77 μs   (54.95 μs .. 146.6 μs)

benchmarking main/decent?
time                 794.3 ms   (32.56 ms .. 1.293 s)
                     0.907 R²   (0.689 R² .. 1.000 R²)
mean                 617.2 ms   (422.7 ms .. 744.8 ms)
std dev              201.3 ms   (105.5 ms .. 283.3 ms)
variance introduced by outliers: 73% (severely inflated)

benchmarking main/fast?
time                 84.60 ms   (84.37 ms .. 84.95 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 84.46 ms   (84.25 ms .. 84.77 ms)
std dev              435.1 μs   (239.0 μs .. 681.4 μs)

benchmarking main/match2
time                 54.87 ms   (54.81 ms .. 54.95 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 54.85 ms   (54.81 ms .. 54.92 ms)
std dev              104.9 μs   (57.03 μs .. 178.7 μs)

benchmarking main/match2
time                 50.60 ms   (47.17 ms .. 53.01 ms)
                     0.993 R²   (0.981 R² .. 0.999 R²)
mean                 60.74 ms   (56.57 ms .. 67.03 ms)
std dev              9.362 ms   (6.074 ms .. 10.95 ms)
variance introduced by outliers: 56% (severely inflated)

benchmarking main/fast?
time                 69.38 ms   (56.64 ms .. 78.73 ms)
                     0.948 R²   (0.835 R² .. 0.994 R²)
mean                 108.2 ms   (92.40 ms .. 129.5 ms)
std dev              30.75 ms   (19.08 ms .. 37.64 ms)
variance introduced by outliers: 76% (severely inflated)

benchmarking main/decent?
time                 770.8 ms   (345.9 ms .. 1.004 s)
                     0.967 R²   (0.894 R² .. 1.000 R²)
mean                 593.4 ms   (422.8 ms .. 691.4 ms)
std dev              167.0 ms   (50.32 ms .. 226.1 ms)
variance introduced by outliers: 72% (severely inflated)

benchmarking main/slow?
time                 54.87 ms   (54.77 ms .. 55.00 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 54.95 ms   (54.88 ms .. 55.10 ms)
std dev              185.3 μs   (54.54 μs .. 251.8 μs)

ऐसा लगता है कि हमारा "धीमा" संस्करण बिल्कुल भी धीमा नहीं है! और पैटर्न मिलान की पेचीदगियां कुछ भी नहीं जोड़ती हैं। (एक छोटी सी गति जो हम देखते हैं कि match2मैं लगातार दो रन के बीच कैशिंग के प्रभावों को बताता हूं।)

अधिक "वैज्ञानिक" डेटा प्राप्त करने का एक तरीका है : -ddump-simplकंपाइलर हमारे कोड को देखने के तरीके पर ध्यान दे सकता है ।

मध्यवर्ती संरचनाओं का निरीक्षण हमारा मित्र है।

"कोर" जीएचसी की एक आंतरिक भाषा है। हर हास्केल स्रोत फ़ाइल को चलाने के लिए अंतिम समय के लिए अंतिम कार्यात्मक ग्राफ में तब्दील होने से पहले कोर को सरलीकृत किया जाता है। यदि हम इस मध्यवर्ती चरण को देखते हैं, तो यह हमें बताएगा कि myButLastऔर butLast2समतुल्य हैं। यह देख रहा है, के बाद से, नामकरण चरण में, हमारे सभी अच्छे पहचानकर्ता बेतरतीब ढंग से मैंग्ड हैं।

% for i in `seq 1 4`; do echo; cat A$i.hs; ghc -O2 -ddump-simpl A$i.hs > A$i.simpl; done

module A1 where

-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"

module A2 where

-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse

module A3 where

-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init

module A4 where

butLast2 :: [a] -> a
butLast2 (x :     _ : [ ] ) = x
butLast2 (_ : xs@(_ : _ ) ) = butLast2 xs
butLast2 _ = error "List too short"

% ./EditDistance.hs *.simpl
(("A1.simpl","A2.simpl"),3866)
(("A1.simpl","A3.simpl"),3794)
(("A2.simpl","A3.simpl"),663)
(("A1.simpl","A4.simpl"),607)
(("A2.simpl","A4.simpl"),4188)
(("A3.simpl","A4.simpl"),4113)

ऐसा लगता है कि A1और A4सबसे समान हैं। पूरी तरह से निरीक्षण से पता चलेगा कि वास्तव में कोड संरचनाएं समान हैं A1और A4समान हैं। यही कारण है कि A2और A3एक जैसे हैं, क्योंकि दोनों ही दो कार्यों की एक संरचना के रूप में परिभाषित कर रहे हैं भी उचित है।

यदि आप coreबड़े पैमाने पर आउटपुट की जांच करने जा रहे हैं , तो यह इस तरह के और झंडे की आपूर्ति करने के लिए भी समझ में आता है । वे इसे पढ़ने में बहुत आसान बनाते हैं।-dsuppress-module-prefixes-dsuppress-uniques

हमारे दुश्मनों की एक छोटी सूची भी।

तो, बेंचमार्किंग और अनुकूलन में क्या गलत हो सकता है?

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

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


बहुत जानकारीपूर्ण है, और इस स्तर पर मेरे लिए थोड़ा भारी भी है। इस मामले में, सभी "बेंचमार्किंग" मैंने 100 मिलियन आइटम सूची के लिए सभी फ़ंक्शन चलाए और नोटिस किया कि एक दूसरे की तुलना में अधिक समय लेता है। मानदंड के साथ बेंचमार्क बल्कि उपयोगी लगता है। इसके अलावा, ghciएक exe बनाने की तुलना में अलग-अलग परिणाम (गति के संदर्भ में) देने के लिए लगता है, जैसे आपने कहा।
तूफान 125
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.