【问题标题】:why vector is always slower than C array, at least in this case?为什么向量总是比 C 数组慢,至少在这种情况下?
【发布时间】:2015-06-10 23:43:19
【问题描述】:

我正在尝试使用 Eratosthenes'Sieve 算法找到所有不大于 n 的素数,并且我有以下代码,筛子在向量和 C 数组中实现,我发现几乎在所有时间, C 数组总是更快。

使用向量:

int countPrimes_vector(int n) {                  
    int res = 0; 
    vector<char>bitmap(n);
    memset(&bitmap[0], '1', bitmap.size() * sizeof( bitmap[0]));
    //vector<bool>bitmap(n, true); Using this one is even slower!!

    for (int i = 2; i<n; ++i){

        if(bitmap[i]=='1')++res;
        if(sqrt(n)>i)
        {
             for(int j = i*i; j < n; j += i) bitmap[j] = '0';
        }
    }

    return res;
} 

使用 C 数组:

int countPrimes_array(int n) {  

    int res = 0; 
    bool * bitmap = new bool[n];
    memset(bitmap, true, sizeof(bool) * n);
    for (int i = 2; i<n; ++i){

        if(bitmap[i])++res;
        if(sqrt(n)>i)
        {
             for(int j = i*i; j < n; j += i) bitmap[j] = false;
        }
    }
    delete []bitmap;
    return res;
}

测试代码:

clock_t t;
t = clock();
int a;
for(int i=0; i<10; ++i)a = countPrimes_vector(8000000); 
t = clock() - t;
cout<<"time for vector = "<<t<<endl;

t = clock();
int b;
for(int i=0; i<10; ++i)b = countPrimes_array(8000000); 
t = clock() - t;
cout<<"time for array = "<<t<<endl;

输出:

 time for vector = 32460000
 time for array = 29840000

我测试了很多次,C数组总是更快。背后的原因是什么?

经常听说vector 和C 数组的性能是一样的,vector 应该一直作为标准容器使用。这种说法是真的,或者至少一般来说是这样吗?在什么情况下应该首选 C 数组?

编辑:

如下cmets提示,开启优化-O2-O3后(原来是用g++ test.cpp编译的),vector与C数组的时间差不再有效,在某些场合@ 987654332@ 比 C 数组快。

【问题讨论】:

  • 没有数据结构比不上数组,就这么简单?
  • 您使用哪些编译器选项进行编译?优化设置对于这类问题非常重要。此外,我认为您所经历的轻微(实际上并不多)性能下降对于您获得的额外安全性和易用性而言非常小。
  • 代码不等价。在一种情况下,您使用值 49 来表示逻辑真,使用 48 来表示假。在另一种情况下,您使用 1 并将其与 0 进行比较。到底为什么要这样做。
  • 在未启用优化的情况下进行性能测试在很大程度上是没有意义的,因为您正在测试生成的可执行文件易于调试,而不是旨在以最高效率运行的可执行文件。
  • @Allanqunzi 另一个区别是,在某些实现中,vector::operator[] 将对未优化的构建执行范围检查,而您将在性能测试中为此付出巨大的代价。

标签: c++ arrays vector


【解决方案1】:

您的比较包含可以解释差异的不一致之处,另一个因素可能是编译没有充分优化的结​​果。一些实现在 STL 的调试版本中有很多额外的代码,例如 MSVC 会对向量元素访问进行边界检查,这会显着降低调试版本的速度。

以下代码显示了两者之间的性能更接近,差异可能只是缺少样本(ideone 的超时限制为 5s)。

#include <vector>
#include <cmath>
#include <cstring>

int countPrimes_vector(int n) {  
    int res = 0; 
    std::vector<bool> bitmap(n, true);
    for (int i = 2; i<n; ++i){
        if(bitmap[i])
          ++res;
        if(sqrt(n)>i)
        {
             for(int j = i*i; j < n; j += i) bitmap[j] = false;
        }
    }
    return res;
}

int countPrimes_carray(int n) {  
    int res = 0; 
    bool* bitmap = new bool[n];
    memset(bitmap, true, sizeof(bool) * n);
    for (int i = 2; i<n; ++i){

        if(bitmap[i])++res;
        if(sqrt(n)>i)
        {
             for(int j = i*i; j < n; j += i) bitmap[j] = false;
        }
    }
    delete []bitmap;
    return res;
}

