为啥调用vector.reserve(required + 1) 比vector.reserve(required) 快?

Posted

技术标签:

【中文标题】为啥调用vector.reserve(required + 1) 比vector.reserve(required) 快?【英文标题】:Why is calling vector.reserve(required + 1) faster than vector.reserve(required)?为什么调用vector.reserve(required + 1) 比vector.reserve(required) 快? 【发布时间】:2013-03-20 10:00:17 【问题描述】:

我正在做一些测试来测量标准容器在各种条件下的性能,我遇到了一些奇怪的事情。当我在std::vector 的中间插入许多项目时,如果我首先使用要添加的元素的确切数量调用reserve,我发现在大多数情况下与不调用reserve 相比基本上没有性能差异,这令人惊讶.然而,更令人惊讶的是,如果我用我需要的确切数量的元素调用 reserve + 1,那么我会获得显着的性能提升。这是我刚刚得到的结果示例表(所有时间都以秒为单位):

+---------------+--------+-------------------+-----------------------+
| # of elements | vector | vector (reserved) | vector (reserved + 1) |
+---------------+--------+-------------------+-----------------------+
|         10000 | 0.04   | 0.04              | 0.03                  |
|         20000 | 0.14   | 0.14              | 0.11                  |
|         30000 | 0.32   | 0.32              | 0.25                  |
|         40000 | 0.55   | 0.55              | 0.44                  |
|         50000 | 0.87   | 0.85              | 0.66                  |
|         60000 | 1.24   | 1.24              | 0.96                  |
|         70000 | 1.69   | 1.68              | 1.31                  |
|         80000 | 2.17   | 2.21              | 1.71                  |
|         90000 | 2.78   | 2.75              | 2.16                  |
|        100000 | 3.43   | 3.44              | 2.68                  |
|        110000 | 4.13   | 4.15              | 3.23                  |
|        120000 | 4.88   | 4.89              | 3.86                  |
|        130000 | 5.79   | 5.8               | 4.51                  |
|        140000 | 6.71   | 6.71              | 5.24                  |
|        150000 | 7.7    | 7.7               | 6.02                  |
|        160000 | 8.76   | 8.67              | 6.86                  |
|        170000 | 9.9    | 9.91              | 7.74                  |
|        180000 | 11.07  | 10.98             | 8.64                  |
|        190000 | 12.34  | 12.35             | 9.64                  |
|        200000 | 13.64  | 13.56             | 10.72                 |
|        210000 | 15.1   | 15.04             | 11.67                 |
|        220000 | 16.59  | 16.41             | 12.89                 |
|        230000 | 18.05  | 18.06             | 14.13                 |
|        240000 | 19.64  | 19.74             | 15.36                 |
|        250000 | 21.34  | 21.17             | 16.66                 |
|        260000 | 23.08  | 23.06             | 18.02                 |
|        270000 | 24.87  | 24.89             | 19.42                 |
|        280000 | 26.5   | 26.58             | 20.9                  |
|        290000 | 28.51  | 28.69             | 22.4                  |
|        300000 | 30.69  | 30.74             | 23.97                 |
|        310000 | 32.73  | 32.81             | 25.57                 |
|        320000 | 34.63  | 34.99             | 27.28                 |
|        330000 | 37.12  | 37.17             | 28.99                 |
|        340000 | 39.36  | 39.43             | 30.83                 |
|        350000 | 41.7   | 41.48             | 32.45                 |
|        360000 | 44.11  | 44.22             | 34.55                 |
|        370000 | 46.62  | 46.71             | 36.22                 |
|        380000 | 49.09  | 48.91             | 38.46                 |
|        390000 | 51.71  | 51.98             | 40.22                 |
|        400000 | 54.45  | 54.56             | 43.03                 |
|        410000 | 57.23  | 57.29             | 44.84                 |
|        420000 | 60     | 59.73             | 46.67                 |
|        430000 | 62.9   | 63.03             | 49.3                  |
+---------------+--------+-------------------+-----------------------+

我检查了实现,它似乎没有错误。然后我在调用reserve后立即打印大小和容量进一步测试,然后在填充向量后再次打印,一切看起来都很好。

before:
    size: 0
    capacity: 10000
after:
    size: 10000
    capacity: 10000

before:
    size: 0
    capacity: 20000
after:
    size: 20000
    capacity: 20000

...

