背景
在C++(C++11)中有了移动语义后,标准库中的很多容器都实现了原地构建对象的接口, 例如 vector::emplace_pack。 得益于此,在适当的场合下,可以大量减少临时对象的创建和对象拷贝, 从而提升程序性能。因此想到rust应该也有类似的机制, 来避免不必要的对象创建和拷贝开销。
C++移动语义:将一个右值(比如一个临时对象表达式)传给一个函数的时候,是优先绑定到参数形式是右值引用的函数上的
测试验证
以Arc对象创建为测试例子:
+ 创建临时变量,传入临时变量创建Arc: create_arc_with_temp_var
+ 原地创建, 直接在new入参里创建对象:create_arc_inplace
猜测原地创建方式会使用移动语义,不会有对象拷贝发生。通过临时变量创建, 会发生一次对象拷贝,性能下降。
以下为完整测试代码:
use std::sync::atomic::AtomicI64;
static GVALUE: AtomicI64 = AtomicI64::new(0);
const BUFFER_SIZE:usize = 10240;
struct TestClass {
a: i64,
b: i64,
buf: [u8; BUFFER_SIZE], // 使用大数组来模拟超大对象
}
/// 创建普通对象,参考组
fn create_normal() -> TestClass {
let v = GVALUE.fetch_add(1, std::sync::atomic::Ordering::SeqCst); // 防止编译器优化,下同
TestClass{ a: v, b:0, buf: [0;BUFFER_SIZE]}
}
fn create_arc_with_temp_var() -> std::sync::Arc<TestClass> {
let v = GVALUE.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let obj = TestClass{ a: v, b:0, buf: [0;BUFFER_SIZE]};
std::sync::Arc::new(obj)
}
fn create_arc_inplace() -> std::sync::Arc<TestClass> {
let v = GVALUE.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
std::sync::Arc::new(TestClass{ a: v, b:0, buf: [0; BUFFER_SIZE]})
}
pub fn test_main() {
let repeat: usize = 1000000;
let mut sum: i64 = 0;
let mut bufsize:usize = 0;
let start_time = std::time::SystemTime::now();
for _ in 0..repeat {
let obj = create_normal();
// 下面两句避免编译器警告和阻止编译器优化, 下同。
sum += obj.a + obj.b;
bufsize += obj.buf.len();
}
println!("create normal: {:?}", start_time.elapsed().unwrap());
sum = 0;
bufsize = 0;
let start_time = std::time::SystemTime::now();
for _ in 0..repeat {
let obj = create_arc_with_temp_var();
sum += obj.a + obj.b;
bufsize += obj.buf.len();
}
println!("create arc with temp var: {:?}", start_time.elapsed().unwrap());
sum = 0;
bufsize = 0;
let start_time = std::time::SystemTime::now();
for _ in 0..repeat {
let obj = create_arc_inplace();
sum += obj.a + obj.b;
bufsize += obj.buf.len();
}
println!("create arc inplace: {:?}", start_time.elapsed().unwrap());
println!("done {}-{}", sum, bufsize);
}
fn main() {
test_main();
}
- 保存为 test.rs, 直接使用rustc进行默认选项编译:
rustc test.rs -o test.exe
, 执行test.exe结果:
create normal: 634.3058ms
create arc with temp var: 2.1003936s
create arc inplace: 1.5110131s
可以看出, 确实如猜测一样,原地创建的方式更快。
生成中间汇编文件rustc --emit asm=test.S test.rs
,从汇编看是否有对象拷贝发生:
create_arc_with_temp_var
.seh_proc _ZN7arctest24create_arc_with_temp_var17heca6a2e43008164bE
movl $30808, %eax
callq __chkstk
subq %rax, %rsp
.seh_stackalloc 30808
.seh_endprologue
movb $4, 55(%rsp)
movb 55(%rsp), %r8b
leaq _ZN7arctest6GVALUE17h56fc0879255456eaE(%rip), %rcx
movl $1, %edx
callq _ZN4core4sync6atomic9AtomicI649fetch_add17hc2e5755d85a2d4e3E
movq %rax, 40(%rsp)
leaq 10312(%rsp), %rcx
xorl %edx, %edx
movl $10240, %r8d
callq memset
movq 40(%rsp), %rax
movq %rax, 56(%rsp)
movq $0, 64(%rsp)
leaq 56(%rsp), %rcx
addq $16, %rcx
leaq 10312(%rsp), %rdx
movl $10240, %r8d
callq memcpy
leaq 20552(%rsp), %rcx
leaq 56(%rsp), %rdx
movl $10256, %r8d
callq memcpy # 多了一次拷贝
leaq 20552(%rsp), %rcx
callq _ZN5alloc4sync12Arc$LT$T$GT$3new17hbf63ab203eef9e84E
movq %rax, 32(%rsp)
movq 32(%rsp), %rax
addq $30808, %rsp
retq
.seh_endproc
create_arc_inplace
seh_proc _ZN7arctest18create_arc_inplace17hfe33c4907cd49bc4E
movl $20552, %eax
callq __chkstk
subq %rax, %rsp
.seh_stackalloc 20552
.seh_endprologue
movb $4, 55(%rsp)
movb 55(%rsp), %r8b
leaq _ZN7arctest6GVALUE17h56fc0879255456eaE(%rip), %rcx
movl $1, %edx
callq _ZN4core4sync6atomic9AtomicI649fetch_add17hc2e5755d85a2d4e3E
movq %rax, 40(%rsp)
leaq 10312(%rsp), %rcx
xorl %edx, %edx
movl $10240, %r8d
callq memset
movq 40(%rsp), %rax
movq %rax, 56(%rsp)
movq $0, 64(%rsp)
leaq 56(%rsp), %rcx
addq $16, %rcx
leaq 10312(%rsp), %rdx
movl $10240, %r8d
callq memcpy
leaq 56(%rsp), %rcx
callq _ZN5alloc4sync12Arc$LT$T$GT$3new17hbf63ab203eef9e84E
movq %rax, 32(%rsp)
movq 32(%rsp), %rax
addq $20552, %rsp
retq
.seh_endproc
从汇编来看, 临时变量的创建方式确实是多了一次拷贝。但是栈上到堆上的拷贝依然发生了:
std::sync::Arc::new
.seh_proc _ZN5alloc4sync12Arc$LT$T$GT$3new17hbf63ab203eef9e84E
.seh_handler __CxxFrameHandler3, @unwind, @except
pushq %rbp
.seh_pushreg %rbp
movl $10416, %eax
callq __chkstk
subq %rax, %rsp
.seh_stackalloc 10416
leaq 128(%rsp), %rbp
.seh_setframe %rbp, 128
.seh_endprologue
movq $-2, 10280(%rbp)
movq %rcx, -40(%rbp)
movb $0, 10263(%rbp)
movb $1, 10263(%rbp)
movl $10272, %ecx
movl $8, %edx
callq _ZN5alloc5alloc15exchange_malloc17h3c474a0026f2bb85E
movq %rax, -32(%rbp)
.Ltmp10:
movl $1, %ecx
callq _ZN4core4sync6atomic11AtomicUsize3new17hf7ef8b97eb7f268bE
.Ltmp11:
movq %rax, -24(%rbp)
jmp .LBB74_1
.LBB74_1:
movq -24(%rbp), %rax
movq %rax, 10264(%rbp)
movq 10264(%rbp), %rax
movq %rax, -16(%rbp)
.Ltmp12:
movl $1, %ecx
callq _ZN4core4sync6atomic11AtomicUsize3new17hf7ef8b97eb7f268bE
.Ltmp13:
movq %rax, -48(%rbp)
jmp .LBB74_3
.LBB74_3:
movq -40(%rbp), %rdx
movq -48(%rbp), %rax
movq %rax, 10272(%rbp)
movq 10272(%rbp), %rax
movq %rax, -8(%rbp)
movb $0, 10263(%rbp)
movq %rbp, %rcx
movq %rcx, -72(%rbp)
movl $10256, %r8d
movq %r8, -64(%rbp)
callq memcpy # 栈到堆拷贝
# ...
说明rust并没有实现真正的原地创建(类似C++原地构造机制)。
- 开启编译优化,再来看结论是否一致:
rustc test.rs -o test_opt.exe -O
create normal: 11.3087ms
create arc with temp var: 422.8822ms
create arc inplace: 436.6645ms
时间消耗基本一致, 应该是编译器把这个拷贝优化掉了。感兴趣的可以自行分析汇编。
结论
rustc不支持原地构建,不管以何种方式创建Arc依然会发生栈到堆的拷贝,但其编译器优化对临时变量的优化还是比较到位的。 尽管编译器优化足够优秀,我们在写代码时,还是应该尽量减少临时变量的创建,尽可能使用原地构建的方式, 避免在特定场景下编译器优化生效。
注:测试所用的rustc版本 rustc 1.56.0 (09c42c458 2021-10-18)