【问题标题】:Dynamic allocation and random access: Raw, Smart, Deque, Vector. Why raw so fast, and deque so slow?动态分配和随机访问:Raw、Smart、Deque、Vector。为什么 raw 这么快,而 deque 这么慢?
【发布时间】:2017-06-05 20:30:06
【问题描述】:

我创建了一个小性能测试,比较了三种流行的动态分配技术的设置和访问时间:原始指针、std::unique_ptr 和 std::deque。

编辑:根据@NathanOliver 的说法,添加了std::vector: 编辑 2:每个后期开发者,分配有 std::vector(n) 和 std::deque(n) 构造函数 编辑 3:根据@BaummitAugen,在计时循环内移动分配,并编译优化版本。 编辑 4:根据@PaulMcKenzie 的 cmets,设置为 2000。

结果:这些变化使事情变得更加紧张。 Deque 和 Vector 在分配和分配上仍然较慢,而 deque 在访问时要慢得多:

pickledEgg$ g++ -std=c++11 -o sp2 -O2 sp2.cpp

Average of 2000 runs:
Method  Assign          Access
======  ======          ======
Raw:    0.0000085643    0.0000000724
Smart:  0.0000085281    0.0000000732
Deque:  0.0000205775    0.0000076908
Vector: 0.0000163492    0.0000000760

只是为了好玩,这里是 -Ofast 结果:
pickledEgg$ g++ -std=c++11 -o sp2 -Ofast sp2.cpp

Average of 2000 runs:
Method  Assign          Access
======  ======          ======
Raw:    0.0000045316    0.0000000893
Smart:  0.0000038308    0.0000000730
Deque:  0.0000165620    0.0000076475
Vector: 0.0000063442    0.0000000699

原创:为了后代;注意缺少优化器 -O2 标志:

pickledEgg$ g++ -std=c++11 -o sp2 sp2.cpp

Average of 100 runs:
Method  Assign      Access
======  ======      ======
Raw:    0.0000466522    0.0000468586
Smart:  0.0004391623    0.0004406758
Deque:  0.0003144142    0.0021758729
Vector: 0.0004715145    0.0003829193

更新代码:

#include <iostream>
#include <iomanip>
#include <vector>
#include <deque>
#include <chrono>
#include <memory>

const int NUM_RUNS(2000);