#include <chrono>
#include <iostream>

using namespace std;

void test(const char* description, int (*fn)(int))
{
    using clock = std::chrono::steady_clock;
    using ms = std::chrono::milliseconds;

    auto start = clock::now();

    int a;
    for(int i=0; i<9; ++i)
        a = countPrimes_vector(8000000); 

    auto end = clock::now();
    auto diff = std::chrono::duration_cast<ms>(end - start);

    std::cout << "time for " << description << " = " << diff.count() << "ms\n";
}

int main()
{
    test("carray", countPrimes_carray);
    test("vector", countPrimes_vector);
}

现场演示:http://ideone.com/0Y9gQx

time for carray = 2251ms
time for vector = 2254ms

虽然在某些运行中,carray 慢了 1-2 毫秒。同样,共享资源上的样本不足。

--- 编辑 ---

在您的主要 cmets 中,您会问“为什么优化可以有所作为”。

std::vector<bool> v = { 1, 2, 3 };
bool b[] = { 1, 2, 3 };

我们有两个由 3 个元素组成的“数组”,因此请考虑以下内容:

v[10]; // illegal!
b[10]; // illegal!

STL 的调试版本通常可以在运行时(在某些情况下,在编译时)捕捉到这一点。数组访问可能只会导致错误数据或崩溃。

此外,STL 是通过对 size() 之类的许多小型成员函数调用来实现的,并且因为 vector 是一个类,所以 [] 实际上是通过函数调用 (operator[]) 实现的。

编译器可以消除其中的许多,但那是优化。如果你不优化,那么像

std::vector<int> v;
v[10];

大致如下:

int* data() { return M_.data_; }

v.operator[](size_t idx = 10) {
    if (idx >= this->size()) {
        raise exception("invalid [] access");
    }
    return *(data() + idx);
}

尽管 data 是一个“可内联”函数,但为了使调试更容易,未优化的代码将其保留为 this。当您使用优化构建时,编译器会认识到这些函数的实现是如此微不足道,它可以将它们的实现替换为调用站点,并很快将上述所有内容简化为更像数组访问的操作。

比如上面这种情况,可能先将operator[]缩减为

v.operator[](size_t idx = 10) {
    if (idx >= this->size()) {
        raise exception("invalid [] access");
    }
    return *(M_.data_ + idx);
}

而且由于在不调试的情况下编译可能会删除边界检查,所以它变成了

v.operator[](size_t idx = 10) {
    return *(M_.data_ + idx);
}

所以现在内联可以减少

x = v[1];

x = *(v.M_.data_ + 1); // comparable to v.M_.data_[1];

一个很小的惩罚。 c-array 涉及内存中的数据块和适合指向该块的寄存器的单个局部变量,您的引用直接相对于该块:

不过,有了向量,你就有了一个向量对象,它是一个指向数据的指针、一个大小和一个容量变量:

vector<T>  // pseudo code
{
    T* ptr;
    size_t size;
    size_t capacity;
}

如果您计算机器指令,向量将有 3 个变量来初始化和管理。

当你写作时

x = v[1];

鉴于上述向量的近似值,您的意思是:

T* ptr = v.data();
x = ptr[1];

但是编译器在构建优化时通常足够聪明,可以识别它可以在循环之前执行第一行,但这往往会花费一个寄存器。

T* ptr = v.data(); // in debug, function call, otherwise inlined.
for ... {
    x = ptr[1];
}

因此,您可能会在测试函数的每次迭代中查看更多机器指令,或者在现代处理器上,可能会增加一到两纳秒的运行时间。

【讨论】:

  • 只是添加一个数据点:通过 g++4.9 和 10 次连续测试,我得到平均时间之间的差异在 0.01 个标准差之内,所以我们肯定是在谈论随机波动。跨度>
  • vector&lt;bool&gt; 甚至比您的解释所表明的还要不同。它甚至没有data() 成员函数。特别是,你说“当你写x = vector_bools[1];时你实际上是在说:bools* bools_ptr = vector_bools.data(); x = bools_ptr[1];”的段落完全是假的
  • 公平点,本,我会尝试纠正和/或表明我是近似的。
  • @BenVoigt 已适应 int 并更清楚地表明我正在接近。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-13
  • 1970-01-01
  • 1970-01-01
  • 2021-09-02
  • 1970-01-01
  • 2012-08-01
相关资源
最近更新 更多