在'string=string+s1'和"string+=s1"之间移动语义可以保存多少个复制操作?
How many copy operations can move semantics save between 'string=string+s1' and 'string+=s1'?
在这个堆栈溢出问题中(不幸的是,自从我开始研究这个问题以来,这个问题一直被搁置),有几个人提到"在现代C++"中",由于移动语义,编译后的代码不需要复制操作string = string + s1
的字符串,给人的印象是,使用现代C++编译器,string = string + s1
可以和string += s1
一样高效。我觉得这种说法很可疑,但我在 C++03 的遗留世界中工作,仍然对移动语义知之甚少。(这是我的工作,我不选择我们的编译器。
string += s1
成本
在操作string += s1
时,除非string
缓冲区的扩展超过其先前分配的容量,否则不需要新的分配,并且假设字符串类的合理实现,则在操作string += s1
中不会创建临时对象。假设结果的大小适合先前分配的容量,则string += s1
最昂贵的部分是使用先前分配但未使用的空间将s1
的内容追加(复制)到string
的原始内容的末尾。另请注意,该复制操作的成本只是s1
的字节数,而不是总字节数。
旧版C++string = string + s1
成本(C++03 及更早版本)
在遗留C++(03 及更早版本)中,string = string + s1
,根据我的理解,至少需要一个临时分配(用于评估string + s1
),以及两个s1
字节数和原始string
之和的完整副本(1.将string
的原始内容复制到临时,并将s1
的字节复制到临时这些字节的末尾, 然后2.将所有生成的字节从临时复制回string
的缓冲区,包括原始内容的字节,这些字节已经存在)。显然,这比上面描述的string += 1
成本要昂贵得多,并且可能会产生显着差异,特别是如果追加操作在循环中多次执行(这是画家的Shlemiel算法,除了它甚至比strcat()
的低效率还要糟糕!
"现代"C++的string = string + s1
成本(C++11(或C++14?)及以后)
在我看来,表达式string + s1
的计算将产生一个右值,随后可以作为右值引用提供给string
的移动赋值运算符,从而消除了将string + s1
的结果复制回string
的需要。但这并不能消除创建原始临时对象及其相关复制的需要,不是吗?在我看来,移动语义所能做的最好的事情就是消除两个完整复制操作中的一个。仍然需要一个分配和一个复制操作(创建临时操作),对吗?如果是这样,那么移动语义只会使代码"不那么糟糕",它并不能阻止它成为画家 Shlemiel 算法的另一个实例,而且它仍然比 C 的 strcat() 更糟糕!
请告诉我我错了。另外:请不要猜测。这是没有帮助的。如果您的回答或评论以"我认为..."开头,请不要提交。
仍然需要一个分配和一个复制操作(创建临时),对吧?如果是这样,那么移动语义只会使代码"不那么糟糕">
据我所知,你是对的。移动语义并没有消除此示例中的所有开销,只有一些开销。
如果string
没有足够的容量,则无法避免重新分配和复制所有字符。但是,如果有足够的容量,为了获得最佳性能,无论标准版本如何,您应该做的是:
string.append(s1); // this
string += s1; // or this
如果容量足够,则不会进行重新分配,也不会复制原始string
的字符。与strcat
相同。与strcat
不同,如果他的能力不足,行为是明确的。
在循环中执行时,理想情况下应在循环之前保留全部容量。如果无法知道结果长度,则存在一个潜在的问题:据我所知,标准没有指定追加的容量增长策略。如果它确实进行几何增长,那么带有附加的平凡循环是好的。如果它只分配完全旧的长度 + 附加长度而没有空间开销,那么简单的追加循环将导致每次迭代中的分配。
我测试了libstdc++,它确实是几何增长。如果你是偏执狂,那么为了保证这一点,你需要在技术上明确检查是否会在每次迭代时发生重新分配,并保留一个几何增长的内存量:
// paranoid mode: enabled
int growth_rate = 2;
for(auto& s1 : range) {
auto new_size = string.size() + s1.size();
auto new_cap = string.capacity();
while (new_cap < new_size)
new_cap *= growth_rate;
string.reserve(new_cap);
string += s1;
}
但是,在这种特殊情况下,std::ostringstream
可能更简单。
出于此答案的目的,我忽略了 SSO(小字符串优化),并假设所有字符串大小都大到需要动态内存分配。 这让我们得到了更多的苹果对苹果的比较。
让我们看看string += s1
. 执行此操作时,字符串将根据是否有足够的可用空间调整大小,然后将s1
的内容复制到string
中。 这意味着您最多可以拥有 O(N+M) 字符副本和分配。 这是你能得到的最好的。
现在我们将检查 C++03string = string + s1
. 在这里,您将创建一个临时字符串,为缓冲区进行分配,然后如果没有足够的容量,则string
另一个分配,以及临时分配的副本。 这意味着在最坏的情况下,您有 2 个分配和 O(2(N+M)) 字符副本。 这远不如第一种情况。
最后我们有 C++11+string = string + s1
. 同样,我们必须创建一个临时的,进行分配并将string
复制到其中并s1
。 与 C++03 不同,我们不必复制回string
,因为我们有移动语义。 这意味着在最坏的情况下,您有一个分配,O(N+M)字符副本,以及一些临时字符串成员与string
的复制/交换。 这不如第一种情况,但比C++03好多了。
这一切意味着什么? 使用string += s1
. 它是string.append(s1)
的缩写形式,由于不需要创建临时,因此可提供最佳性能。
std::string s0 = "whatever length";
std::string s1 = "whatever length";
std::string s = s0+s1;
这需要零临时人员。
s = s0+s1;
这需要 1 个临时的,但该临时没有可观察到的效果,因此编译器可以自由地消除它的创建。 这很有挑战性;我不知道在这种情况下有编译器可以将其关闭,但标准明确允许。
s = s + s1;
与前一种情况相同。
s += s1;
如果s
和s1
的长度足够短,则不涉及分配。
std::string short_string = "short"; // so short that 2 copies will fit in SBO
std::string long_string = "some long string that won't fit in SBO";
s = short_string + short_string;
这可以通过零内存分配或副本来完成。
s = s + short_string;
如果s + short_string
适合 SBO,则可以使用零内存分配或副本来完成此操作。 这比前一种情况更具挑战性。
s += short_string;
如果总和很短,或者如果它适合缓冲区,这很容易优化分配。
所以这里有一些事情发生。
首先,C++明确允许编译器消除new
分配。 因此,如果编译器能够很好地理解这些类型,它可以看到临时new
缓冲区仅用作交换空间,并且可以确定在 as-if 规则下它不需要它。
第二件事是省略。 消除是允许的
std::string s = s0+s1;
s0+s1
曾经是一个临时对象;现在,它是一个 prvalue 表达式,必须根据 C++17 规则与此处的std::string s
对象合并。 那里不再有单独的对象。
在这一点上,PRV值表达式有点像可移植的构造指令。某些操作可以强制它们具体化临时,但使用它们构造匹配类型的值不会具体化临时。
在 c++17 之前,即使在 c++03 中,这里也可以发生 elision,并且s0+s1
的返回值可以(并且通常确实)用std::string s
省略其恒等式;这就是为什么 prvalue 表达式规则通常被描述为"保证省略"。
现在,其他感兴趣的是:
SomeType s = s0 + s1 + s2 + s3 + s4 + s5;
如果移动SomeType
成本低,并且operator+
写得正确,这可能涉及零浪费的临时缓冲区。
假设我们有
SomeType operator+( SomeType lhs, SomeType const& rhs ) {
lhs += rhs;
return lhs;
}
而且SomeType(SomeType&&)
非常便宜,而且是免费的(就像搬出SomeType
~SomeType
一样)。
然后
SomeType s = s0 + s1 + s2 + s3 + s4 + s5;
与
SomeType s = s0;
s += s1;
s += s2;
s += s3;
s += s4;
s += s5;
直至可能生成的汇编代码。
现在
s = s0 + s1 + s2 + s3 + s4 + s5;
不如
s += s0; s += s1; s += s2; s += s3; s += s4; s += s5;
因为第一个不能重用s
中已经存在的存储(假设它足够),而第二个可以。
TL;DR有几种不同的s = s + s1
;在某些情况下,s += s1
更有效率,而在其他情况下则不然。
在几乎所有情况下,标准都允许它同样高效,但编译器不太可能这样做。 在某些情况下,它既被允许具有同等的效率,也可能具有相同的效率。
- 在全局变量中保存类的实例以重新创建类(创建"backup")
- 如何在选项卡视图Qt中设置一个新项目,并保存以前的项目
- 如何使用OpenCV将RBG图像转换为HSV,并将H、S和V值保存为C++中的3个独立图像
- 复制列表初始化的隐式转换的等级是多少
- while循环中while循环的时间复杂度是多少
- 将字符指针十六进制转换为字符串并保存在文本文件C++中
- 在没有定义返回类型的函数中返回布尔值,并将结果保存在无错误的char编译中-为什么
- EvtExportLogneneneba API正在将远程计算机的事件日志保存到远程PC本身.如何将其保存到主机
- 如何检查一个c++字符串中有多少相同的字符/数字
- 在C++中将类(带有Vector成员)保存为二进制文件
- C++有多少类型的循环
- C++.我想将更改后的特定字符行保存在字符串中
- 使用FFMPEG将RGB图像序列保存到.mp4时出现问题
- 求出有多少个数字是完美平方,而sqrt()是L,R范围内的素数
- 将RGB图像保存为PPM格式
- 哈希文件递归并保存到矢量Cryptopp中
- 如何将二进制格式的 C++ 对象的 std::vector 保存到磁盘?
- 在条件变量中触发错误信号的频率是多少
- 在'string=string+s1'和"string+=s1"之间移动语义可以保存多少个复制操作?
- 一个DWORD可以保存多少个标志