为什么 .push_back(x) 比 .push_back(std::move(x)) 快

Why is .push_back(x) faster than .push_back(std::move(x))

我有一个很大的 .txt 文件需要加载并存储在向量中。该文件大小约为 5MB,500,000 行,每行约 10-20 个字符,以 '\n' 分隔。我正在使用下面的示例代码对读取整个文件所需的时间进行一些基准测试:

#include<iostream>
#include<vector>
#include<fstream>

int main()
{
    std::fstream input("words.txt");
    std::vector<std::string> vector;
    std::string line;

    while(input >> line){
        vector.push_back(line);
    }
}

我很好奇是否将字符串作为右值引用传递会更快,但实际上却慢了大约 10 毫秒。

#include<iostream>
#include<vector>
#include<fstream>

int main()
{
    std::fstream input("words.txt");
    std::vector<std::string> vector;
    std::string line;

    while(input >> line){
        vector.push_back(std::move(line));
    }
}

第一个代码示例的平均加载时间约为 58 毫秒,第二个代码示例为 68-70 毫秒。我在想移动总是比复制更快或等于复制,这就是为什么这对我来说似乎不正确。

有谁知道发生这种情况的原因吗? 基准测试使用:

perf stats -r 100 ./a.out

在 Arch Linux 上,代码已使用 GCC 10.2、C++17 标准编译。

此外,如果有人知道更优化的方法,我们将不胜感激。

如果你调用g++ -E你可以浏览相关代码:

复制构造:

  basic_string(const basic_string& __str)
    : _M_dataplus(_M_local_data(),
                  _Alloc_traits::_S_select_on_copy(__str._M_get_allocator()))
  {
      _M_construct(__str._M_data(), __str._M_data() + __str.length());
  }

移动构造:

# 552 "/usr/local/include/c++/10.2.0/bits/basic_string.h" 3
basic_string(basic_string&& __str) noexcept
  : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator()))
{
    if (__str._M_is_local())
    {
        traits_type::copy(_M_local_buf, __str._M_local_buf,
                          _S_local_capacity + 1);
    }
    else
    {
        _M_data(__str._M_data());
        _M_capacity(__str._M_allocated_capacity);
    }
    _M_length(__str.length());
    __str._M_data(__str._M_local_data());
    __str._M_set_length(0);
}

值得注意的是,(为了支持短字符串优化)移动构造函数需要查看 ._M_is_local() 以确定是复制还是移动(因此有一个分支要预测),然后它会清除移出的字符串 / 将其长度设置为 0。额外的工作 = 额外的时间。


@Manuel 发表了一个有趣的评论:

When you move line it gets empty, so the next iteration needs to allocate space. std::string has a small buffer as an optimization (most of the implementations, if not all) and the copy just copies chars, no memory allocation. That could be the difference.

这与所说的相加并不完全,但这里存在细微差别。对于输入space-分隔的单词足够长需要动态分配,则:

a) move 版本可能在最后一个单词后清除了 line 的动态缓冲区,因此需要重新分配; 如果输入足够长,它可能必须重新分配一次或多次以增加容量

b) 复制版本可能会有足够大的缓冲区(其容量会根据需要增加,有效地成为所见单词的高水位线),但复制时将需要动态分配在 push_back 内建造。虽然该分配的确切大小是预先知道的 - 它不需要调整大小来增加容量。

这确实表明当输入字长变化很大时,复制可能会更快。


Also if anyone knows a more optimal way to do this would be much appreciated.

如果您真的关心性能,我建议对文件进行内存映射基准测试,并在其中创建 vectorstring_views:这可能会快得多。