【问题标题】:Why is .push_back(x) faster than .push_back(std::move(x))为什么 .push_back(x) 比 .push_back(std::move(x)) 快
【发布时间】:2021-03-11 17:37:24
【问题描述】:

我有一个大的 .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 std 编译。

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

【问题讨论】:

  • 文件 IO 破坏了所有基准测试的合法性,因为它比 cpu 计算慢得多。
  • @Aplet123 考虑到该文件只有 5MB 大小,我很确定在这种情况下文件 IO 是 cpu 计算。这更可能与程序必须再次为新字符串重新分配内存有关,因为前一个块已移动到向量中。
  • 当你移动line时它会变空,所以下一次迭代需要分配空间。 std::string 有一个小的缓冲区作为优化(大多数实现,如果不是全部的话)并且副本只是复制字符,没有内存分配。这可能是不同的。

标签: c++ stdvector move-semantics


【解决方案1】:

如果您调用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 发表了有趣的评论:

当你移动行时它会变空,所以下一次迭代需要分配空间。 std::string 有一个小的缓冲区作为优化(大多数实现,如果不是全部的话),并且副本只是复制字符,没有内存分配。这可能是不同的。

这并不像所说的那样加起来,但这里有细微的差别。对于足够长的输入空格分隔的单词需要动态分配,:

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

b) 复制版本可能有一个足够大的缓冲区(它的容量将根据需要增加,实际上是看到单词的高水位线),但是在内部构建复制时需要动态分配push_back。该分配的确切大小是预先知道的 - 它不需要调整大小来增加容量。

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


如果有人知道更优化的方法来做到这一点,我们将不胜感激。

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

【讨论】:

  • 感谢您的洞察力。我对移动语义还是很陌生,这让它变得更加清晰。 std::string 类可能是比实际“移动”部分更大的问题。我正在阅读您关于将文件加载到 RAM 并使用 string_view 访问它的建议。我稍后会试一试。再次,非常感谢!
猜你喜欢
  • 2014-07-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-07
  • 1970-01-01
  • 2020-06-20
相关资源
最近更新 更多