C++字符串按值、引用与右值传递的性能对比及疑问
嘿,你的这个测试结果确实挺反直觉的,咱们来好好唠唠背后的门道:
小字符串场景的反常表现
你发现短字符串时StringMove()居然比涉及拷贝的StringCopy()还慢,甚至不如StringCreation()和StringReference(),核心原因是C++标准库的短字符串优化(SSO)。
现在主流的std::string实现(比如GCC、Clang的标准库)都有SSO机制:当字符串长度小于某个阈值(一般是15或22字节,具体看库实现)时,字符串内容会直接存在std::string对象本身的栈内存里,不会去堆上分配空间。
那看你的StringMove()代码:
void StringMove() { std::string msg("hello "); add(std::move(msg)); }
add的参数是按值传递的std::string,所以会用std::move(msg)触发移动构造来初始化参数。但因为原字符串是小字符串("hello "只有6个字符,在SSO范围内),移动构造根本没法“移动”堆内存——因为本来就没堆内存可移!这时候移动构造的实际操作和拷贝构造完全一样:把msg对象内部的栈上字符数组复制给函数参数。
雪上加霜的是,std::move()虽然只是个类型转换,但它会让编译器放弃一些可能的优化(比如返回值优化RVO或者省略构造的优化)。而StringCreation()里的add(std::string("hello ")),编译器可以直接在add的参数位置构造临时字符串,甚至把整个操作内联,完全避免构造开销;StringReference()是直接传引用,自然也没构造成本。这就导致小字符串时StringMove()的表现反而最差。
长字符串场景的预期结果
当字符串长度超过SSO阈值后,std::string会在堆上分配内存。这时候移动构造才真正体现优势:它只需要把原字符串的堆指针、长度、容量这些成员变量复制过去,再把原对象置空,几乎没什么开销。而StringCopy()要完整复制堆上的所有字符,开销一下就上来了,所以这时候StringMove()的表现就接近StringCreation()和StringReference(),符合咱们的预期。
对你疑问的解答
你担心的“先移动构造再拷贝构造”其实不存在,StringMove()只执行一次移动构造。但在SSO场景下,这个移动构造的行为等价于拷贝构造,没有真正的堆内存转移,所以开销和拷贝差不多,再加上std::move带来的优化限制,才显得比其他方式慢。
另外要记住:字符串的移动构造开销低是针对非SSO的长字符串而言的,小字符串因为没有堆内存可移,移动和拷贝的成本是一样的。
内容的提问来源于stack exchange,提问作者jignatius

