Câu trả lời của Shepmaster giải thích rằng tối ưu hóa cuộc gọi đuôi, mà tôi thích gọi là loại bỏ cuộc gọi đuôi, không được đảm bảo xảy ra ở Rust. Nhưng đó không phải là toàn bộ câu chuyện! Có rất nhiều khả năng giữa "không bao giờ xảy ra" và "được bảo đảm". Chúng ta hãy xem trình biên dịch làm gì với một số mã thực.
Nó có xảy ra trong chức năng này ?
Tính đến thời điểm hiện tại, bản phát hành mới nhất của Rust có sẵn trên Compiler Explorer là 1.39 và nó không loại bỏ lệnh gọi đuôi read_all
.
example::read_all:
push r15
push r14
push rbx
sub rsp, 32
mov r14, rdx
mov r15, rsi
mov rbx, rdi
mov byte ptr [rsp + 7], 0
lea rdi, [rsp + 8]
lea rdx, [rsp + 7]
mov ecx, 1
call qword ptr [r14 + 24]
cmp qword ptr [rsp + 8], 1
jne .LBB3_1
movups xmm0, xmmword ptr [rsp + 16]
movups xmmword ptr [rbx], xmm0
jmp .LBB3_3
.LBB3_1:
cmp qword ptr [rsp + 16], 0
je .LBB3_2
mov rdi, rbx
mov rsi, r15
mov rdx, r14
call qword ptr [rip + example::read_all@GOTPCREL]
jmp .LBB3_3
.LBB3_2:
mov byte ptr [rbx], 3
.LBB3_3:
mov rax, rbx
add rsp, 32
pop rbx
pop r14
pop r15
ret
mov rbx, rax
lea rdi, [rsp + 8]
call core::ptr::real_drop_in_place
mov rdi, rbx
call _Unwind_Resume@PLT
ud2
Lưu ý dòng này: call qword ptr [rip + example::read_all@GOTPCREL]
. Đó là cuộc gọi đệ quy. Như bạn có thể nói từ sự tồn tại của nó, nó đã không bị loại bỏ.
So sánh điều này với một chức năng tương đương với một hàm rõ ràngloop
:
pub fn read_all(input: &mut dyn std::io::Read) -> std::io::Result<()> {
loop {
match input.read(&mut [0u8]) {
Ok ( 0) => return Ok(()),
Ok ( _) => continue,
Err(err) => return Err(err),
}
}
}
không có lệnh gọi đuôi để loại bỏ, và do đó biên dịch thành hàm chỉ có một call
trong đó (theo địa chỉ được tính của input.read
).
Ồ tốt Có lẽ Rust không tốt bằng C. Hay là vậy?
Nó có xảy ra ở C không?
Đây là một hàm đệ quy đuôi trong C thực hiện một nhiệm vụ rất giống nhau:
int read_all(FILE *input) {
char buf[] = {0, 0};
if (!fgets(buf, sizeof buf, input))
return feof(input);
return read_all(input);
}
Điều này sẽ siêu dễ dàng cho trình biên dịch để loại bỏ. Cuộc gọi đệ quy ở ngay dưới cùng của hàm và C không phải lo lắng về việc chạy các hàm hủy. Nhưng tuy nhiên, có cuộc gọi đuôi đệ quy , khó chịu không được loại bỏ:
call read_all
Hóa ra, tối ưu hóa cuộc gọi đuôi cũng không được đảm bảo trong C. Tôi đã thử Clang và gcc dưới các mức tối ưu hóa khác nhau, nhưng không có gì tôi đã thử sẽ biến chức năng đệ quy khá đơn giản này thành một vòng lặp.
Phải không có bao giờ xảy ra không?
Được rồi, vì vậy nó không được bảo đảm. Trình biên dịch có thể làm điều đó không? Đúng! Đây là một hàm tính toán các số Fibonacci thông qua hàm bên trong đệ quy đuôi:
pub fn fibonacci(n: u64) -> u64 {
fn fibonacci_lr(n: u64, a: u64, b: u64) -> u64 {
match n {
0 => a,
_ => fibonacci_lr(n - 1, a + b, a),
}
}
fibonacci_lr(n, 1, 0)
}
Không chỉ loại bỏ cuộc gọi đuôi, toàn bộ fibonacci_lr
chức năng được nội tuyến fibonacci
, chỉ mang lại 12 hướng dẫn (và không phải call
trong tầm nhìn):
example::fibonacci:
push 1
pop rdx
xor ecx, ecx
.LBB0_1:
mov rax, rdx
test rdi, rdi
je .LBB0_3
dec rdi
add rcx, rax
mov rdx, rcx
mov rcx, rax
jmp .LBB0_1
.LBB0_3:
ret
Nếu bạn so sánh điều này với một while
vòng lặp tương đương , trình biên dịch sẽ tạo ra gần như cùng một cụm.
Vấn đề ở đây là gì?
Có lẽ bạn không nên dựa vào tối ưu hóa để loại bỏ các cuộc gọi đuôi, trong Rust hoặc ở C. Thật tuyệt khi điều đó xảy ra, nhưng nếu bạn cần chắc chắn rằng một hàm sẽ biên dịch thành một vòng lặp chặt chẽ, ít nhất là cho Bây giờ, là sử dụng một vòng lặp.