循环内容器的性能

时间:2015-02-25 10:48:40

标签: c++ performance

我是否必须关心这样的代码:

for (int i=0; i < BIG; ++i) {
  std::vector<int> v (10);
  ... do something with v ...
}

重构为:

std::vector<int> v;
for (int i=0; i < BIG; ++i) {
  v.clear(); v.resize(10); // or fill with zeros
  ... do something with v ...
}

或编译器是否足够智能以优化内存(de)分配?

我更喜欢第一个,因为当我不再需要它时std::vector<int> v超出了范围。

1 个答案:

答案 0 :(得分:2)

在实践中,编译器很难为两者生成相同的代码,除非标准库的作者给它一些帮助。

特别是,vector通常模糊地实现:

template <class T, class Allocator=std::allocator<T>>
class vector {
    T *data;
    size_t allocated_size, current_size;
public:
    vector(size_t);
    // ...
};

我正在简化很多,但这足以证明我在这里要做的主要观点:矢量对象本身包含实际数据。向量对象仅包含指向数据的指针以及有关分配大小等的一些元数据。

这意味着每次创建10个整数的vector时,vector的构造函数必须使用operator new为(至少)10个整数分配空间(从技术上讲,它使用传递给它的Allocator类型来执行此操作,但默认情况下,它将使用免费存储。同样,当它超出范围时,它使用分配器(再次,默认为免费存储)来销毁10个整数。

避免分配的一个显而易见的方法是为向量对象本身内的至少少量元素分配空间,并且当数据增长大于允许的空间时,仅在空闲存储上分配空间。这样,创建10个int的向量相当于只创建一个10个int的数组(本机数组或std::array) - 在典型的机器上,当执行进入封闭时,它将在堆栈上分配函数,以及在进入块时发生的所有事情都将初始化内容(如果您在写入之前尝试读取其中的一些内容)。

至少在一般情况下,我们不能这样做。例如,如果我移动赋值向量,则该移动赋值不会抛出异常 - 即使单个元素的移动赋值将抛出异常。因此,它不能对各个元素进行任何操作。使用上面的结构,这个要求很容易满足 - 我们基本上从源到目的地执行浅拷贝,并将源中的所有项清零。

但是,标准库中有一个 允许优化的容器:std::basic_string专门允许它。它可能最初看起来有点奇怪(老实说, 有点奇怪),但如果你用std::vector替换了std::basic_string<int> v(10, 0);,并将其用于实现包括短字符串优化(例如,VC ++),您可以获得速度的实质性改进。 std::string允许这样做的一种方式是你不能用它来存储抛出异常的类型 - 如果int只是一个例子,你可能真的需要存储其他可以抛出的类型,然后basic_string可能不适合你。即使对于int这样的原生类型,char_traits<T>也可能是不完整的类型,因此无论如何这可能都不起作用。如果您确定需要足够严重,可以将其用作自己类型的容器,1)确保它们不抛出,2)为您的类型专门化char_traits。一句话:尝试这个很有意思,但它很少(如果有的话)实用,几乎不可能推荐。

显而易见的替代方案是使用std::array<int, 10>代替。如果数组的大小是固定的,这可能是首选。与在非字符类型上实例化basic_string不同,您将按预期使用它,并获得其预期的行为。缺点是大小是编译时常量,因此如果您可能需要在运行时更改大小,那么它根本不是一个选项。