Fedora Linux x86_64 上的编译器是 gcc 4.7.2。编译器选项是-std=c++11 -Ofast -march=native -funsafe-loop-optimizations -flto=4 - fwhole-program

代码如下。

#include <algorithm>
#include <array>
#include <cstdint>
#include <vector>
#include <random>
#include <string>
#include <iostream>
#include <fstream>

#include <boost/timer.hpp>

namespace 
constexpr size_t array_size = 1;

unsigned number() 
        static std::random_device rd;
        static std::mt19937 random_engine(rd());
        static std::uniform_int_distribution<uint32_t> distribution(0, std::numeric_limits<uint32_t>::max());
        return distribution(random_engine);


class Class 
        public:
                Class() 
                        x[0] = number();
                
                std::string to_string() const 
                        return std::to_string(x[0]);
                
                inline friend bool operator<=(Class const & lhs, Class const & rhs) 
                        return lhs.x[0] <= rhs.x[0];
                
        private:
                std::array<uint32_t, array_size> x;
;

template<typename Container>
void add(Container & container, Class const & value) 
        auto const it = std::find_if(std::begin(container), std::end(container), [&](Class const & c) 
                return value <= c;
        );
        container.emplace(it, value);


// Do something with the result
template<typename Container>
void insert_to_file(Container const & container) 
        std::fstream file("file.txt");
        for (auto const & value : container) 
                file << value.to_string() << '\n';
        


template<typename Container>
void f(std::vector<Class> const & values) 
        Container container;
        container.reserve(values.size());
        for (auto const & value : values) 
                add(container, value);
        
        insert_to_file(container);




int main(int argc, char ** argv) 
        std::size_t const size = (argc == 1) ? 1 : std::stoul(argv[1]);
        // Default constructor of Class fills in values here
        std::vector<Class> const values_to_be_copied(size);
        typedef std::vector<Class> Container;
        boost::timer timer;
        f<Container>(values_to_be_copied);
        std::cerr << "Finished in " << timer.elapsed() << " seconds.\n";

我创建了一个 C++03 版本来尝试帮助其他人重现它,但我无法在此版本中重现它,尽管我试图通过使其尽可能直接的翻译来显示问题:

#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <vector>
#include <string>
#include <iostream>

#include <boost/array.hpp>
#include <boost/cstdint.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_int_distribution.hpp>
#include <boost/timer.hpp>

namespace 
unsigned number() 
        static boost::random::mt19937 random_engine;
        static boost::random::uniform_int_distribution<boost::uint32_t> distribution(0, std::numeric_limits<boost::uint32_t>::max());
        return distribution(random_engine);


class Class 
        public:
                Class() 
                        x[0] = number();
                
                inline friend bool operator<=(Class const & lhs, Class const & rhs) 
                        return lhs.x[0] <= rhs.x[0];
                
                std::string to_string() const 
                        return boost::lexical_cast<std::string>(x[0]);
                
        private:
                boost::array<boost::uint32_t, 1> x;
;

class Less 
public:
        Less(Class const & c):
                value(c) 
        
        bool operator()(Class const & c) const 
                return value <= c;
        
private:
        Class value;
;

void add(std::vector<Class> & container, Class const & value) 
        std::vector<Class>::iterator it = std::find_if(container.begin(), container.end(), Less(value));
        container.insert(it, value);


// Do something with the result
void insert_to_file(std::vector<Class> const & container) 
        std::fstream file("file.txt");
        for (std::vector<Class>::const_iterator it = container.begin(); it != container.end(); ++it) 
                file << it->to_string() << '\n';
        


void f(std::vector<Class> const & values) 
        std::vector<Class> container;
        container.reserve(values.size() + 1);
        for (std::vector<Class>::const_iterator it = values.begin(); it != values.end(); ++it) 
                add(container, *it);
        
        insert_to_file(container);




int main(int argc, char ** argv) 
        std::size_t const size = (argc == 1) ? 1 : boost::lexical_cast<std::size_t>(argv[1]);
        // Default constructor of Class fills in values here
        std::vector<Class> const values_to_be_copied(size);
        boost::timer timer;
        f(values_to_be_copied);
        std::cerr << "Finished in " << timer.elapsed() << " seconds.\n";

当前调用reserve 的行已更改为包含+ 1 或完全删除,具体取决于我正在运行的测试。整个过程是从一个 shell 脚本运行的,该脚本从 10000 个元素开始,增加到 430000 个元素,一次运行一个版本。

