GNU STL 字符串:这里是否涉及写时复制?

14

(免责声明:我不知道C++标准可能对此有何规定...我知道,我很糟糕)

在操作非常大的字符串时,我注意到std::string使用了写时复制技术。我成功编写了最简循环以重现观察到的行为,例如以下循环运行得异常快速:

#include <string>
using std::string;
int main(void) {
    string basestr(1024 * 1024 * 10, 'A');
    for (int i = 0; i < 100; i++) {
        string a_copy = basestr;
    }
}

在循环体中添加一次写操作a_copy[1] = 'B';后,实际上进行了一次复制,程序运行时间从几毫秒变为了0.3秒。100个写操作使其变慢了约100倍。

但之后情况变得奇怪起来。我的某些字符串只读取而没有写入,但这并没有反映在执行时间上,执行时间几乎完全与字符串操作的数量成正比。经过一些挖掘,我发现仅从字符串中读取数据仍然会导致性能下降,因此我推断 GNU STL 字符串使用了按需拷贝(copy-on-read)的技术。

#include <string>
using std::string;
int main(void) {
    string basestr(1024 * 1024 * 10, 'A');
    for (int i = 0; i < 100; i++) {
        string a_copy = basestr;
        a_copy[99]; // this also ran in 0.3s!
    }
}

在我发现这个问题后兴奋了一段时间,但随后发现从基础字符串中使用operator[]进行读取也需要0.3秒的时间,我对此并不完全满意。STL字符串确实是按照读取时复制(copy-on-read)的吗?或者它们是否允许写入时复制(copy-on-write)?我认为operator[]有一些防范措施来防止其返回的引用被保留并在稍后被写入,这真的是这种情况吗?如果不是,那么真正发生了什么?如果有人可以指出C++标准中的相关部分,那也将不胜感激。

供参考,我使用的是g++ (Ubuntu 4.4.3-4ubuntu5) 4.4.3和GNU STL。


我想这与OP使用的特定STL实现有关,不仅仅是编译器的问题。从标准的角度来看,我认为Charles Bailey已经回答了。 - Raj
@Ranju V:对于每个示例,各种优化级别(-O1到3,-Os)的执行时间保持不变。 - Michael Foukarakis
1
@Roger:这非常有趣!因此,虽然标准没有强制要求,但std::string实际上可以使用COW,是吗? - Michael Foukarakis
在C++中,你不能为读和写操作分别使用不同的索引运算符(虽然可以通过代理类来应付,但它们会给你带来更多罕见、更丑陋的问题)。就好像编译器无法区分在使用[]时的读取和写入操作。 - peterchen
7
想指出的是,随着移动语义的引入(使得许多典型使用情况下COW变得过时)和并发性的增强(由于同步问题可能导致COW非常低效),C++0x中的写时复制可能会逐渐消失。 - fredoverflow
显示剩余2条评论
3个回答

14

C++并不区分用于读写的operator[],而只区分常量对象和可变(非常量)对象所使用的operator[]。因为a_copy是可变的,所以将选择可变的operator[],这将强制进行复制,因为该运算符返回一个(可变的)引用。

如果效率是个问题,你可以将a_copy转换为const string,以强制使用const版本的operator[],这样就不会对内部缓冲区进行复制了。

char f = static_cast<const string>(a_copy)[99];

我根本没有考虑过const的因素。谢谢你。效率在这里并不是我的主要关注点,我想重点了解GNU STL的内部结构。了解你的工具等等。 :) - Michael Foukarakis
2
你应该使用const_cast<>(http://msdn.microsoft.com/en-us/library/bz6at95h(VS.80).aspx)进行CV转换。 - J-16 SDiZ
@DeadMG,不是的。请参考(草案)标准:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3126.pdf [5.2.11] [expr.const.cast],脚注(73)指出“const_cast 不仅限于转换去除 const 限定符”。请参见 5.2.11#3,了解此特定情况。 - J-16 SDiZ
@DeadMG:J-16是正确的——你的逻辑有误。关键不在于其他类型转换可以添加const,而在于这些其他类型转换甚至可以做更多的事情,而const_cast<>不能。在类型转换中,通常尝试使用最不强大的类型转换运算符,以便在模板参数意味着任何比预期更大的更改时获得编译器警告。 - Tony Delroy
@DeadMG:1)如果事物一开始不是const,那么你就不能无意中删除const。2)添加和删除const是const_cast<>的作用-使用它是程序员确认其意图的方式,并且比任何不够自我说明和限制性较小的替代方法更安全。 - Tony Delroy
显示剩余3条评论

13

C++标准并不禁止或强制使用写时复制或任何其他实现细节来实现std::string。只要满足语义和复杂度要求,实现可以选择任何实现策略。

请注意,对于非const字符串的operator[]实际上是一个“写”操作,因为它返回一个引用,可以在下一次改变字符串之前的任何时间点上修改该字符串。这样的修改不应影响任何副本。

您尝试过对其中一个进行分析吗?

const string a_copy = basestr;
a_copy[99];

或者

string a_copy = basestr;
const std::string& a_copy_ref = a_copy;
a_copy_ref[99];

实际上,这两个循环体与第一个示例一样快;也就是说,只需要几毫秒。 - Michael Foukarakis

3

试一试这段代码:

#include <iostream>
#include <iomanip>
#include <string>

using namespace std;

template<typename T>
void dump(std::ostream & ostr, const T & val)
{
    const unsigned char * cp = reinterpret_cast<const unsigned char *>(&val);
    for(int i=0; i<sizeof(T); i++)
        ostr
            << setw(2) << setfill('0') << hex << (int)cp[i] << ' ';
    ostr << endl;
}

int main(void) {
    string a = "hello world";
    string b = a;
    dump(cout,a);
    dump(cout,b);

    char c = b[0];

    dump(cout,a);
    dump(cout,b);
}

在GCC上,我得到了以下输出:
3c 10 51 00
3c 10 51 00
3c 10 51 00
5c 10 51 00

这似乎表明在这种情况下它们是“按需复制”的。

我正在使用您在GCC 12(Windows)下的示例测试代码,看起来最近的GCC中,变量ab不共享std :: string的内部缓冲区,请参见此处的结果:https://forums.wxwidgets.org/viewtopic.php?p=214933#p214933。 - ollydbg23

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接