কীভাবে কার্যক্ষম প্রোগ্রামিংয়ের সাথে দক্ষতা বাড়ানো যায়?


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
আমার উল্লেখ করা উচিত যে সমাধান করা অয়লার সমস্যার অনেকগুলি পিডিএফ রয়েছে যা গণিতের সমস্যার সমাধান করতে চলেছে। আপনি সেই পিডিএফটি পড়ার চেষ্টা করতে পারেন এবং প্রতিটি ভাষায় বর্ণিত অ্যালগরিদম বাস্তবায়ন করতে পারেন এবং তারপরে এটি প্রোফাইল করতে পারেন।

উত্তর:


25

প্রথমে আপনার ভাষাটি সমস্যা বলে মনে করার আগে আপনার একটি অনুকূলিত বাইনারি রয়েছে তা নিশ্চিত করা দরকার। রিয়েল ওয়াল্ড হাস্কেলের প্রোফাইলিং এবং অপ্টিমাইজেশন অধ্যায়টি পড়ুন । এটি লক্ষণীয় যে বেশিরভাগ ক্ষেত্রে ভাষার উচ্চ-স্তরের প্রকৃতির আপনাকে কমপক্ষে কিছু সম্পাদন করতে হবে।

তবে নোট করুন যে অন্যান্য সমাধানটি দ্রুত নয় কারণ এটি একটি অন্তর্নির্মিত ফাংশন ব্যবহার করে, তবে কেবলমাত্র এটি একটি দ্রুত অ্যালগরিদম ব্যবহার করে : সংখ্যার সংখ্যার সর্বনিম্ন সাধারণ একাধিকটি খুঁজে পেতে আপনাকে কেবল কয়েকটি জিসিডি খুঁজে পেতে হবে। এটি আপনার সমাধানের সাথে তুলনা করুন, যা 1 থেকে সমস্ত সংখ্যার মধ্য দিয়ে চক্র করে foldl lcm [1..20]। যদি আপনি 30 দিয়ে চেষ্টা করেন, রানটাইমের মধ্যে পার্থক্য আরও বেশি হবে।

জটিলতাগুলি দেখুন: আপনার অ্যালগরিদমের O(ans*N)রানটাইম রয়েছে, ansউত্তরটি কোথায় এবং Nআপনি বিভাজ্যতার জন্য যাচাই করছেন এমন নম্বর (আপনার ক্ষেত্রে 20)।
অন্যান্য অ্যালগরিদম Nসময়গুলি কার্যকর করে lcm, তবে lcm(a,b) = a*b/gcd(a,b), এবং জিসিডির জটিলতা রয়েছে O(log(max(a,b)))। সুতরাং দ্বিতীয় অ্যালগরিদমের জটিলতা রয়েছে O(N*log(ans))। আপনি নিজের জন্য বিচার করতে পারেন যা দ্রুত।

সুতরাং, সংক্ষেপে বলতে গেলে :
আপনার সমস্যা ভাষা নয়, আপনার অ্যালগরিদম।

মনে রাখবেন যে গাণিতিক ভারী প্রোগ্রামগুলিতে গণিত-ভারী প্রোগ্রামগুলিতে উভয়ই কার্যকরী এবং ফোকাসযুক্ত ভাষা রয়েছে যা গণিত-কেন্দ্রিক সমস্যার জন্য সম্ভবত অন্য যে কোনও কিছুর চেয়ে দ্রুত গতিযুক্ত। এটিতে ফাংশনগুলির একটি খুব অনুকূলিতকরণ গ্রন্থাগার রয়েছে এবং এটি কার্যকরী দৃষ্টান্তকে সমর্থন করে (স্বীকার করে এটি এটিও জরুরী প্রোগ্রামিং সমর্থন করে)।


3
আমি সম্প্রতি একটি হাস্কেল প্রোগ্রামে পারফরম্যান্সে সমস্যা পেয়েছিলাম এবং তখন আমি বুঝতে পারি যে আমি অপটিমাইজেশনগুলি স্যুইচ অফ দিয়ে সংকলন করেছি। প্রায় 10 বার দ্বারা উত্সাহিত পারফরম্যান্সে অপ্টিমাইজেশন স্যুইচিং। সুতরাং সি তে লিখিত একই প্রোগ্রামটি এখনও ত্বরান্বিত ছিল, তবে হাস্কেল খুব ধীর ছিল না (প্রায় 2, 3 গুণ ধীর, যা আমি মনে করি একটি ভাল পারফরম্যান্স, এছাড়াও আমি হ্যাসেল কোডটি আরও উন্নত করার চেষ্টা না করে বিবেচনা করেছি)। নীচের লাইন: প্রোফাইলিং এবং অপ্টিমাইজেশন একটি ভাল পরামর্শ। +1
জর্জিও

