একটি ফাংশন যখন নিজেকেই কল করে, তখন তাকে রিকার্সিভ বলা হয়।
ফাংশন কল আর লুপের মধ্যে একটি গুরুত্বপূর্ণ পার্থক্য হলো এই যে, কোনো ফাংশন কল করলে যে অ্যাড্রেসে ফিরে আসতে হবে সেটি স্ট্যাকে পুশ করা হয়। এর মানে হলো, একটি রিকার্সিভ ফাংশন সাধারণত সমতুল্য একটি লুপের চেয়ে বেশি স্ট্যাক স্পেস দাবি করে।
এর ফলাফল হিসেবে, যে ফাংশন বারবার নিজেকেই কল করে চলে, সেটি একসময় পুরো স্ট্যাক স্পেস শেষ করে ফেলতে পারে। এটাকে বলা হয় স্ট্যাক ওভারফ্লো।
এই কারণেই প্রতিটি রিকার্সিভ ফাংশনে অন্তত একটি বেস কেস থাকতে হবে, অর্থাৎ এমন একটি অবস্থা, যেখানে ফাংশনটি নিজেকে কল না করেই রিটার্ন করে। যেকোনো রিকার্সিভ কলকে একসময় না একসময় একটি বেস কেসে পৌঁছাতেই হবে।
উদাহরণস্বরূপ, ফ্যাক্টোরিয়াল ফাংশনটি, অর্থাৎ n! = n * (n - 1) * ... * 1, রিকার্সিভভাবে ডিফাইন করা যায়, যেখানে বেস কেস হলো 1:
factorial:
; the argument `n` is passed on `rdi`
; the factorial will be returned on `rax`
cmp rdi, 1
jle .base_case ; base case -> if rdi <= 1, return 1
push rdi ; save n
dec rdi ; rdi = n - 1
call factorial ; recursive call, rax = (n - 1)!
pop rdi ; restore n
imul rax, rdi ; rax = n * (n - 1)! = n!
ret
.base_case:
mov rax, 1
ret
খেয়াল করুন, factorial ফাংশনটি রিকার্স করার আগে অবশ্যই push rdi করে এবং পরে pop rdi করে।
কারণ রিকার্সিভ কলটি ফিরে আসার পরও n * (n-1)! হিসাব করার জন্য এর n দরকার হয়।
খেয়াল করুন, ক্যালি-সেভড রেজিস্টার ব্যবহার করলেও এই সমস্যার সমাধান হবে না।
একটি রিকার্সিভ ফাংশন নিজের সম্ভাব্য কলার হলেও, সে নিজেই আবার অন্য কোনো ফাংশনের ক্যালি।
তার মানে হলো, ফাংশনটিকে ক্যালি-সেভড রেজিস্টারগুলো ব্যবহার করার আগে সংরক্ষণ করতে হয় এবং ব্যবহারের পর সেগুলোর মান ফিরিয়ে আনতে হয়।
এটি সাধারণত push/pop-এর একটি ক্রম দিয়ে করা হয়, যা আমরা আগের একটি কনসেপ্টে দেখেছি।
যেহেতু বেস কেস বাদে রিকার্সিভ ফাংশনের প্রতিটি ফ্রেমই একটি কলার, যার নিজের লোকাল ভ্যারিয়েবল সংরক্ষণ করা দরকার, তাই প্রতিটি ফ্রেমের জন্যই এই push/pop-এর ক্রমটি আবার করতে হয়।
রেজিস্টার ব্যবহার না করে সরাসরি স্ট্যাকে ভ্যারিয়েবল রাখলেও প্রতিটি ফ্রেমে একই 8 বাইট খরচ হতো।
এর মানে হলো, প্রতিটি রিকার্সিভ কল যে রিটার্ন অ্যাড্রেসটি call পুশ করে, তার জন্য স্ট্যাকে 8 বাইট যোগ করে, আর তার সঙ্গে সংরক্ষণ করতে হওয়া প্রতিটি লোকাল ভ্যারিয়েবলের জন্য আরও 8 বাইট।
ফাংশনটি বেস কেসে পৌঁছানো পর্যন্ত প্রতিটি ফ্রেমে স্ট্যাকে এই বাইটগুলো যোগ করতেই থাকবে।
এরপরই এটি উল্টো ক্রমে আনওয়াইন্ড হতে শুরু করে: প্রতিটি রিকার্সিভ কল প্রয়োজনমতো pop চালায় এবং শেষে একটি ret।
উদাহরণস্বরূপ, factorial ফাংশনটি আর্গুমেন্ট 10 নিয়ে কল করা হলে, বেস কেস 1-এ পৌঁছানোর আগে এটি নিজেকে নয়বার কল করবে।
সেই সময়ে, প্রতিটি পূর্ববর্তী ফ্রেমের n (8 বাইট) আর রিটার্ন অ্যাড্রেস (8 বাইট) সংরক্ষণ করতে 144 বাইট খরচ হয়ে যাবে।
কিছু কিছু ক্ষেত্রে কোনো ফাংশন অন্য একটি ফাংশন কল করার পর এবং রিটার্ন করার আগে আর কোনো কাজ করে না।
উদাহরণ হিসেবে এটি দেখুন:
times_three:
imul rax, rdi, 3
ret
triple_of_square:
imul rdi, rdi
call times_three
ret
triple_of_square ফাংশনটি:
rdi-তে আছে) নিজের সঙ্গে গুণ করে তার বর্গ বের করে;times_three কল করে, যা পাস করা আর্গুমেন্টকে তিন দিয়ে গুণ করে রিটার্ন করে।ফলে triple_of_square রিটার্ন করে 3*x², যেখানে x হলো তার আর্গুমেন্ট, যা rdi-তে পাস করা হয়।
খেয়াল করুন, times_three কল করার পর triple_of_square-এ আর কোনো কাজ হয় না, ফাংশনটি শুধু রিটার্ন করে।
এমন পরিস্থিতিতে call ব্যবহার না করে একটি ফাংশন jmp ব্যবহার করে এক্সিকিউশনকে কল করা ফাংশনের কাছে হস্তান্তর করতে পারে:
times_three:
imul rax, rdi, 3
ret
triple_of_square:
imul rdi, rdi
jmp times_three
এটাকে বলা হয় টেইল কল।
টেইল কলের মূল সুবিধা হলো call-এর বাড়তি খরচ এড়ানো।
একটি call স্ট্যাকে একটি রিটার্ন অ্যাড্রেস পুশ করে, আর কন্ট্রোল যাতে সেই বিন্দুতে ফিরে আসতে পারে, তার জন্য থাকতে হবে মিলে যাওয়া একটি ret।
টেইল কল দুটোই বাদ দেয়: পুশ করার কোনো রিটার্ন অ্যাড্রেস নেই, আর জোড়া লাগানোর মতো বাড়তি ret-ও নেই, শুধু কল করা ফাংশনের নিজের ret।
যে রিকার্সিভ ফাংশনগুলো রিটার্ন করার আগে নিজেদের অনেকবার কল করতে পারে, তাদের জন্য টেইল কল বিশেষভাবে কাজে লাগে।
তবে প্রতিটি রিকার্সিভ কলকেই সরাসরি টেইল কলে রূপান্তর করা যায় না।
যেহেতু একটি jmp কন্ট্রোলকে কল করা ফাংশনের কাছে হস্তান্তর করে, তাই টেইল কলের পর কলার আর কোনো কাজ করতে পারে না।
উদাহরণস্বরূপ, আগের factorial ফাংশনটি টেইল রিকার্সিভ নয়।
রিকার্সিভ কলের পরেও imul rax, rdi ব্যবহার করে ফলাফলটিকে বর্তমান n-এর সঙ্গে গুণ করতে হয়।
এমন পরিস্থিতিতে মাঝে মাঝে একটি অ্যাকিউমুলেটর ব্যবহার করা সম্ভব, যা আংশিক হিসাবগুলো জমা করে শেষে রিটার্ন করা হবে।
উদাহরণস্বরূপ, আমরা একটি factorial_helper ডিফাইন করতে পারি, যা বেশিরভাগ কাজ করে, আর তারপর factorial একটি অ্যাকিউমুলেটর তৈরি করে কন্ট্রোল factorial_helper-এর কাছে হস্তান্তর করে:
factorial_helper:
; the argument `n` is passed on `rdi`
; `rax` is used as an accumulator and will be returned at the end
cmp rdi, 1
jle .base_case
imul rax, rdi ; we accumulate the partial result on `rax`
dec rdi ; rdi = n - 1
jmp factorial_helper ; tail call to accumulate (n - 1)!
.base_case:
ret ; returns the factorial already accumulated on `rax`
factorial:
mov rax, 1 ; initial value for the accumulator
jmp factorial_helper ; tail call
যেহেতু রিকার্সিভ কলের পর আর কোনো কাজ করা হয় না, তাই আমাদের আর rdi সংরক্ষণ করারও দরকার নেই।
এখানে কোনো call বা push rdi নেই, তাই প্রতিটি রিকার্সিভ ইটারেশন স্ট্যাকে 0 বাইট যোগ করে: বাড়তি কোনো স্ট্যাক স্পেস ব্যবহৃত হয় না।
এই ভার্সনটি স্ট্যাক ওভারফ্লো না করেই যত বড়ই n হোক সামলাতে পারে।
এটি একইসঙ্গে বেশি দক্ষ এবং বেশি নিরাপদ।
কিছু ক্ষেত্রে ফাংশনগুলোর ক্রম একটু বদলে দিলে সহায়ক ফাংশনে যাওয়ার jmp-টিও এড়ানো যায়।
উদাহরণস্বরূপ, factorial আর triple_of_square-কে এভাবে আবার লেখা যায়:
factorial:
mov rax, 1
factorial_helper:
cmp rdi, 1
jle .base_case
imul rax, rdi
dec rdi
jmp factorial_helper
.base_case:
ret
triple_of_square:
imul rdi, rdi
times_three:
imul rax, rdi, 3
ret
উপরের স্নিপেটে factorial-এর এক্সিকিউশন ফল থ্রু করে সরাসরি factorial_helper-এ গিয়ে পড়ে।
triple_of_square আর times_three-এর ক্ষেত্রেও একই ঘটনা ঘটে।
দুটি ক্ষেত্রেই এক্সিকিউশন ক্রম অনুযায়ী এগিয়ে যায় এবং মনে হয় যেন টেইল ফাংশনটি আসলে "মূল" ফাংশনের ভেতরের একটি লোকাল লেবেল।
আসলে কোনো লোকাল লেবেল আর একটি ফাংশনের মধ্যে মৌলিক কোনো পার্থক্য নেই।
x86-64 অ্যাসেম্বলি এদের কোনোটিকেই বিশেষ কোনো আচরণ করে না, এরা সবাই এক্সিকিউটেবল কোডের একটি সেকশনের ভেতরের অ্যাড্রেসমাত্র, যেমন section .text।
এই অর্থে একটি টেইল রিকার্সিভ ফাংশনকে মূলত একটি লুপের মতোই ভাবা যায়, যেখানে রিকার্সিভ কলটি আবার উপরে লাফিয়ে যায়, আর বেস কেসটিই সেই শর্ত যা লুপটি শেষ করে।
পাইপার পাই বেক করতে দারুণ ভালোবাসেন।
তিনি নামের কারণেই পাই বেক করা বেছে নিয়েছিলেন, নাকি নিজের শখের সঙ্গে মিলিয়ে নাম বদলে ফেলেছিলেন, তা কেউ জানে না। প্রথম দেখায় পরেরটা খুব একটা সম্ভাবনাময় মনে হয় না, কিন্তু ব্যাপারটা হলো, পাইপার পাইয়ের প্রতি পুরোপুরি মুগ্ধ। তিনি সব সময় রান্নাঘরে ঘাঁটাঘাঁটি করেন, রেসিপিতে এদিক-ওদিক বদল আনেন, নিজের হাতের কাজ আরও নিখুঁত করেন, আর তাতে তার বন্ধুরা দারুণ খুশি হয়। খুঁটিনাটির প্রতি তার নজর থেকে কিছুই এড়ায় না, ওভেনের তাপমাত্রা নয়, মণ্ডের প্রতিটি বলের ওজন নয়, আর পাইয়ের নিজের আকৃতি তো নয়ই।
তার সর্বশেষ আগ্রহ? যতটা সম্ভব গোলাকার পাই বেক করা, গাণিতিক নিখুঁততার পর্যায় পর্যন্ত, তার প্রিয় সংখ্যাটির সাহায্যে, যা আপনি ঠিকই ধরে ফেলেছেন: π।
ইটারেটিভ পদ্ধতিতে π হিসাব করার একটি চমৎকার সূত্র পাইপার খুঁজে পেয়েছেন, নিউটন/অয়লার কনভার্জেন্স ট্রান্সফরমেশন:
π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!
পাইপারকে তার রান্নাঘর গুছিয়ে নিতে সাহায্য করুন, আর তার গাণিতিকভাবে নিখুঁত পাইটি বেক করুন।
আজ সকালে পাইপার দুই ব্যাচ মণ্ড বেলেছেন, দুটোর ওজন আলাদা (g এককে)।
তার পাইগুলো যেন একরকম হয়, সেজন্য তিনি দুই ব্যাচকেই একই ওজনের বলে ভাগ করতে চান।
আর অবশ্যই তিনি চান ভাগগুলো যতটা সম্ভব বড় হোক, যাতে যতটা সম্ভব কম মণ্ড নষ্ট হয়!
যে বৃহত্তম ওজন দুটি ব্যাচকেই নিঃশেষে ভাগ করে, সেটিই তাদের গরিষ্ঠ সাধারণ গুণনীয়ক। ইউক্লিডীয় অ্যালগরিদম এটি রিকার্সিভভাবে হিসাব করে:
gcd(a, 0) = a (বেস কেস)gcd(a, b) = gcd(b, a mod b)লক্ষ্য করুন, রিকার্সিভ কলটি থাকে টেইল পজিশনে: তার পরে আর কিছুই ঘটে না।
largest_portion এমনভাবে ডিফাইন করুন যাতে রিকার্সিভ ধাপটি ফাংশনটির নিজের কাছে একটি jmp হয়, call নয়।
largest_portion(252, 105);
// => 21
দুটি আর্গুমেন্টই ৬৪-বিটের অ-ঋণাত্মক ইন্টিজার। রিটার্ন ভ্যালুটি একটি ৬৪-বিটের অ-ঋণাত্মক ইন্টিজার।
সাধারণ ফ্যাক্টোরিয়াল কীভাবে টেইল রিকার্সিভভাবে লিখতে হয়, তা আপনি কনসেপ্ট থেকেই জানেন। একই ফাংশন আপনার স্টাব ফাইলে রয়েছে।
তবে নিউটন/অয়লার সূত্রে ডাবল ফ্যাক্টোরিয়ালও ব্যবহার হয়, যা লেখা হয় !!।
ডাবল ফ্যাক্টোরিয়াল অপারেটরটি এভাবে সংজ্ঞায়িত:
0!! = 1
n!! = 1 * 3 * 5 * ... * n // if n is odd
n!! = 2 * 4 * 6 * ... * n // if n is even
লক্ষ্য করুন, ডাবল ফ্যাক্টোরিয়াল ফ্যাক্টোরিয়ালের মতোই একই ধরন অনুসরণ করে, শুধু প্রতিটি ধাপে ১-এর বদলে ২ করে কমে।
double_factorial ফাংশনটি ডিফাইন করুন, যা টেইল রিকার্সিভ পদ্ধতিতে ডাবল ফ্যাক্টোরিয়াল হিসাব করবে।
double_factorial(5);
// => 15
double_factorial(6);
// => 48
আর্গুমেন্টটি একটি ৩২-বিটের আনসাইনড ইন্টিজার। রিটার্ন ভ্যালুটি একটি ৬৪-বিটের আনসাইনড ইন্টিজার।
এখন পাইপারের কাছে প্রয়োজনীয় সব সরঞ্জাম আছে।
pipers_pi ফাংশনটি ডিফাইন করুন, যা নিউটন/অয়লার কনভার্জেন্স ট্রান্সফরমেশন সূত্রের একটি নির্দিষ্ট সংখ্যক টার্ম ব্যবহার করে π-এর আসন্ন মান বের করে:
π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!
লবটিতে ব্যবহার হয় সাধারণ ফ্যাক্টোরিয়াল।
আপনার জন্য আগেই ডিফাইন করা factorial ফাংশনটি আপনি কল করতে পারেন!
হরটিতে ব্যবহার হয় টাস্ক 2-এ আপনি লেখা double_factorial।
চলুন প্রথম টার্মটি একসঙ্গে হিসাব করি।
ঊর্ধ্বসীমা 0 ধরলে (অসীমের বদলে), আমরা পাই:
π / 2 ≈ sum for k from 0 to 0 of ( k! ) / ( 2 * k + 1 )!!
π / 2 ≈ (0!) / ( 2 * 0 + 1 )!!
π / 2 ≈ 0! / 1!!
π / 2 ≈ 1 / 1
π / 2 ≈ 1.0
π ≈ 2.0
আর ঊর্ধ্বসীমা 2 ধরলে আমরা পাই:
π / 2 ≈ sum for k from 0 to 2 of ( k! ) / ( 2 * k + 1 )!!
π / 2 ≈ ((0!) / ( 2 * 0 + 1 )!!) + ((1!) / ( 2 * 1 + 1 )!!) + ((2!) / ( 2 * 2 + 1 )!!)
π / 2 ≈ 1 + (1! / 3!!) + (2! / 5!!)
π / 2 ≈ 1 + (1 / 3) + (2 / 15)
π / 2 ≈ 1.4666666
π ≈ 2.9333333
প্রতিটি অতিরিক্ত টার্ম আসন্ন মানকে আরও উন্নত করবে।
pipers_pi(0);
// => 2.0
pipers_pi(1);
// => 2.6666666
pipers_pi(2);
// => 2.9333333
আর্গুমেন্টটি একটি ৩২-বিটের অ-ঋণাত্মক ইন্টিজার। রিটার্ন ভ্যালুটি একটি ৬৪-বিটের ফ্লোটিং পয়েন্ট সংখ্যা।
Exercism-এ সাইন আপ করুন, x86-64 Assembly ট্র্যাকের 22টি কনসেপ্ট130টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।