为什么VS2008的std::string.erase()函数会移动缓冲区?

5
我希望逐行读取文件并捕获特定的输入行,为了最大化性能,我可以通过读取整个文件并使用指针迭代其内容的低级方式来完成,但是这段代码并不关键,因此我希望使用更可读且类型安全的标准库风格实现。所以我有以下代码:
 std::string line;
 line.reserve(1024);
 std::ifstream file(filePath);
 while(file)
 {
    std::getline(file, line);
    if(line.substr(0, 8) == "Whatever")
    {
        // Do something ...
    }
 }

虽然此代码不需要高性能,但在解析操作之前我调用了line.reserve(1024),以防止读入更大的行时多次重新分配字符串。

在std::getline内部,在将每行字符添加到字符串之前,该字符串被清除。我逐步执行了此代码以满足自己,使我惊讶的是发现:

在string::erase深处,它并不只是将其大小变量重置为零,而是使用指针值调用memmove_s函数,该指针值将覆盖缓冲区的已使用部分,以及紧随其后的未使用部分。但是,memmove_s带有一个计数参数为零,即请求移动零字节。

问题:

为什么我要在我的循环中增加一个库函数调用的开销,尤其是一个被调用却什么都不做的函数?

我还没有自己分析它,但在什么情况下,这个调用实际上不会什么都不做,而是会开始移动缓冲区的块呢?

以及,为什么它要这样做?

奖励问题:C++标准库标签是什么?


STL(有些错误,但被接受),std或stdlib。 - Joe McGrath
啊,我尝试了C++标准库,但是没有解决问题。我太习惯使用命名空间了,忘记它只是一个缩写 :) - Neutrino
3个回答

11

找到编译器错误加一分。不是很多人都能做到这一点。 - FailedDev
1
不是真正的编译器,而是 STL 实现上的“ bug”。我只想指出回答问题的 MS 人员的缩写 - 多么巧合 :D - Voo
+1 不错的报告,Ben。显然我在你最初报告时就看到了它,因为我已经在Connect上点赞了。:-P - ildjarn
谢谢。通过这些回答提出的见解并进一步研究后,很明显,在我的代码情况下,移动永远不会发生,所以正如你所说,库的实现是次优的,但在这种情况下不足以让我担心。 - Neutrino

3

std::string::clear() 是通过调用 std::string::erase() 实现的, 而 std::string::erase() 确实需要移动被删除块后面的所有字符。那么为什么不调用标准函数来完成呢? 如果你有一些证明这是瓶颈的分析结果,那么或许你可以抱怨一下,但除此之外,说实话我看不出有什么不同。(避免调用所需的逻辑可能会比调用本身更耗费资源。)

另外,在使用 getline 返回值之前,你没有检查调用结果。你的循环应该像这样:

while ( std::getline( file, line ) ) {
    //  ...
}

如果您非常关注性能,为了进行比较而创建一个子串(即一个新的std::string)的成本远高于调用memmove_s。这样的代码有什么问题吗:

static std::string const target( "Whatever" );
if ( line.size() >= target.size()
        && std::equal( target.begin(), target().end(), line.being() ) ) {
    //  ...
}

我认为这是最符合习惯的确定字符串是否以特定值开头的方法。

(我可以补充说,从经验上看,这里的 reserve 也不会给你带来太多好处。在读取了几行文件之后,你的字符串不会再增长太多,因此在前几行之后很少进行重新分配。又一种过早优化的情况?)


我不认为这是过早的。它不会增加任何混乱,而且确实有速度优势。 - Mooing Duck
@MooingDuck 我猜你在谈论 reserve:它对性能没有任何可测量的影响,而且增加了一行不必要的代码。 - James Kanze
我在谈论“reserve”,是的。看起来你和我对于“过早”优化有不同的定义,这没关系。这并不重要到需要争论。 - Mooing Duck
我现在看到了一个显而易见的事情,那就是我忽视了当只有部分字符串被清除时,erase确实必须将其余的内容向下移动,但正如Ben Voigt所指出的,在清除整个字符串的常见情况下,这是相当多的执行无用代码的工作量,在这种常见情况下,避免调用的检查将远远不及执行所有那些无意义的代码的成本。不过还是感谢你的其他提示,都很正确,我在被erase发现的问题分散注意力之前,没有真正完成我的循环整理 :) - Neutrino
@Neutrino:相反,当字符串的末尾没有被删除时,才需要移动。无论字符串是否被截断(长度为零或不为零),都不需要移动。在erase中已经有一个检查,“修复”纯粹是节省而没有任何副作用。 - Ben Voigt
显示剩余5条评论

0
在这种情况下,我认为你提到的读取整个文件并迭代结果的想法可能会给出大致相同简单代码。你只需将“读取行、检查前缀、处理”更改为“读取文件、扫描前缀、处理”即可:
size_t not_found = std::string::npos;
std::istringstream buffer;

buffer << file.rdbuf();

std::string &data = buffer.str();

char const target[] = "\nWhatever";
size_t len = sizeof(target)-1;

for (size_t pos=0; not_found!=(pos=data.find(target, pos)); pos+=len)
{
    // process relevant line starting at contents[pos+1]
}

我实际上是指进一步降低级别,使用Win32 CreateFile将整个文件读入原始内存缓冲区,而我考虑这样做的唯一原因是因为这就是我目前正在工作的应用程序中所有其他IO的方式:(在您的示例中,即使您引用istringstream.str(),它仍然是整个受控序列的副本,对吗?在这种情况下,既然您已经复制了整个文件,那么这是否真的比逐行读取文件更高效? - Neutrino
@Neutrino:它可能是一份副本,也可能不是。根据我的经验,与逐行读取相比,它通常具有更好的性能。您可以查看先前的回答进行一些基准测试和性能更好的代码。如果我没记错的话,逐行读取似乎与那里使用的迭代器的速度大致相同。 - Jerry Coffin

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