Saat fungsi rekursif dipanggil, compiler tidak hanya melacak “di mana kita sekarang” tetapi juga semua konteks pemanggilan sebelumnya. Setiap pemanggilan fungsi mendapatkan stack frame sendiri di call stack — area memori yang berisi parameter, variabel lokal, dan alamat pengembalian. Memahami call stack adalah kunci untuk memahami mengapa rekursi bekerja, mengapa variabel lokal bersifat independen antar pemanggilan, serta mengapa rekursi terlalu dalam menyebabkan stack overflow. Pada Chapter ini kita akan membedah call stack untuk factorial(5), melihat pertumbuhan dan unwinding-nya, lalu mengukur batas stack praktis di sistem Linux.
Anatomi Stack Frame #
Setiap stack frame berisi:
- Parameter fungsi: nilai argumen yang dilewatkan ke fungsi.
- Variabel lokal: variabel yang dideklarasikan di dalam fungsi.
- Alamat pengembalian: instruksi setelah pemanggil (caller).
- Saved registers: register yang harus dipulihkan saat fungsi kembali.
Ukuran frame bergantung pada jumlah dan tipe variabel lokal. Frame factorial(n) mungkin hanya 16 byte (parameter n, return address, padding), sedangkan frame dengan banyak std::vector bisa ratusan byte.
Mekanisme: Push saat Panggil, Pop saat Return #
Saat fungsi f memanggil g:
- Caller
fmengevaluasi argumen, menempatkannya di register atau stack. - Instruksi
call gmenyimpan alamat pengembalian ke stack, lalu melompat keg. - Prologue
gmenyimpan register yang akan dipakai, mengalokasikan ruang untuk variabel lokal. gdieksekusi.- Epilogue
gmengembalikan nilai, memulihkan register, pop frame, melompat ke alamat pengembalian.
Pada rekursi, langkah 1-3 terjadi berulang hingga base case. Lalu langkah 4-5 berurutan dari frame terdalam ke terluar — unwinding.
Call Stack factorial(5): Pertumbuhan #
Saat factorial(5) dipanggil dari main, call stack memiliki frame main dan factorial(5). Frame factorial(5) menyimpan n = 5. Saat factorial(5) memanggil factorial(4), frame baru ditambahkan di atas dengan n = 4. Demikian seterusnya hingga factorial(1) mencapai base case dan mengembalikan 1.
Call Stack factorial(5): Unwinding #
Setelah base case factorial(1) = 1 dikembalikan, frame factorial(1) di-pop. Sekarang factorial(2) melanjutkan dari titik pemanggilan, mengalikan 2 * 1 = 2, lalu return. Proses ini berlanjut hingga factorial(5) mengembalikan 120 ke main.
Variabel Lokal Bersifat Independen #
Kode: Variabel Lokal Berbeda di Setiap Frame #
#include <iostream>
long long factorial_with_local(int n) {
long long local_result; // variabel lokal unik per frame
std::cout << "masuk factorial(" << n
<< "), &local_result = " << &local_result << '\n';
if (n <= 1) {
local_result = 1;
} else {
local_result = n * factorial_with_local(n - 1);
}
std::cout << "kembali factorial(" << n
<< ") = " << local_result << '\n';
return local_result;
}
int main() {
long long hasil = factorial_with_local(3);
std::cout << "hasil: " << hasil << '\n';
return 0;
}Output yang Dihasilkan:
masuk factorial(3), &local_result = 0x7fff945118d8
masuk factorial(2), &local_result = 0x7fff945118a8
masuk factorial(1), &local_result = 0x7fff94511878
kembali factorial(1) = 1
kembali factorial(2) = 2
kembali factorial(3) = 6
hasil: 6- Alamat
local_resultberbeda di setiap frame:0x...18d8,0x...18a8,0x...1878— turun 48 byte per frame (ukuran framefactorial_with_local). Ini menunjukkan setiap pemanggilan memiliki variabel lokalnya sendiri. - Variabel tidak terlihat antar frame:
factorial(3)tidak bisa mengakseslocal_resultdarifactorial(2). Variabel lokal bersifat private per pemanggilan. - Urutan “masuk”: turun (3 → 2 → 1), urutan “kembali”: naik (1 → 2 → 3). Sesuai pola LIFO stack.
Mengukur Ukuran Frame #
Kita dapat mengukur ukuran frame dengan mengurangi alamat variabel lokal di dua pemanggilan berurutan.
Kode: Mengukur Frame Size #
#include <iostream>
#include <cstdint>
long g_prev_addr = 0;
long g_frame_size = 0;
long long factorial_measure(int n) {
int local_marker;
long cur_addr = reinterpret_cast<intptr_t>(&local_marker) & 0xFFFFF;
if (g_prev_addr != 0) {
g_frame_size = g_prev_addr - cur_addr;
}
g_prev_addr = cur_addr;
if (n <= 1) return 1;
return n * factorial_measure(n - 1);
}
int main() {
long long hasil = factorial_measure(5);
std::cout << "hasil = " << hasil << '\n';
std::cout << "ukuran frame (approx) = " << g_frame_size << " byte\n";
return 0;
}Kompilasi tanpa optimasi agar layout stack akurat: g++ -O0 -std=c++17 ...
Output yang Dihasilkan:
hasil = 120
ukuran frame (approx) = 64 bytereinterpret_cast<intptr_t>(&local_marker): mengambil alamat variabel lokal sebagai integer.& 0xFFFFF: mask 20 bit bawah agar mudah dibandingkan. 20 bit cukup untuk variasi dalam satu stack.g_frame_size = g_prev_addr - cur_addr: selisih alamat antar frame = ukuran frame. Karena stack tumbuh ke alamat rendah,prev - cur > 0.- Hasil 64 byte: ukuran frame untuk fungsi sederhana dengan satu
intlokal. Frame ini juga berisi parameter, return address, saved registers, alignment padding.
Batas Call Stack dan Overflow #
Call stack berukuran terbatas. Default di Linux: 8 MB. Di macOS: 8 MB. Di Windows: 1 MB (lebih kecil!).
Bila rekursi terlalu dalam, stack habis → stack overflow. Gejalanya:
- Program crash dengan
Segmentation fault(Linux) atauStack overflow(Windows). - Pesan error sering tidak menyebut penyebab asli.
- Tidak ada exception C++ yang dilempar — stack overflow langsung menghentikan proses.
Kode: Memicu Stack Overflow (Demonstrasi) #
#include <iostream>
// Fungsi rekursi tak berhenti — akan overflow
void infinite_recursion(int depth) {
// Safety net untuk demonstrasi (jangan dihapus)
if (depth > 100000) {
std::cout << "depth > 100000, berhenti paksa\n";
return;
}
volatile int dummy[64]; // paksa frame lebih besar
dummy[0] = depth;
if (dummy[0] < 0) return; // baca dummy agar compiler tidak optimasi
infinite_recursion(depth + 1);
}
int main() {
infinite_recursion(1);
std::cout << "selesai\n";
return 0;
}Output yang Dihasilkan:
depth > 100000, berhenti paksa
selesai- Fungsi
infinite_recursion: tanpa safety net, akan terus memanggil dirinya.dummy[64]memaksa frame 256 byte → batas ~32000 frame pada stack 8 MB. - Kata kunci
volatile: mencegah compiler mengoptimalkandummykeluar dari stack. Variabel tetap dialokasikan di memori. - Safety net
if (depth > 100000): berhenti paksa untuk demonstrasi. Tanpa safety net, program crash. - Pelajaran: rekursi yang dalam butuh perhatian khusus — gunakan iterasi atau tail recursion.
Memeriksa Ukuran Stack di Linux #
Kode: Membaca Limit Stack #
#include <iostream>
#include <sys/resource.h>
int main() {
struct rlimit rl;
getrlimit(RLIMIT_STACK, &rl);
std::cout << "soft limit stack: " << rl.rlim_cur / 1024 << " KB\n";
if (rl.rlim_max == RLIM_INFINITY) {
std::cout << "hard limit stack: unlimited\n";
} else {
std::cout << "hard limit stack: " << rl.rlim_max / 1024 << " KB\n";
}
// Estimasi maksimum frame rekursi (asumsi 64 byte/frame)
long long max_frames = rl.rlim_cur / 64;
std::cout << "estimasi maksimum frame (64 B/frame): "
<< max_frames << " frame\n";
return 0;
}Output yang Dihasilkan:
soft limit stack: 8192 KB
hard limit stack: unlimited
estimasi maksimum frame (64 B/frame): 131072 framegetrlimit(RLIMIT_STACK, &rl): system call POSIX untuk membaca batas stack.rlim_cur= batas soft (bisa dinaikkan ke hard),rlim_max= batas hard.- Cek
RLIM_INFINITY: hard limit bisa “unlimited” (RLIM_INFINITY), yang ditampilkan sebagai angka sangat besar. Kita cek eksplisit untuk mencetak teks yang ramah. - Hasil 8192 KB: stack 8 MB — default Linux.
- Estimasi 131072 frame: pada 64 byte/frame, kita bisa rekursi ~131 ribu tingkat. Bila frame lebih besar (misal 256 byte), batas turun ke ~32 ribu.
ulimit -sdi shell juga menampilkan/mengatur batas ini.
Mengubah Rekursi menjadi Iterasi dengan Stack Eksplisit #
Kode: Factorial dengan std::stack #
#include <iostream>
#include <stack>
long long factorial_iterative(int n) {
std::stack<int> call_stack;
while (n > 1) {
call_stack.push(n);
--n;
}
long long result = 1;
while (!call_stack.empty()) {
result *= call_stack.top();
call_stack.pop();
}
return result;
}
int main() {
for (int n : {5, 10, 20}) {
std::cout << "factorial(" << n << ") = "
<< factorial_iterative(n) << '\n';
}
return 0;
}Output yang Dihasilkan:
factorial(5) = 120
factorial(10) = 3628800
factorial(20) = 2432902008176640000- Loop pertama (turun): push setiap nilai
n, n-1, ..., 2ke stack. Simulasi “memanggil rekursif”. - Loop kedua (naik): pop satu per satu, kalikan ke
result. Simulasi “unwinding”. - Kompleksitas: O(n) waktu, O(n) ruang — sama dengan rekursi. Tetapi stack berada di heap, jauh lebih besar kapasitasnya.
- Hasil sesuai teori:
factorial(20) = 2432902008176640000(mendekati bataslong long).
Ringkasan #
- Setiap pemanggilan fungsi (rekursif atau biasa) membuat stack frame di call stack.
- Stack frame berisi parameter, variabel lokal, alamat pengembalian, saved registers.
- Rekursi tumbuh ke alamat rendah; base case memicu unwinding ke alamat tinggi.
- Variabel lokal bersifat independen per pemanggilan — tidak bisa diakses antar frame.
- Batas call stack di Linux: 8 MB default (cek
ulimit -s). Di Windows: 1 MB. - Untuk rekursi yang sangat dalam, gunakan iterasi dengan stack eksplisit (
std::stack<T>) yang dialokasikan di heap.