环路内容器的性能

Performance of containers inside loops

本文关键字:性能 环路      更新时间:2023-10-16

我必须关心这样的代码吗:

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 ...
}

或者编译器足够聪明,可以优化内存分配?

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

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

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

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

我简化了很多,但这足以证明我试图在这里提出的要点:向量对象本身不包含实际数据。矢量对象只包含一个指向数据的指针以及一些关于分配大小等的元数据。

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

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

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

然而,标准库中有一个容器确实允许进行优化:std::basic_string特别允许。它最初看起来可能有点奇怪(老实说,有点奇怪),但如果用std::basic_string<int> v(10, 0);替换std::vector,并在包括短字符串优化(例如VC++)的实现中使用它,您可能会在速度上得到显著提高。std::string允许这样做的方法之一是,您不能使用它来存储抛出异常的类型——如果int只是一个例子,您可能真的需要存储其他可以抛出的类型,那么basic_string可能不适合您。即使对于像int这样的原生类型,char_traits<T>也可能是一个不完整的类型,所以这可能无论如何都不起作用。如果你认为你非常需要,你可以把它作为你自己类型的容器,方法是1)确保它们不会抛出,2)专门为你的类型设置char_trait。一句话:尝试一下可能很有趣,但它很少(如果有的话)实用,而且几乎不可能推荐。

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