3
আন্তরিকভাবে মনে হয় আপনি প্রথম দুটি অনুচ্ছেদ সরিয়ে ফেলতে পারেন, তারা সত্যিকার অর্থে প্রশ্নের উত্তর দেয় না এবং সম্ভবত ভুল হয় (তারা অবশ্যই পরিভাষা দিয়ে দ্রুত এবং আলগা খেলায়, ভাষাগুলির গতি নেই)
জে কে।

1
আপনি একটি বিরোধী উত্তর দিচ্ছেন। একদিকে, আপনি ওপিকে দৃ "়ভাবে বলেছেন যে "কোনও কিছুকেই ভুল বোঝেনি", এবং হতাশার বিষয়টি হাস্কেলের অন্তর্নিহিত। অন্যদিকে, আপনি অ্যালগরিদমের পছন্দটি দেখান কিছু যায় আসে না! আপনার উত্তরটি আরও ভাল হতে পারে যদি এটি প্রথম দুটি অনুচ্ছেদ বাদ দেয়, যা উত্তরের বাকী অংশের সাথে কিছুটা পরস্পরবিরোধী।
আন্দ্রেস এফ।

2
আন্দ্রেস এফ এবং জে.কে থেকে প্রতিক্রিয়া নিচ্ছেন। আমি প্রথম দুটি অনুচ্ছেদ কয়েকটি বাক্যে হ্রাস করার সিদ্ধান্ত নিয়েছি। মন্তব্যের জন্য ধন্যবাদ
কে.স্টেফ

5

আমার প্রথম ধারণাটি ছিল যে সমস্ত প্রাইম দ্বারা কেবল বিভাজ্য সংখ্যাগুলি = = 20 সমস্ত সংখ্যার দ্বারা 20 এর চেয়ে কম বিভাজ্য হবে So । এই জাতীয় সমাধান ব্রুট-ফোর্স পদ্ধতির যতগুলি সংখ্যা 1 / 9,699,690 হিসাবে পরীক্ষা করে। তবে আপনার দ্রুত-হাস্কেল সমাধান এর চেয়ে ভাল করে better

যদি আমি "ফাস্ট হ্যাসেল" সমাধানটি বুঝতে পারি তবে এটি 1 থেকে 20 সাল পর্যন্ত তালিকার জন্য এলসিএম (সর্বনিম্ন সাধারণ একাধিক) ফাংশনটি প্রয়োগ করতে ভাঁজ 1 ব্যবহার করে So সুতরাং এটি ফলক 2 কে, এলসিএম 1 2 প্রয়োগ করবে Then । তারপর এলসিএম 6 4 উত্পাদনকারী 12, এবং আরও অনেক কিছু। এইভাবে, আপনার উত্তরটি উত্পন্ন করতে lcm ফাংশনটি কেবল 19 বার বলা হয়। বিগ ও স্বরলিপিতে, এটি কোনও সমাধানে পৌঁছানোর জন্য ও (এন -1) ক্রিয়াকলাপ।

আপনার ধীর-হাস্কেল সমাধানটি 1 থেকে আপনার সমাধানে প্রতিটি সংখ্যার জন্য 1-20 নম্বর দিয়ে যায়। আমরা যদি সলিউশনকে কল করি, তবে ধীর-হাস্কেল সলিউশন ও (গুলি *) অপারেশন করে। আমরা ইতিমধ্যে জানি যে 9 টি 9 মিলিয়নেরও বেশি, সুতরাং এটি সম্ভবত অলসতা ব্যাখ্যা করে। এমনকি যদি সমস্ত শর্টকাট হয় এবং 1-20 সংখ্যার তালিকার মধ্য দিয়ে অর্ধপথে গড় পায়, তবে এটি এখনও কেবল ও (গুলি * এন / 2)।

কল করা headআপনাকে এই গণনাগুলি করা থেকে বাঁচায় না, প্রথম সমাধানটি গণনা করার জন্য সেগুলি করতে হবে।

ধন্যবাদ, এটি একটি আকর্ষণীয় প্রশ্ন ছিল। এটি সত্যই আমার হাস্কেল জ্ঞান প্রসারিত করেছে। আমি শেষ পতনের অ্যালগরিদম অধ্যয়ন না করে থাকলে আমি এর উত্তর দিতে সক্ষম হব না।