int main() {
    std::chrono::high_resolution_clock::time_point b, e;
    std::chrono::duration<double> t, raw_assign(0), raw_access(0), smart_assign(0), smart_access(0), deque_assign(0), deque_access(0), vector_assign(0), vector_access(0);
    int k, tmp, n(32768);

    std::cout << "Average of " << NUM_RUNS << " runs:" << std::endl; 
    std::cout << "Method " << '\t' << "Assign" << "\t\t" << "Access" << std::endl;
    std::cout << "====== " << '\t' << "======" << "\t\t" << "======" << std::endl;

    // Raw
    for (k=0; k<NUM_RUNS; ++k) {
        b = std::chrono::high_resolution_clock::now();
        int* raw_p = new int[n]; // run-time allocation
        for (int i=0; i<n; ++i) { //assign
            raw_p[i] = i;
        }
        e = std::chrono::high_resolution_clock::now();
        t = std::chrono::duration_cast<std::chrono::duration<double> >(e - b);
        raw_assign+=t;
        b = std::chrono::high_resolution_clock::now();
        for (int i=0; i<n; ++i) { //access
            tmp = raw_p[i];
        }
        e = std::chrono::high_resolution_clock::now();
        t = std::chrono::duration_cast<std::chrono::duration<double> >(e - b);
        raw_access+=t;
        delete [] raw_p; // :^)
    }
    raw_assign /= NUM_RUNS;
    raw_access /= NUM_RUNS;
    std::cout << "Raw:   " << '\t' << std::setprecision(10) << std::fixed << raw_assign.count() << '\t' << raw_access.count() << std::endl;

    // Smart
    for (k=0; k<NUM_RUNS; ++k) {
        b = std::chrono::high_resolution_clock::now();
        std::unique_ptr<int []> smart_p(new int[n]); // run-time allocation
        for (int i=0; i<n; ++i) { //assign
            smart_p[i] = i;
        }
        e = std::chrono::high_resolution_clock::now();
        t = std::chrono::duration_cast<std::chrono::duration<double> >(e - b);
        smart_assign+=t;
        b = std::chrono::high_resolution_clock::now();
        for (int i=0; i<n; ++i) { //access
            tmp = smart_p[i];
        }
        e = std::chrono::high_resolution_clock::now();
        t = std::chrono::duration_cast<std::chrono::duration<double> >(e - b);
        smart_access+=t;
    }
    smart_assign /= NUM_RUNS;
    smart_access /= NUM_RUNS;
    std::cout << "Smart: " << '\t' << std::setprecision(10) << std::fixed << smart_assign.count() << '\t' << smart_access.count() << std::endl;

    // Deque
    for (k=0; k<NUM_RUNS; ++k) {
        b = std::chrono::high_resolution_clock::now();
        std::deque<int> myDeque(n);
        for (int i=0; i<n; ++i) { //assign
            myDeque[n] = i;
//          myDeque.push_back(i);
        }
        e = std::chrono::high_resolution_clock::now();
        t = std::chrono::duration_cast<std::chrono::duration<double> >(e - b);
        deque_assign+=t;
        b = std::chrono::high_resolution_clock::now();
        for (int i=0; i<n; ++i) { //access
            tmp = myDeque[n];
        }
        e = std::chrono::high_resolution_clock::now();
        t = std::chrono::duration_cast<std::chrono::duration<double> >(e - b);
        deque_access+=t;
    }
    deque_assign /= NUM_RUNS;
    deque_access /= NUM_RUNS;
    std::cout << "Deque: " << '\t' << std::setprecision(10) << std::fixed << deque_assign.count() << '\t' << deque_access.count() << std::endl;

    // vector
    for (k=0; k<NUM_RUNS; ++k) {
        b = std::chrono::high_resolution_clock::now();
        std::vector<int> myVector(n);
        for (int i=0; i<n; ++i) { //assign
            myVector[i] = i;
//          .push_back(i);
        }
        e = std::chrono::high_resolution_clock::now();
        t = std::chrono::duration_cast<std::chrono::duration<double> >(e - b);
        vector_assign+=t;
        b = std::chrono::high_resolution_clock::now();
        for (int i=0; i<n; ++i) { //access
            tmp = myVector[i];
//          tmp = *(myVector.begin() + i);
        }
        e = std::chrono::high_resolution_clock::now();
        t = std::chrono::duration_cast<std::chrono::duration<double> >(e - b);
        vector_access+=t;
    }
    vector_assign /= NUM_RUNS;
    vector_access /= NUM_RUNS;
    std::cout << "Vector:" << '\t' << std::setprecision(10) << std::fixed << vector_assign.count() << '\t' << vector_access.count() << std::endl;

    std::cout << std::endl;
    return 0;
}

【问题讨论】:

  • A deque 不像数组。您应该将性能与std::vector 进行比较。
  • 使用std::deque(n)std::vector(n) 构造函数。
  • 要获得接近这些的时间,我必须在调试模式下运行。通过优化,原始指针和unique_ptr 之间没有区别。 deque 要慢很多,因为您在计时中包含了内存分配。
  • 使用-O2-O3 看看会发生什么
  • @kmiklas 除非您正在计时优化构建,否则结果毫无意义。拥有超过 2K 的代表,这应该是给定的。

标签: c++ performance pointers deque


【解决方案1】:

从结果中可以看出,原始指针在这两个类别中都是明显的赢家。这是为什么呢?

因为……

g++ -std=c++11 -o sp2 sp2.cpp

