Die Antwort von Shepmaster erklärt, dass die Optimierung von Tail Calls, die ich lieber als Eliminierung von Tail Calls bezeichne, in Rust nicht garantiert ist. Aber das ist nicht die ganze Geschichte! Es gibt viele Möglichkeiten zwischen "nie passiert" und "garantiert". Schauen wir uns an, was der Compiler mit echtem Code macht.
Kommt es in dieser Funktion vor?
Ab jetzt ist die neueste Version von Rust auf Compiler Explorer ist 1,39, und es hat nicht den Schwanz Anruf zu beseitigen 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
Beachten Sie diese Zeile : call qword ptr [rip + example::read_all@GOTPCREL]
. Das ist der rekursive Aufruf. Wie Sie an seiner Existenz erkennen können, wurde es nicht beseitigt.
Vergleichen Sie dies mit einer äquivalenten Funktion mit einem explizitenloop
:
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),
}
}
}
die keinen zu eliminierenden Tail-Aufruf hat und daher zu einer Funktion mit nur einer kompiliert wird call
(zur berechneten Adresse von input.read
).
Naja. Vielleicht ist Rust nicht so gut wie C. Oder doch?
Kommt es in C vor?
Hier ist eine rekursive Funktion in C, die eine sehr ähnliche Aufgabe ausführt:
int read_all(FILE *input) {
char buf[] = {0, 0};
if (!fgets(buf, sizeof buf, input))
return feof(input);
return read_all(input);
}
Dies sollte für den Compiler sehr einfach zu beseitigen sein. Der rekursive Aufruf befindet sich ganz unten in der Funktion und C muss sich nicht um die Ausführung von Destruktoren kümmern. Trotzdem gibt es diesen rekursiven Tail Call , der ärgerlicherweise nicht beseitigt wird:
call read_all
Es stellt sich heraus, dass die Tail-Call-Optimierung auch in C nicht garantiert ist. Ich habe Clang und gcc unter verschiedenen Optimierungsstufen ausprobiert, aber nichts, was ich versucht habe, würde diese ziemlich einfache rekursive Funktion in eine Schleife verwandeln.
Passiert das jemals ?
Okay, das ist nicht garantiert. Kann der Compiler das überhaupt? Ja! Hier ist eine Funktion, die Fibonacci-Zahlen über eine schwanzrekursive innere Funktion berechnet:
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)
}
Der Tail-Aufruf wird nicht nur eliminiert, fibonacci_lr
sondern die gesamte Funktion wird eingebunden fibonacci
, was nur 12 Anweisungen ergibt (und keine call
in Sicht ist):
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
Wenn Sie dies mit einer äquivalenten while
Schleife vergleichen , generiert der Compiler fast dieselbe Assembly.
Was ist der Punkt?
Sie sollten sich wahrscheinlich nicht auf Optimierungen verlassen, um Tail Calls zu eliminieren, weder in Rust noch in C. Es ist schön, wenn es passiert, aber wenn Sie sicher sein müssen, dass eine Funktion in einer engen Schleife kompiliert wird, ist dies zumindest für den sichersten Weg Jetzt ist eine Schleife zu verwenden.