我的处理器是 Intel i5 4 核处理器,我有 4 GiB 的内存。我会尽量简化C++11版本的代码,看看能不能隔离问题。

有谁知道为什么保留比我需要的多一个元素会导致速度提高?

【问题讨论】:

不幸的是,无法在 OSX x86_64 上使用 gcc 4.7.2 重现(相同的编译器选项,除了我必须删除 -march=native)。 你有没有尝试过除了那些整数之外的其他尺寸? 我觉得奇怪的是,保留正确的大小对性能没有统计上的影响。 转载,FC18,g++ (GCC) 4.7.2 20121109 (Red Hat 4.7.2-8),AMD Phenom II 1100T,所以不是 CPU 的东西。 如果我放弃 LTO 标志,问题就会消失(reserve() 变得和reserve(+1) 一样快)。可能是 GCC 错误,但我对 LTO 的了解还不够,无法调查。 【参考方案1】:

我对程序做了如下修改:

size_t a = getenv("A") ? 1 : 0;

void f(std::vector<Class> const & values) 
    ...
    container.reserve(values.size() + a);
    ...

现在,无论 a 是 0 还是 1,性能都是相同的(快速)。结论必须是额外项目的保留对性能没有影响(问题中假设了这一点)。此外,对源代码或编译器标志的其他小改动会在快慢之间切换性能,因此看起来代码优化器在某些情况下比在其他情况下运气更好。

以下 f() 的实现使用相同的编译器标志触发相反的行为,因此在保留精确大小时它会很快,而在保留额外项目时会很慢:

template<typename Container>
void f(std::vector<Class> const & values) 
    Container container;
    container.reserve(values.size());
    for (auto it = values.begin(); it != values.end(); ++it) 
        add(container, *it);
    
    insert_to_file(container);

【讨论】:

-1,这并不能回答问题,甚至不能做出合理的解释。 排除了前面的解释。 (a) 观察到的行为不依赖于实际的保留大小。 (b) 它不依赖于是否使用 LTO。 看起来英特尔处理器的成本模型不匹配。使用 -march=corei7 编译,保留 +1 版本会运行得更快,但仅在 core2 上运行时。但是这两个二进制文件都将在 AMD 上以相同的性能运行。使用-march=k8编译,在corei7上运行会更快,不依赖+1预留等细节。 这实际上是一个巧妙的证明,它确实回答了这个问题:一个添加元素什么都不做。到目前为止,这个答案被低估了。 我认为这是足够有力的证据向 gcc 团队报告优化错误。现在我只需要弄清楚为什么 Visual Studio 2012(使用声明为 const 而不是 constexpr 的 array_size 进行编译)在使用 reserve 与不使用时提供相同的性能,但使用 reserve + 时的性能要差得多1...无论哪种方式,似乎人们使用reserve 的频率不足以让编译器编写者对其进行彻底测试。【参考方案2】:

我相信reserve 在您的具体情况下与一个额外的项目之间的差异是绝对可以忽略不计的。此外,某些实现可能会将请求的大小截断为某个数字(例如 2 的幂),因此根本没有区别。

OTOH 你的程序的性能瓶颈似乎是别的东西。您执行两个昂贵的数组操作:

为元素插入搜索合适的位置 在“中间”插入单个元素

您可能已经注意到,您的程序依赖于random。很容易看出,在您的特定情况下,如果随机数正在增长 - 您的程序将运行得更快,OTOH 如果它们缩小 - 您将不得不执行更多“昂贵”的插入。

我猜细微的代码更改可能会触发不同的随机数生成。

【讨论】:

1) 变化远非微不足道,它是 15-20%。 2) 当您启用 LTO 时问题出现,这表明 GCC 在该部分出现问题。然而,它难以重现的事实(看到它出现的两个版本都是 Fedora 18、GCC 4.7.2)可能暗示它是该特定版本/构建中的一个错误。 可能会出现请求大小的“上限”,但不会截断,因为实现必须保证在达到保留大小之前不会发生进一步的分配。

以上是关于为啥调用vector.reserve(required + 1) 比vector.reserve(required) 快?的主要内容,如果未能解决你的问题,请参考以下文章

在vector.reserve()之后vector.data()不为空吗?

vector reserve与resize区别

std::vector::reserve 是不是重新分配内部数组?

std::vector::resize() 与 std::vector::reserve()

在 vector::resize() 和 vector::reserve() 之间选择

libxx.so: undefined reference, vector.reserve(n)