【发布时间】:2022-11-13 13:16:23
【问题描述】:
以下two functions 生成非常不同的结果汇编语言:
pub struct X {
a: u64,
b: u64,
c: u64,
d: u64,
e: u64,
f: u64,
}
pub fn f(a: u8, x: X) -> u64 {
[
(0b000001, x.a),
(0b000010, x.b),
(0b000100, x.c),
(0b001000, x.d),
(0b010000, x.e),
(0b100000, x.f),
]
.into_iter()
.find(|(bb, _)| (*bb & a) != 0)
.map_or(0, |(_, m)| m)
}
pub fn g(a: u8, x: X) -> u64 {
match a {
_ if (a & 0b000001) != 0 => x.a,
_ if (a & 0b000010) != 0 => x.b,
_ if (a & 0b000100) != 0 => x.c,
_ if (a & 0b001000) != 0 => x.d,
_ if (a & 0b010000) != 0 => x.e,
_ if (a & 0b100000) != 0 => x.f,
_ => 0,
}
}
他们做同样的事情:基于位模式,返回正确的值。我更喜欢f,因为它将数据和逻辑分开,但会导致组装质量差。因为我正在运行模拟,所以一点就是很多。 (参见上面的操场链接的程序集,生成发布 asm)
在f 中,Rust 不必要地在内存中构建数组,而不是识别出这些值已被使用并立即丢弃。 g 将数据和逻辑混合在一起,但 Rust 只是简单地进行比较然后返回结果,正如您所期望的那样。
有什么我可以做的来帮助这个迭代器风格的代码生成更好的代码,还是我最好写命令式风格?
【问题讨论】:
-
不是对您问题的直接回答,但看起来您可以在此处使用
leading_zeros()。 -
@DanGetz - 哈哈,是的,在这种情况下。不幸的是,我有更复杂的评估。不确定 ctz 无论如何会如何简化这一点,因为我只是在比较位。
-
有趣的是,如果您不自己预加载值而是使用引用,它们会生成几乎相同的程序集:playground。也许更容易预先优化引用的固定偏移量,而不是尝试回溯原始值的来源以省略数组。
-
此外,您可能会从通过引用而不是通过值传递
X获得一些性能优势,因为这会减少寄存器压力/堆栈移动,而且我怀疑如果它已经在缓存中,间接会花费任何代价。但是,当然,测量! -
“……还是我写命令式风格更好?”- 我个人认为无论如何这里的匹配版本比迭代版本要清晰得多。