Rust 编译器没有优化 lzcnt? (和类似的功能)
Rust compiler not optimising lzcnt? (and similar functions)
完成了什么:
这是在 Compiler Explorer 上进行实验的结果,以确定编译器 (rustc) 在涉及 log2()
/leading_zeros()
和类似函数时的行为。我发现这个结果似乎既奇怪又令人担忧:
代码:
pub fn lzcnt0(val: u64) -> u64 {
val.leading_zeros() as u64
}
pub unsafe fn lzcnt1(val: u64) -> u64 {
core::arch::x86_64::_lzcnt_u64(val)
}
pub unsafe fn lzcnt2(val: u64) -> u64 {
asm_lzcnt(val)
}
#[inline]
pub unsafe fn asm_lzcnt(val: u64) -> u64 {
let lzcnt: u64;
core::arch::asm!("lzcnt {}, {}", in(reg) val, lateout(reg) lzcnt, options(nomem, nostack));
lzcnt
}
输出:
example::lzcnt0:
test rdi, rdi
je .LBB0_2
bsr rax, rdi
xor rax, 63
ret
.LBB0_2:
mov eax, 64
ret
example::lzcnt1:
jmp core::core_arch::x86_64::abm::_lzcnt_u64
core::core_arch::x86_64::abm::_lzcnt_u64:
lzcnt rax, rdi
ret
example::lzcnt2:
lzcnt rdi, rax
ret
编译器选项是为了最好地模拟 cargo 的 'release' 配置(使用 opt-level=3 作为一个很好的衡量标准),否则尽我最大的努力让编译器优化功能。具体目标应该无关紧要,只要它针对 x86-64,我已经尝试过 x86_64-{pc-windows-{msvc,gnu},unknown-linux-gnu}
.
预期结果:
所有这些输出都应与 lzcnt2
相同。 Instruction Performance Tables lzcnt
显然是一个全面的快速指令,应该使用,在如此低级的功能中有一个不必要的分支是令人沮丧的。更奇怪的是,函数 _lzcnt_u64()
在幕后调用 leading_zeros()
- 编译器很乐意将其魔术化(也没有检查或断言),但似乎不会为底层函数执行此操作。更何况,即使在那种情况下,编译器也不会内联 lzcnt
指令? (实现也标记了函数 a #[inline]
)当然,a jmp
没有那么糟糕,但完全没有必要,应该避免。
它可能是什么:
- 编译器错误?
- 有目的的选择我不懂?
- 我不明白如何正确使用 Compiler Explorer?
- 其他?
我在 log2
和(我想)其他依赖 ctlz
rust 编译器实现的函数中看到了类似的结果。
如果您充分了解编译器,将不胜感激。出于某种原因,我不喜欢编写大量实用函数,但如果没有更好的选择,我会这样做。
P.S。如果您的回答是在大多数情况下性能提升可以忽略不计,and/or 由于代码质量或类似推理,我不应该关心:我理解这种情绪,但这不是这个问题的重点.我正在为个人项目中的裸机热代码编写代码。
旧的 x86-64 CPUs 不支持 lzcnt
,所以 rustc/llvm 默认不会发出它。 (他们会以 bsr
的形式执行它,但行为并不相同。)
使用-C target-feature=+lzcnt
启用它。 Try.
更一般地说,您可能希望使用 -C target-cpu=XXX
来启用特定 CPU 模型的所有功能。使用 rustc --print target-cpus
作为列表。
特别是,-C target-cpu=native
将为 rustc 本身 运行 正在运行的 CPU 生成代码,例如如果您将 运行 代码放在您正在编译它的同一台机器上。
完成了什么:
这是在 Compiler Explorer 上进行实验的结果,以确定编译器 (rustc) 在涉及 log2()
/leading_zeros()
和类似函数时的行为。我发现这个结果似乎既奇怪又令人担忧:
代码:
pub fn lzcnt0(val: u64) -> u64 {
val.leading_zeros() as u64
}
pub unsafe fn lzcnt1(val: u64) -> u64 {
core::arch::x86_64::_lzcnt_u64(val)
}
pub unsafe fn lzcnt2(val: u64) -> u64 {
asm_lzcnt(val)
}
#[inline]
pub unsafe fn asm_lzcnt(val: u64) -> u64 {
let lzcnt: u64;
core::arch::asm!("lzcnt {}, {}", in(reg) val, lateout(reg) lzcnt, options(nomem, nostack));
lzcnt
}
输出:
example::lzcnt0:
test rdi, rdi
je .LBB0_2
bsr rax, rdi
xor rax, 63
ret
.LBB0_2:
mov eax, 64
ret
example::lzcnt1:
jmp core::core_arch::x86_64::abm::_lzcnt_u64
core::core_arch::x86_64::abm::_lzcnt_u64:
lzcnt rax, rdi
ret
example::lzcnt2:
lzcnt rdi, rax
ret
编译器选项是为了最好地模拟 cargo 的 'release' 配置(使用 opt-level=3 作为一个很好的衡量标准),否则尽我最大的努力让编译器优化功能。具体目标应该无关紧要,只要它针对 x86-64,我已经尝试过 x86_64-{pc-windows-{msvc,gnu},unknown-linux-gnu}
.
预期结果:
所有这些输出都应与 lzcnt2
相同。 Instruction Performance Tables lzcnt
显然是一个全面的快速指令,应该使用,在如此低级的功能中有一个不必要的分支是令人沮丧的。更奇怪的是,函数 _lzcnt_u64()
在幕后调用 leading_zeros()
- 编译器很乐意将其魔术化(也没有检查或断言),但似乎不会为底层函数执行此操作。更何况,即使在那种情况下,编译器也不会内联 lzcnt
指令? (实现也标记了函数 a #[inline]
)当然,a jmp
没有那么糟糕,但完全没有必要,应该避免。
它可能是什么:
- 编译器错误?
- 有目的的选择我不懂?
- 我不明白如何正确使用 Compiler Explorer?
- 其他?
我在 log2
和(我想)其他依赖 ctlz
rust 编译器实现的函数中看到了类似的结果。
如果您充分了解编译器,将不胜感激。出于某种原因,我不喜欢编写大量实用函数,但如果没有更好的选择,我会这样做。
P.S。如果您的回答是在大多数情况下性能提升可以忽略不计,and/or 由于代码质量或类似推理,我不应该关心:我理解这种情绪,但这不是这个问题的重点.我正在为个人项目中的裸机热代码编写代码。
旧的 x86-64 CPUs 不支持 lzcnt
,所以 rustc/llvm 默认不会发出它。 (他们会以 bsr
的形式执行它,但行为并不相同。)
使用-C target-feature=+lzcnt
启用它。 Try.
更一般地说,您可能希望使用 -C target-cpu=XXX
来启用特定 CPU 模型的所有功能。使用 rustc --print target-cpus
作为列表。
特别是,-C target-cpu=native
将为 rustc 本身 运行 正在运行的 CPU 生成代码,例如如果您将 运行 代码放在您正在编译它的同一台机器上。