...您没有启用优化。调用为非基本类型(例如std::vectorstd::unique_ptr)重载的运算符涉及函数调用。使用原始指针等基本类型的运算符不涉及函数调用。

函数调用通常比没有函数调用慢。经过多次迭代,函数调用的小开销成倍增加。但是,优化器可以内联扩展函数调用,从而使非基本类型的缺点无效。但前提是执行了优化。


std::deque 速度较慢还有另一个原因:访问双端队列的任意元素的算法比访问数组更复杂。虽然std::deque 具有不错的随机访问性能,但它不如数组好。 std::deque 更合适的用例是线性迭代(使用迭代器)。

此外,您使用了std::deque::at,它会进行边界检查。下标运算符不进行边界检查。边界检查会增加运行时开销。


原始数组在分配速度上似乎比std::vector 略有优势,可能是因为std::vector 对数据进行了零初始化。

【讨论】:

  • "使用迭代器可以获得更好的性能。"
  • @kmiklas 您可以使用迭代器进行随机访问,但这不是我的意思。我是说std::deque 的随机访问速度比数组慢。对于std::deque,线性迭代是更合适的用例。我会澄清的。
【解决方案2】:

std::deque 是一个双向链表。 myDeque.at(i) 必须在每次调用时遍历前 i 个元素。这就是对双端队列的访问如此缓慢的原因。

std::vector 的初始化很慢,因为你没有预先分配足够的内存。 std::vector 然后从少量元素开始,一旦您尝试插入更多元素,通常会加倍。这种重新分配涉及为所有元素调用移动构造函数。尝试像这样构造向量:

std::vector<int> myVector{n};

在矢量访问中,我想知道您为什么不使用tmp = myVector[i]。不是调用索引运算符,而是实例化一个迭代器,调用它的 + 运算符,然后在结果上调用解引用运算符。由于您没有进行优化,因此函数调用可能不会被内联,这就是为什么 std::vector 访问比原始指针慢的原因。

对于std::uniqe_ptr 我想,它的原因与std::vector 类似。您总是在唯一指针上调用索引运算符,这也是一个函数调用。只是作为一个实验,您能否尝试在为smart_p 分配内存后立即调用smart_p.get() 并使用原始指针进行其余操作。我假设它会和原始指针一样快。这可以证明我的假设,它是函数调用。那么简单的建议是,启用优化并重试。

kmiklas 编辑

Average of 2000 runs:
Method  Assign          Access
======  ======          ======
Raw:    0.0000086415    0.0000000681
Smart:  0.0000081824    0.0000000670
Deque:  0.0000204542    0.0000076554
Vector: 0.0000164252    0.0000000678

【讨论】:

  • "您能否在为 smart_p 分配内存后立即尝试并调用 smart_p.get()"
  • std::deque 不是一个普通的链表。 std::deque::at 不必遍历第一个 i 元素。这意味着随机访问的复杂性是线性的。但该标准要求它具有恒定的复杂性。
  • std::deque 是一个双向链表 不太正确。它是元素块的列表。否则与std::list 相同。
  • std::vector myVector{n};产生分段错误。 std::vector myVector(n) 可以接受吗?
  • 啊,我明白了。 vector&lt;int&gt; 是一个特例。是的,这是可以接受的,当然。正如您的结果所示,函数调用有所不同。因此,当您打开优化时,编译器可以将它们全部内联。您可能会发现 raw 和 uniqe_ptr 之间没有太大区别。我认为,您现在看到的主要是统计噪音。 `vector 可能会稍微慢一些,但不会慢很多。我不希望您能够区分统计噪声的差异。
猜你喜欢
  • 2013-05-20
  • 1970-01-01
  • 2016-10-08
  • 2012-02-13
  • 2020-02-01
  • 1970-01-01
  • 2023-03-02
  • 2012-11-25
  • 1970-01-01
相关资源
最近更新 更多