Shepmasterの答えは、私がテールコールの除去と呼んでいるテールコールの最適化がRustで発生するとは限らないことを説明しています。しかし、それだけではありません。「決して起こらない」と「保証される」の間には多くの可能性があります。コンパイラが実際のコードで何をするかを見てみましょう。
この関数で発生しますか?
現在のところ、コンパイラエクスプローラで利用できるRustの最新リリースは1.39であり、での末尾呼び出しが排除されていません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
次の行に注意してくださいcall qword ptr [rip + example::read_all@GOTPCREL]
。それが再帰呼び出しです。その存在からわかるように、除去されませんでした。
これを明示的な同等の関数と比較してくださいloop
:
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),
}
}
}
削除する末尾呼び出しがないため、1つだけが含まれる関数にコンパイルさcall
れます(計算されたアドレスにinput.read
)。
しかたがない。たぶん、RustはCほど良くないのかもしれません。
Cで発生しますか?
これは、非常によく似たタスクを実行するCの末尾再帰関数です。
int read_all(FILE *input) {
char buf[] = {0, 0};
if (!fgets(buf, sizeof buf, input))
return feof(input);
return read_all(input);
}
これは、コンパイラが排除するのが非常に簡単なはずです。再帰呼び出しは関数の下部にあり、Cはデストラクタの実行について心配する必要はありません。しかし、それにもかかわらず、再帰的な末尾呼び出しがあり、迷惑なことに排除されていません。
call read_all
Cでは、末尾呼び出しの最適化も保証されていないことがわかります。さまざまな最適化レベルでClangとgccを試しましたが、このかなり単純な再帰関数をループに変えようとはしませんでした。
それが起こることはありますか?
わかりましたので、それは保証されません。コンパイラはそれを行うことができますか?はい!これは、末尾再帰的な内部関数を介してフィボナッチ数を計算する関数です。
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)
}
末尾呼び出しが削除fibonacci_lr
されるfibonacci
だけでなく、関数全体がにインライン化され、12の命令のみが生成されます(call
見えないものではありません)。
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
あなたがいる場合、これは同等と比較while
ループ、コンパイラが生成し、ほぼ同じアセンブリを。
ポイントは何ですか?
RustまたはCのいずれかで、末尾呼び出しを排除するために最適化に依存すべきではないでしょう。それが発生するのは良いことですが、関数がタイトループにコンパイルされることを確認する必要がある場合は、少なくとも今、ループを使用することです。