আসলে আপনি যে পন্থাটি 2 * 3 * 5 * 7 * 11 * 13 * 17 * 19 এর সাথে পেয়ে যাচ্ছেন তা সম্ভবত কমপক্ষে এলসিএম ভিত্তিক সমাধান হিসাবে দ্রুত fast আপনার বিশেষত যা প্রয়োজন তা হ'ল 2 ^ 4 * 3 ^ 2 * 5 * 7 * 11 * 13 * 17 * 19. কারণ 2 ^ 4 হল 20 এর চেয়ে কম বা সমান 2 এর বৃহত্তম শক্তি এবং 3 ^ 2 হ'ল বৃহত্তম শক্তি 3 এর চেয়ে কম বা 20 এর সমান এবং আরও অনেক কিছু।
সেমিকোলন

@ সেমিকোলন অন্যান্য আলোচিত বিকল্পগুলির তুলনায় অবশ্যই দ্রুততর, এই পদ্ধতির জন্য ইনপুট প্যারামিটারের চেয়ে ছোট প্রাইমগুলির একটি প্রাক-গণনাযুক্ত তালিকাও প্রয়োজন। যদি আমরা
এটির

@ কে.স্টেফ আপনি কি আমার সাথে কৌতুক করছেন ... 19 সাল অবধি আপনাকে প্রাইমগুলি কম্পিউটার করতে হবে ... এটি একটি সেকেন্ডের একটি ক্ষুদ্র অংশ নেয় takes আপনার বক্তব্যটি একেবারে শূন্য করে তোলে, আমার পদ্ধতির মোট রানটাইম প্রাইম প্রজন্মের সাথেও অবিশ্বাস্যভাবে ছোট iny আমি প্রোফাইলিং সক্ষম করেছি এবং আমার পদ্ধতির (হাস্কেলের মধ্যে) পেয়েছি total time = 0.00 secs (0 ticks @ 1000 us, 1 processor)এবং পেয়েছি total alloc = 51,504 bytes। রানটাইম এমনকি প্রোফাইলে নিবন্ধন না করার জন্য একটি সেকেন্ডের একটি তুচ্ছ পর্যাপ্ত অনুপাত।
সেমিকোলন

@ সেমিকোলনের আমার মন্তব্যটি দক্ষ হওয়া উচিত ছিল, এটির জন্য দুঃখিত। আমার বক্তব্যটি এন পর্যন্ত সমস্ত প্রাইম গণনা করার গোপন মূল্যের সাথে সম্পর্কিত ছিল - ভ্রাতৃত্বীয় ইরোটোস্টেনেস হ'ল হে (এন * লগ (এন) * লগ (লগ (এন))) অপারেশন এবং ও (এন) মেমরি যার অর্থ এটি প্রথম N সত্যই বড় হলে অ্যালগরিদমের উপাদান যা মেমরি বা সময় থেকে শেষ হয়ে যাবে। এটি অ্যাটকিনের চালনী দিয়ে খুব ভাল হয় না, তাই আমি সিদ্ধান্ত নিয়েছি যে অ্যালগরিদমটি এর চেয়ে কম আবেদনকারী হবে foldl lcm [1..N], যার জন্য অবিচ্ছিন্ন সংখ্যার দাগ দরকার।
কে.স্টেফ

@ কে.স্টেফ ওয়েল আমি কেবল দুটি অ্যালগরিদম পরীক্ষা করেছি। আমার প্রাথমিক ভিত্তিক অ্যালগরিদমের জন্য প্রোফাইলার আমাকে দিয়েছেন (এন = 100,000 এর জন্য): total time = 0.04 secsএবং total alloc = 108,327,328 bytes। অন্যান্য এলসিএম ভিত্তিক অ্যালগরিদমের জন্য প্রোফাইলার আমাকে দিয়েছেন: total time = 0.67 secsএবং total alloc = 1,975,550,160 bytes। এন = 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কেবল এখান থেকে আমদানি করা হয়েছিল 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 থেকে 1 nএবং অন্যটি জ্যামিতিকভাবে 1 থেকে যায় ans*একই পরিস্থিতি এবং প্রতিটি অ-মৌলিক সংখ্যা এড়িয়ে যাওয়ার অতিরিক্ত উপকারের সাথে কল করার জন্য (আরও ব্যয়বহুল প্রকৃতির কারণে নিখরচায় বিনা মূল্যে *)।

এবং এটি সুপরিচিত যা এর *চেয়ে দ্রুততর lcm, কারণ এর lcmপুনরাবৃত্তি প্রয়োগের প্রয়োজন হয় mod, এবং modঅ্যাসেম্পোটোটিক্যালি ধীর ( O(n^2)বনাম ~O(n^1.5))।

সুতরাং উপরের ফলাফলগুলি এবং সংক্ষিপ্ত অ্যালগরিদম বিশ্লেষণকে এটিকে খুব স্পষ্ট করে তুলতে হবে যা কোন অ্যালগরিদম দ্রুত।

আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.