我有一个小结构:
pub struct Foo {
pub a: i32,
pub b: i32,
pub c: i32,
}
我正在使用(a,b) (b,c) (c,a)
形式的字段对。为了避免重复代码,我创建了一个实用程序函数,它允许我迭代对:
fn get_foo_ref(&self) -> [(&i32, &i32); 3] {
[(&self.a, &self.b), (&self.b, &self.c), (&self.c, &self.a)]
}
我必须决定是否应该将值作为引用返回或复制i32
。稍后,我计划切换到非Copy
类型而不是i32
,因此我决定使用引用。我期望得到的代码应该是等价的,因为所有内容都将被内联。
我对优化一般都很乐观,所以我怀疑使用这个函数时代码与手写代码示例相比是等效的。
首先使用函数的变体:
pub fn testing_ref(f: Foo) -> i32 {
let mut sum = 0;
for i in 0..3 {
let (l, r) = f.get_foo_ref()[i];
sum += *l + *r;
}
sum
}
然后手写变体:
pub fn testing_direct(f: Foo) -> i32 {
let mut sum = 0;
sum += f.a + f.b;
sum += f.b + f.c;
sum += f.c + f.a;
sum
}
令我失望的是,所有3种方法都产生了不同的汇编代码。对于带引用的情况生成了最差的代码,最好的代码是根本没有使用我的实用程序功能的代码。这是为什么?在这种情况下,编译器不应该生成等效的代码吗?
您可以查看resulting assembly code on Godbolt;我也有相应的' assembly code from C++
在C ++中,编译器在get_foo
和get_foo_ref
之间生成了等效代码,但我不明白为什么所有3个案例的代码都不相等。
为什么编译器没有为所有3个案例生成等效代码?
更新:
我稍微修改了代码以使用数组并添加了一个直接案例。
Rust version with f64 and arrays
C++ version with f64 and arrays
这次在C ++中生成的代码完全相同。然而Rust'装配不同,通过引用返回导致更糟糕的装配。
嗯,我想这是另一个例子,没有什么可以被视为理所当然。
答案 0 :(得分:2)
TL; DR:微量标记是一种技巧,指令数不能直接转化为高/低性能。
稍后,我打算切换到非复制类型而不是i32,所以我决定使用引用。
然后,您应检查为新类型生成的程序集。
在优化的示例中,编译器非常狡猾:
pub fn testing_direct(f: Foo) -> i32 { let mut sum = 0; sum += f.a + f.b; sum += f.b + f.c; sum += f.c + f.a; sum }
收率:
example::testing_direct: push rbp mov rbp, rsp mov eax, dword ptr [rdi + 4] add eax, dword ptr [rdi] add eax, dword ptr [rdi + 8] add eax, eax pop rbp ret
大致是sum += f.a; sum += f.b; sum += f.c; sum += sum;
。
也就是说,编译器意识到:
f.X
已添加两次f.X * 2
相当于将其添加两次虽然在其他情况下可以通过使用间接禁止前者,但后者非常特定于i32
(并且添加是可交换的)。
例如,将代码切换为f32
(仍为Copy
,但添加不再是可交换的),我为testing_direct
和testing
获得了相同的程序集(testing_ref
略有不同):
example::testing: push rbp mov rbp, rsp movss xmm1, dword ptr [rdi] movss xmm2, dword ptr [rdi + 4] movss xmm0, dword ptr [rdi + 8] movaps xmm3, xmm1 addss xmm3, xmm2 xorps xmm4, xmm4 addss xmm4, xmm3 addss xmm2, xmm0 addss xmm2, xmm4 addss xmm0, xmm1 addss xmm0, xmm2 pop rbp ret
再也没有诡计了。
所以真的不可能从你的例子中推断出很多东西,检查真实的类型。