簡體   English   中英

std::vector 比普通數組慢這么多嗎?

[英]Is std::vector so much slower than plain arrays?

我一直認為std::vector是“作為數組實現的”,這是普遍的智慧,等等。 今天下下來測試了一下,好像不是這樣:

下面是一些測試結果:

UseArray completed in 2.619 seconds
UseVector completed in 9.284 seconds
UseVectorPushBack completed in 14.669 seconds
The whole thing completed in 26.591 seconds

這大約慢了 3 - 4 倍! 並不能真正證明“ vector可能會慢幾納秒”的評論。

我使用的代碼:

#include <cstdlib>
#include <vector>

#include <iostream>
#include <string>

#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/microsec_time_clock.hpp>

class TestTimer
{
    public:
        TestTimer(const std::string & name) : name(name),
            start(boost::date_time::microsec_clock<boost::posix_time::ptime>::local_time())
        {
        }

        ~TestTimer()
        {
            using namespace std;
            using namespace boost;

            posix_time::ptime now(date_time::microsec_clock<posix_time::ptime>::local_time());
            posix_time::time_duration d = now - start;

            cout << name << " completed in " << d.total_milliseconds() / 1000.0 <<
                " seconds" << endl;
        }

    private:
        std::string name;
        boost::posix_time::ptime start;
};

struct Pixel
{
    Pixel()
    {
    }

    Pixel(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b)
    {
    }

    unsigned char r, g, b;
};

void UseVector()
{
    TestTimer t("UseVector");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels;
        pixels.resize(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

void UseVectorPushBack()
{
    TestTimer t("UseVectorPushBack");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels;
            pixels.reserve(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
            pixels.push_back(Pixel(255, 0, 0));
    }
}

void UseArray()
{
    TestTimer t("UseArray");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        Pixel * pixels = (Pixel *)malloc(sizeof(Pixel) * dimension * dimension);

        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }

        free(pixels);
    }
}

int main()
{
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();
    UseVectorPushBack();

    return 0;
}

我做錯了還是什么? 還是我剛剛打破了這個性能神話?

我在Visual Studio 2005 中使用 Release 模式。


Visual C++ 中#define _SECURE_SCL 0UseVector減少了一半(使其減少到 4 秒)。 這真的很大,IMO。

使用以下內容:

g++ -O3 Time.cpp -I <MyBoost>
./a.out
UseArray 在 2.196 秒內完成
UseVector 在 4.412 秒內完成
UseVectorPushBack 在 8.017 秒內完成
整個過程在 14.626 秒內完成

所以數組的速度是向量的兩倍。

但是在更詳細地查看代碼之后,這是意料之中的; 當你穿過向量兩次而數組只跑一次時。 注意:當您resize()向量時,您不僅要分配內存,還要運行向量並在每個成員上調用構造函數。

稍微重新排列代碼,以便向量只初始化每個對象一次:

 std::vector<Pixel>  pixels(dimensions * dimensions, Pixel(255,0,0));

現在再次執行相同的計時:

g++ -O3 Time.cpp -I <MyBoost>
./a.out
UseVector 在 2.216 秒內完成

向量現在的性能只比數組稍差。 IMO 這種差異微不足道,可能是由與測試無關的一大堆事情引起的。

我還會考慮到您沒有正確初始化/銷毀UseArrray()方法中的 Pixel 對象,因為沒有調用構造函數/析構函數(這對於這個簡單的類來說可能不是問題,但稍微復雜一點的(即指針或帶有指針的成員)會導致問題。

很好的問題。 我來到這里是希望找到一些簡單的修復方法來加快矢量測試的速度。 這並不像我預期的那樣成功!

優化有幫助,但這還不夠。 通過優化,我仍然看到 UseArray 和 UseVector 之間的 2X 性能差異。 有趣的是,在沒有優化的情況下,UseVector 比 UseVectorPushBack 慢得多。

# g++ -Wall -Wextra -pedantic -o vector vector.cpp
# ./vector
UseArray completed in 20.68 seconds
UseVector completed in 120.509 seconds
UseVectorPushBack completed in 37.654 seconds
The whole thing completed in 178.845 seconds
# g++ -Wall -Wextra -pedantic -O3 -o vector vector.cpp
# ./vector
UseArray completed in 3.09 seconds
UseVector completed in 6.09 seconds
UseVectorPushBack completed in 9.847 seconds
The whole thing completed in 19.028 seconds

想法 #1 - 使用 new[] 而不是 malloc

我嘗試在 UseArray 中將malloc()更改為new[]以便構建對象。 並從單個字段分配更改為分配 Pixel 實例。 哦,並將內部循環變量重命名為j

void UseArray()
{
    TestTimer t("UseArray");

    for(int i = 0; i < 1000; ++i)
    {   
        int dimension = 999;

        // Same speed as malloc().
        Pixel * pixels = new Pixel[dimension * dimension];

        for(int j = 0 ; j < dimension * dimension; ++j)
            pixels[j] = Pixel(255, 0, 0);

        delete[] pixels;
    }
}

令人驚訝的是(對我而言),這些變化都沒有產生任何影響。 甚至對new[]的更改都不會默認構造所有像素。 似乎 gcc 可以在使用new[]時優化默認構造函數調用,但在使用vector時則不能。

想法#2 - 刪除重復的 operator[] 調用

我還試圖擺脫三重operator[]查找並緩存對pixels[j]的引用。 這實際上減慢了 UseVector 的速度! 哎呀。

for(int j = 0; j < dimension * dimension; ++j)
{
    // Slower than accessing pixels[j] three times.
    Pixel &pixel = pixels[j];
    pixel.r = 255;
    pixel.g = 0;
    pixel.b = 0;
}

# ./vector 
UseArray completed in 3.226 seconds
UseVector completed in 7.54 seconds
UseVectorPushBack completed in 9.859 seconds
The whole thing completed in 20.626 seconds

想法 #3 - 刪除構造函數

完全刪除構造函數怎么樣? 然后也許 gcc 可以在創建向量時優化所有對象的構造。 如果我們將 Pixel 更改為:

struct Pixel
{
    unsigned char r, g, b;
};

結果:大約快 10%。 仍然比數組慢。 嗯。

# ./vector 
UseArray completed in 3.239 seconds
UseVector completed in 5.567 seconds

想法 #4 - 使用迭代器而不是循環索引

如何使用vector<Pixel>::iterator而不是循環索引?

for (std::vector<Pixel>::iterator j = pixels.begin(); j != pixels.end(); ++j)
{
    j->r = 255;
    j->g = 0;
    j->b = 0;
}

結果:

# ./vector 
UseArray completed in 3.264 seconds
UseVector completed in 5.443 seconds

不,沒有什么不同。 至少不會變慢。 我認為這將具有類似於我使用Pixel&參考的 #2 的性能。

結論

即使一些聰明的 cookie 想出如何使向量循環與數組一樣快,這也不能很好地說明std::vector的默認行為。 編譯器足夠聰明,可以優化所有 C++ 特性並使 STL 容器與原始數組一樣快。

最重要的是,在使用std::vector時,編譯器無法優化掉無操作默認構造函數調用。 如果您使用普通的new[]它可以很好地優化它們。 但不是std::vector 即使你可以重寫你的代碼來消除那些面對這里的口頭禪的構造函數調用:“編譯器比你更聰明。STL 和普通 C 一樣快。別擔心。”

這是一個古老但流行的問題。

此時,許多程序員將使用 C++11。 在 C++11 中,OP 的代碼對於UseArrayUseVector運行速度同樣快。

UseVector completed in 3.74482 seconds
UseArray completed in 3.70414 seconds

根本問題是,雖然您的Pixel結構未初始化,但std::vector<T>::resize( size_t, T const&=T() )會采用默認構造的Pixel並將其復制 編譯器沒有注意到它被要求復制未初始化的數據,所以它實際上執行了復制。

在 C++11 中, std::vector<T>::resize有兩個重載。 第一個是std::vector<T>::resize(size_t) ,另一個是std::vector<T>::resize(size_t, T const&) 這意味着當你在沒有第二個參數的情況下調用resize時,它只是默認構造,並且編譯器足夠聰明,意識到默認構造什么都不做,所以它跳過緩沖區的傳遞。

(添加用於處理可移動、可構造和不可復制類型的兩個重載——處理未初始化數據時的性能改進是一個獎勵)。

push_back解決方案還進行了 fencepost 檢查,這會減慢它的速度,因此它仍然比malloc版本慢。

現場示例(我還用chrono::high_resolution_clock替換了計時器)。

請注意,如果您有一個通常需要初始化的結構,但您想在增加緩沖區后處理它,則可以使用自定義std::vector分配器來執行此操作。 如果您想將其移動到更正常的std::vector ,我相信仔細使用allocator_traits和覆蓋==可能會實現這一點,但我不確定。

公平地說,您不能將 C++ 實現與 C 實現進行比較,因為我會稱您為 malloc 版本。 malloc 不創建對象 - 它只分配原始內存。 然后將該內存視為對象而不調用構造函數是糟糕的 C++(可能無效 - 我將把它留給語言律師)。

也就是說,簡單地將 malloc 更改為new Pixel[dimensions*dimensions]並自由delete [] pixels與您擁有的 Pixel 的簡單實現沒有太大區別。 這是我的盒子(E6600,64 位)上的結果:

UseArray completed in 0.269 seconds
UseVector completed in 1.665 seconds
UseVectorPushBack completed in 7.309 seconds
The whole thing completed in 9.244 seconds

但是稍有變化,表格就變成了:

像素.h

struct Pixel
{
    Pixel();
    Pixel(unsigned char r, unsigned char g, unsigned char b);

    unsigned char r, g, b;
};

像素.cc

#include "Pixel.h"

Pixel::Pixel() {}
Pixel::Pixel(unsigned char r, unsigned char g, unsigned char b) 
  : r(r), g(g), b(b) {}

主文件

#include "Pixel.h"
[rest of test harness without class Pixel]
[UseArray now uses new/delete not malloc/free]

這樣編譯:

$ g++ -O3 -c -o Pixel.o Pixel.cc
$ g++ -O3 -c -o main.o main.cc
$ g++ -o main main.o Pixel.o

我們得到非常不同的結果:

UseArray completed in 2.78 seconds
UseVector completed in 1.651 seconds
UseVectorPushBack completed in 7.826 seconds
The whole thing completed in 12.258 seconds

使用 Pixel 的非內聯構造函數,std::vector 現在勝過原始數組。

通過 std::vector 和 std:allocator 分配的復雜性似乎太高了,無法像簡單的new Pixel[n]那樣有效地優化。 但是,我們可以看到問題只是分配而不是向量訪問,方法是調整幾個測試函數,通過將向量/數組移到循環外來創建向量/數組:

void UseVector()
{
    TestTimer t("UseVector");

    int dimension = 999;
    std::vector<Pixel> pixels;
    pixels.resize(dimension * dimension);

    for(int i = 0; i < 1000; ++i)
    {
        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

void UseArray()
{
    TestTimer t("UseArray");

    int dimension = 999;
    Pixel * pixels = new Pixel[dimension * dimension];

    for(int i = 0; i < 1000; ++i)
    {
        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
    delete [] pixels;
}

我們現在得到這些結果:

UseArray completed in 0.254 seconds
UseVector completed in 0.249 seconds
UseVectorPushBack completed in 7.298 seconds
The whole thing completed in 7.802 seconds

從中我們可以了解到,std::vector 相當於一個原始數組進行訪問,但是如果你需要多次創建和刪除向量/數組,創建一個復雜的對象會比創建一個簡單的數組更耗時當元素的構造函數沒有內聯時。 我不認為這是非常令人驚訝的。

試試這個:

void UseVectorCtor()
{
    TestTimer t("UseConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension * dimension, Pixel(255, 0, 0));
    }
}

我得到的性能幾乎與數組完全相同。

vector的問題在於它是一個比數組更通用的工具。 這意味着您必須考慮如何使用它。 它可以以多種不同的方式使用,提供數組甚至沒有的功能。 如果你為了你的目的“錯誤地”使用它,你會產生很多開銷,但如果你正確使用它,它通常基本上是一個零開銷的數據結構。 在這種情況下,問題在於您單獨初始化了向量(導致所有元素都調用了它們的默認構造函數),然后用正確的值單獨覆蓋每個元素。 對於編譯器來說,這比使用數組做同樣的事情更難優化。 這就是為什么 vector 提供了一個構造函數,它可以讓你做到這一點:用值X初始化N元素。

當你使用它時,向量和數組一樣快。

所以不,你還沒有打破性能神話。 但是你已經證明只有當你最佳地使用向量時它才是正確的,這也是一個很好的觀點。 :)

從好的方面來說,它確實是最簡單的用法,但結果卻是最快的。 如果您將我的代碼片段(單行)與 John Kugelman 的答案進行對比,其中包含大量的調整和優化,但仍然不能完全消除性能差異,很明顯vector畢竟設計得很巧妙。 您不必跳過箍來獲得與數組相等的速度。 相反,您必須使用最簡單的解決方案。

當我第一次查看您的代碼時,這幾乎不是一個公平的比較。 我絕對認為你不是在拿蘋果和蘋果比較。 所以我想,讓我們在所有測試中調用構造函數和析構函數; 然后比較。

const size_t dimension = 1000;

void UseArray() {
    TestTimer t("UseArray");
    for(size_t j = 0; j < dimension; ++j) {
        Pixel* pixels = new Pixel[dimension * dimension];
        for(size_t i = 0 ; i < dimension * dimension; ++i) {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = (unsigned char) (i % 255);
        }
        delete[] pixels;
    }
}

void UseVector() {
    TestTimer t("UseVector");
    for(size_t j = 0; j < dimension; ++j) {
        std::vector<Pixel> pixels(dimension * dimension);
        for(size_t i = 0; i < dimension * dimension; ++i) {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = (unsigned char) (i % 255);
        }
    }
}

int main() {
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();

    return 0;
}

我的想法是,通過這種設置,它們應該完全相同 事實證明,我錯了。

UseArray completed in 3.06 seconds
UseVector completed in 4.087 seconds
The whole thing completed in 10.14 seconds

那么為什么會出現這 30% 的性能損失呢? STL 包含頭文件中的所有內容,因此編譯器應該可以理解所需的所有內容。

我的想法是循環如何將所有值初始化為默認構造函數。 所以我進行了一個測試:

class Tester {
public:
    static int count;
    static int count2;
    Tester() { count++; }
    Tester(const Tester&) { count2++; }
};
int Tester::count = 0;
int Tester::count2 = 0;

int main() {
    std::vector<Tester> myvec(300);
    printf("Default Constructed: %i\nCopy Constructed: %i\n", Tester::count, Tester::count2);

    return 0;
}

結果如我所料:

Default Constructed: 1
Copy Constructed: 300

這顯然是減速的根源,事實上向量使用復制構造函數從默認構造對象初始化元素。

這意味着,在構建向量期間會發生以下偽操作順序:

Pixel pixel;
for (auto i = 0; i < N; ++i) vector[i] = pixel;

由於編譯器的隱式復制構造函數,其中擴展為以下內容:

Pixel pixel;
for (auto i = 0; i < N; ++i) {
    vector[i].r = pixel.r;
    vector[i].g = pixel.g;
    vector[i].b = pixel.b;
}

因此默認Pixel保持未初始化,而其余部分使用默認Pixel未初始化進行初始化

New[] / Delete[]的替代情況相比:

int main() {
    Tester* myvec = new Tester[300];

    printf("Default Constructed: %i\nCopy Constructed:%i\n", Tester::count, Tester::count2);

    delete[] myvec;

    return 0;
}

Default Constructed: 300
Copy Constructed: 0

它們都保留為未初始化的值,並且沒有對序列進行雙重迭代。

有了這些信息,我們如何測試呢? 讓我們嘗試覆蓋隱式復制構造函數。

Pixel(const Pixel&) {}

結果呢?

UseArray completed in 2.617 seconds
UseVector completed in 2.682 seconds
The whole thing completed in 5.301 seconds

總而言之,如果您經常制作數百個向量:請重新考慮您的算法

在任何情況下,由於某些未知原因, STL實現並不會變慢,它只是完全按照您的要求執行; 希望你知道得更好。

嘗試禁用已檢查的迭代器並在發布模式下構建。 您不應該看到太大的性能差異。

GNU 的 STL(和其他),給定vector<T>(n) ,默認構造一個原型對象T() - 編譯器將優化掉空的構造函數 - 但隨后在現在保留的內存地址中的任何垃圾的副本對象由 STL 的__uninitialized_fill_n_aux ,它循環填充該對象的副本作為向量中的默認值。 所以,“我的”STL 不是循環構造,而是構造然后循環/復制。 這是違反直覺的,但我應該記住,因為我評論了最近關於這一點的 stackoverflow 問題:構造/復制對於引用計數對象等可以更有效。

所以:

vector<T> x(n);

vector<T> x;
x.resize(n);

是 - 在許多 STL 實現中 - 類似於:

T temp;
for (int i = 0; i < n; ++i)
    x[i] = temp;

問題在於,從 temp 是未初始化的垃圾的洞察力來看,當前一代編譯器優化器似乎不起作用,並且無法優化循環和默認復制構造函數調用。 您可以可信地爭辯說編譯器絕對不應該優化它,因為編寫上述內容的程序員有一個合理的期望,即所有對象在循環后都是相同的,即使是垃圾(關於“相同”/operator== vs memcmp/operator= 等適用)。 不能期望編譯器對 std::vector<> 的更大上下文有任何額外的了解,或者對表明此優化安全的數據的后期使用有任何額外的了解。

這可以與更明顯的直接實現形成對比:

for (int i = 0; i < n; ++i)
    x[i] = T();

我們可以期望編譯器對其進行優化。

為了更明確地說明 vector 行為的這一方面的理由,請考慮:

std::vector<big_reference_counted_object> x(10000);

顯然,如果我們制作 10000 個獨立對象與 10000 個引用相同數據,這是一個主要區別。 有一個合理的論點是,保護臨時 C++ 用戶免於意外地做如此昂貴的事情的優勢超過了難以優化的副本構造的非常小的現實成本。

原始答案(供參考/理解評論):沒有機會。 vector 與數組一樣快,至少如果您明智地保留空間。 ...

Martin York 的回答讓我感到困擾,因為這似乎是試圖將初始化問題隱藏在地毯下。 但他將多余的默認構造確定為性能問題的根源是正確的。

[編輯:馬丁的回答不再建議更改默認構造函數。]

對於手頭的直接問題,您當然可以調用vector<Pixel> ctor 的 2 參數版本:

std::vector<Pixel> pixels(dimension * dimension, Pixel(255, 0, 0));

如果你想用一個常數值初始化,這是一種常見的情況。 但更普遍的問題是:如何用比常量值更復雜的東西有效地初始化?

為此,您可以使用back_insert_iterator ,它是一個迭代器適配器。 這是一個帶有int向量的示例,盡管總體思路對Pixel也同樣適用:

#include <iterator>
// Simple functor return a list of squares: 1, 4, 9, 16...
struct squares {
    squares() { i = 0; }
    int operator()() const { ++i; return i * i; }

private:
    int i;
};

...

std::vector<int> v;
v.reserve(someSize);     // To make insertions efficient
std::generate_n(std::back_inserter(v), someSize, squares());

或者,您可以使用copy()transform()而不是generate_n()

缺點是構造初始值的邏輯需要移到一個單獨的類中,這比將其放在適當的位置方便(盡管 C++1x 中的 lambda 使這更好)。 此外,我預計這仍然不會像基於malloc()的非 STL 版本一樣快,但我預計它會很接近,因為它只對每個元素進行一次構造。

向量的另外調用像素構造函數。

每個都會導致您計時的近一百萬次 ctor 運行。

編輯:然后是外部 1...1000 循環,因此調用 10 億個 ctor!

編輯 2:看到 UseArray 案例的反匯編會很有趣。 優化器可以優化整個事情,因為它除了消耗 CPU 之外沒有任何影響。

以下是 vector 中push_back方法的工作原理:

  1. 向量在初始化時分配 X 量的空間。
  2. 如下所述,它檢查當前底層數組中是否有空間用於該項目。
  3. 它在 push_back 調用中制作項目的副本。

調用push_back X 項后:

  1. 該向量將 kX 空間量重新分配到第二個數組中。
  2. 它將第一個數組的條目復制到第二個數組中。
  3. 丟棄第一個數組。
  4. 現在使用第二個數組作為存儲,直到它達到 kX 個條目。

重復。 如果您不reserving空間,它肯定會變慢。 更重要的是,如果復制該項目的成本很高,那么像這樣的“push_back”會活活吃掉你。

至於vector與數組的事情,我將不得不同意其他人的看法。 在發布中運行,打開優化,並添加更多標志,以便 Microsoft 的友好人員不會#@%$^ 為您准備好。

還有一件事,如果您不需要調整大小,請使用 Boost.Array。

一些分析器數據(像素對齊到 32 位):

g++ -msse3 -O3 -ftree-vectorize -g test.cpp -DNDEBUG && ./a.out
UseVector completed in 3.123 seconds
UseArray completed in 1.847 seconds
UseVectorPushBack completed in 9.186 seconds
The whole thing completed in 14.159 seconds

廢話

andrey@nv:~$ opannotate --source libcchem/src/a.out  | grep "Total samples for file" -A3
Overflow stats not available
 * Total samples for file : "/usr/include/c++/4.4/ext/new_allocator.h"
 *
 * 141008 52.5367
 */
--
 * Total samples for file : "/home/andrey/libcchem/src/test.cpp"
 *
 *  61556 22.9345
 */
--
 * Total samples for file : "/usr/include/c++/4.4/bits/stl_vector.h"
 *
 *  41956 15.6320
 */
--
 * Total samples for file : "/usr/include/c++/4.4/bits/stl_uninitialized.h"
 *
 *  20956  7.8078
 */
--
 * Total samples for file : "/usr/include/c++/4.4/bits/stl_construct.h"
 *
 *   2923  1.0891
 */

allocator

               :      // _GLIBCXX_RESOLVE_LIB_DEFECTS
               :      // 402. wrong new expression in [some_] allocator::construct
               :      void
               :      construct(pointer __p, const _Tp& __val)
141008 52.5367 :      { ::new((void *)__p) _Tp(__val); }

vector

               :void UseVector()
               :{ /* UseVector() total:  60121 22.3999 */
...
               :
               :
 10790  4.0201 :        for (int i = 0; i < dimension * dimension; ++i) {
               :
   495  0.1844 :            pixels[i].r = 255;
               :
 12618  4.7012 :            pixels[i].g = 0;
               :
  2253  0.8394 :            pixels[i].b = 0;
               :
               :        }

數組

               :void UseArray()
               :{ /* UseArray() total:  35191 13.1114 */
               :
...
               :
   136  0.0507 :        for (int i = 0; i < dimension * dimension; ++i) {
               :
  9897  3.6874 :            pixels[i].r = 255;
               :
  3511  1.3081 :            pixels[i].g = 0;
               :
 21647  8.0652 :            pixels[i].b = 0;

大部分開銷都在復制構造函數中。 例如,

    std::vector < Pixel > pixels;//(dimension * dimension, Pixel());

    pixels.reserve(dimension * dimension);

    for (int i = 0; i < dimension * dimension; ++i) {

        pixels[i].r = 255;

        pixels[i].g = 0;

        pixels[i].b = 0;
    }

它具有與數組相同的性能。

更好的基准測試(我認為...),由於優化的編譯器可以更改代碼,因為分配的向量/數組的結果不在任何地方使用。 結果:

$ g++ test.cpp -o test -O3 -march=native
$ ./test 
UseArray inner completed in 0.652 seconds
UseArray completed in 0.773 seconds
UseVector inner completed in 0.638 seconds
UseVector completed in 0.757 seconds
UseVectorPushBack inner completed in 6.732 seconds
UseVectorPush completed in 6.856 seconds
The whole thing completed in 8.387 seconds

編譯器:

gcc version 6.2.0 20161019 (Debian 6.2.0-9)

中央處理器:

model name  : Intel(R) Core(TM) i7-3630QM CPU @ 2.40GHz

和代碼:

#include <cstdlib>
#include <vector>

#include <iostream>
#include <string>

#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/microsec_time_clock.hpp>

class TestTimer
{
    public:
        TestTimer(const std::string & name) : name(name),
            start(boost::date_time::microsec_clock<boost::posix_time::ptime>::local_time())
        {
        }

        ~TestTimer()
        {
            using namespace std;
            using namespace boost;

            posix_time::ptime now(date_time::microsec_clock<posix_time::ptime>::local_time());
            posix_time::time_duration d = now - start;

            cout << name << " completed in " << d.total_milliseconds() / 1000.0 <<
                " seconds" << endl;
        }

    private:
        std::string name;
        boost::posix_time::ptime start;
};

struct Pixel
{
    Pixel()
    {
    }

    Pixel(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b)
    {
    }

    unsigned char r, g, b;
};

void UseVector(std::vector<std::vector<Pixel> >& results)
{
    TestTimer t("UseVector inner");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel>& pixels = results.at(i);
        pixels.resize(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

void UseVectorPushBack(std::vector<std::vector<Pixel> >& results)
{
    TestTimer t("UseVectorPushBack inner");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel>& pixels = results.at(i);
            pixels.reserve(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
            pixels.push_back(Pixel(255, 0, 0));
    }
}

void UseArray(Pixel** results)
{
    TestTimer t("UseArray inner");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        Pixel * pixels = (Pixel *)malloc(sizeof(Pixel) * dimension * dimension);

        results[i] = pixels;

        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }

        // free(pixels);
    }
}

void UseArray()
{
    TestTimer t("UseArray");
    Pixel** array = (Pixel**)malloc(sizeof(Pixel*)* 1000);
    UseArray(array);
    for(int i=0;i<1000;++i)
        free(array[i]);
    free(array);
}

void UseVector()
{
    TestTimer t("UseVector");
    {
        std::vector<std::vector<Pixel> > vector(1000, std::vector<Pixel>());
        UseVector(vector);
    }
}

void UseVectorPushBack()
{
    TestTimer t("UseVectorPush");
    {
        std::vector<std::vector<Pixel> > vector(1000, std::vector<Pixel>());
        UseVectorPushBack(vector);
    }
}


int main()
{
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();
    UseVectorPushBack();

    return 0;
}

我做了一些廣泛的測試,我現在想做一段時間。 不妨也分享一下。

這是我的雙引導機器 i7-3770、16GB 內存、x86_64,在 Windows 8.1 和 Ubuntu 16.04 上。 更多信息和結論,評論如下。 測試了 MSVS 2017 和 g++(在 Windows 和 Linux 上)。

測試程序

#include <iostream>
#include <chrono>
//#include <algorithm>
#include <array>
#include <locale>
#include <vector>
#include <queue>
#include <deque>

// Note: total size of array must not exceed 0x7fffffff B = 2,147,483,647B
//  which means that largest int array size is 536,870,911
// Also image size cannot be larger than 80,000,000B
constexpr int long g_size = 100000;
int g_A[g_size];


int main()
{
    std::locale loc("");
    std::cout.imbue(loc);
    constexpr int long size = 100000;  // largest array stack size

    // stack allocated c array
    std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
    int A[size];
    for (int i = 0; i < size; i++)
        A[i] = i;

    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "c-style stack array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "c-style stack array size=" << sizeof(A) << "B\n\n";

    // global stack c array
    start = std::chrono::steady_clock::now();
    for (int i = 0; i < g_size; i++)
        g_A[i] = i;

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "global c-style stack array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "global c-style stack array size=" << sizeof(g_A) << "B\n\n";

    // raw c array heap array
    start = std::chrono::steady_clock::now();
    int* AA = new int[size];    // bad_alloc() if it goes higher than 1,000,000,000
    for (int i = 0; i < size; i++)
        AA[i] = i;

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "c-style heap array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "c-style heap array size=" << sizeof(AA) << "B\n\n";
    delete[] AA;

    // std::array<>
    start = std::chrono::steady_clock::now();
    std::array<int, size> AAA;
    for (int i = 0; i < size; i++)
        AAA[i] = i;
    //std::sort(AAA.begin(), AAA.end());

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::array size=" << sizeof(AAA) << "B\n\n";

    // std::vector<>
    start = std::chrono::steady_clock::now();
    std::vector<int> v;
    for (int i = 0; i < size; i++)
        v.push_back(i);
    //std::sort(v.begin(), v.end());

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::vector duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::vector size=" << v.size() * sizeof(v.back()) << "B\n\n";

    // std::deque<>
    start = std::chrono::steady_clock::now();
    std::deque<int> dq;
    for (int i = 0; i < size; i++)
        dq.push_back(i);
    //std::sort(dq.begin(), dq.end());

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::deque duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::deque size=" << dq.size() * sizeof(dq.back()) << "B\n\n";

    // std::queue<>
    start = std::chrono::steady_clock::now();
    std::queue<int> q;
    for (int i = 0; i < size; i++)
        q.push(i);

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::queue duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::queue size=" << q.size() * sizeof(q.front()) << "B\n\n";
}

結果

//////////////////////////////////////////////////////////////////////////////////////////
// with MSVS 2017:
// >> cl /std:c++14 /Wall -O2 array_bench.cpp
//
// c-style stack array duration=0.15ms
// c-style stack array size=400,000B
//
// global c-style stack array duration=0.130ms
// global c-style stack array size=400,000B
//
// c-style heap array duration=0.90ms
// c-style heap array size=4B
//
// std::array duration=0.20ms
// std::array size=400,000B
//
// std::vector duration=0.544ms
// std::vector size=400,000B
//
// std::deque duration=1.375ms
// std::deque size=400,000B
//
// std::queue duration=1.491ms
// std::queue size=400,000B
//
//////////////////////////////////////////////////////////////////////////////////////////
//
// with g++ version:
//      - (tdm64-1) 5.1.0 on Windows
//      - (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609 on Ubuntu 16.04
// >> g++ -std=c++14 -Wall -march=native -O2 array_bench.cpp -o array_bench
//
// c-style stack array duration=0ms
// c-style stack array size=400,000B
//
// global c-style stack array duration=0.124ms
// global c-style stack array size=400,000B
//
// c-style heap array duration=0.648ms
// c-style heap array size=8B
//
// std::array duration=1ms
// std::array size=400,000B
//
// std::vector duration=0.402ms
// std::vector size=400,000B
//
// std::deque duration=0.234ms
// std::deque size=400,000B
//
// std::queue duration=0.304ms
// std::queue size=400,000
//
//////////////////////////////////////////////////////////////////////////////////////////

筆記

  • 由平均 10 次運行組裝而成。
  • 我最初也用std::sort()進行了測試(你可以看到它被注釋掉了)但后來刪除了它們,因為沒有顯着的相對差異。

我的結論和評論

  • 注意全局 c 樣式數組如何花費幾乎與堆 c 樣式數組一樣多的時間
  • 在所有測試中,我注意到std::array在連續運行之間的時間變化具有顯着的穩定性,而其他尤其是 std:: 數據結構相比之下變化很大
  • O3 優化沒有顯示任何值得注意的時間差異
  • 在 Windows cl(無 -O2)和 g++(Win/Linux 無 -O2,無 -march=native)上移除優化會顯着增加時間。 特別是對於 std::data 結構。 MSVS 上的總體時間比 g++ 高,但std::array和 c 樣式數組在 Windows 上速度更快,無需優化
  • g++ 生成的代碼比微軟的編譯器更快(顯然它甚至在 Windows 上運行得更快)。

判決書

當然,這是優化構建的代碼。 既然問題是關於std::vector那么是的!很多! 比普通數組慢(優化/未優化)。 但是當您進行基准測試時,您自然希望生成優化的代碼。

不過對我來說,這部劇的主角是std::array

我的筆記本電腦是 Lenova G770(4 GB RAM)。

操作系統是 Windows 7 64 位(帶筆記本電腦的)

編譯器是MinGW 4.6.1。

IDE 是Code::Blocks

我測試了第一篇文章的源代碼。

結果

氧氣優化

UseArray 在 2.841 秒內完成

UseVector 在 2.548 秒內完成

UseVectorPushBack 在 11.95 秒內完成

整個過程在 17.342 秒內完成

系統暫停

O3優化

UseArray 在 1.452 秒內完成

UseVector 在 2.514 秒內完成

UseVectorPushBack 在 12.967 秒內完成

整個過程在 16.937 秒內完成

看起來矢量在 O3 優化下的性能更差。

如果您將循環更改為

    pixels[i].r = i;
    pixels[i].g = i;
    pixels[i].b = i;

O2和O3下array和vector的速度幾乎一樣。

順便說一下,在使用 vector 的類中看到的速度減慢也發生在像 int 這樣的標准類型中。 這是一個多線程代碼:

#include <iostream>
#include <cstdio>
#include <map>
#include <string>
#include <typeinfo>
#include <vector>
#include <pthread.h>
#include <sstream>
#include <fstream>
using namespace std;

//pthread_mutex_t map_mutex=PTHREAD_MUTEX_INITIALIZER;

long long num=500000000;
int procs=1;

struct iterate
{
    int id;
    int num;
    void * member;
    iterate(int a, int b, void *c) : id(a), num(b), member(c) {}
};

//fill out viterate and piterate
void * viterate(void * input)
{
    printf("am in viterate\n");
    iterate * info=static_cast<iterate *> (input);
    // reproduce member type
    vector<int> test= *static_cast<vector<int>*> (info->member);
    for (int i=info->id; i<test.size(); i+=info->num)
    {
        //printf("am in viterate loop\n");
        test[i];
    }
    pthread_exit(NULL);
}

void * piterate(void * input)
{
    printf("am in piterate\n");
    iterate * info=static_cast<iterate *> (input);;
    int * test=static_cast<int *> (info->member);
    for (int i=info->id; i<num; i+=info->num) {
        //printf("am in piterate loop\n");
        test[i];
    }
    pthread_exit(NULL);
}

int main()
{
    cout<<"producing vector of size "<<num<<endl;
    vector<int> vtest(num);
    cout<<"produced  a vector of size "<<vtest.size()<<endl;
    pthread_t thread[procs];

    iterate** it=new iterate*[procs];
    int ans;
    void *status;

    cout<<"begining to thread through the vector\n";
    for (int i=0; i<procs; i++) {
        it[i]=new iterate(i, procs, (void *) &vtest);
    //  ans=pthread_create(&thread[i],NULL,viterate, (void *) it[i]);
    }
    for (int i=0; i<procs; i++) {
        pthread_join(thread[i], &status);
    }
    cout<<"end of threading through the vector";
    //reuse the iterate structures

    cout<<"producing a pointer with size "<<num<<endl;
    int * pint=new int[num];
    cout<<"produced a pointer with size "<<num<<endl;

    cout<<"begining to thread through the pointer\n";
    for (int i=0; i<procs; i++) {
        it[i]->member=&pint;
        ans=pthread_create(&thread[i], NULL, piterate, (void*) it[i]);
    }
    for (int i=0; i<procs; i++) {
        pthread_join(thread[i], &status);
    }
    cout<<"end of threading through the pointer\n";

    //delete structure array for iterate
    for (int i=0; i<procs; i++) {
        delete it[i];
    }
    delete [] it;

    //delete pointer
    delete [] pint;

    cout<<"end of the program"<<endl;
    return 0;
}

代碼的行為表明 vector 的實例化是代碼中最長的部分。 一旦你克服了那個瓶頸。 其余代碼運行速度非常快。 無論您運行多少個線程,這都是正確的。

順便說一下,忽略絕對瘋狂的包含數量。 我一直在使用此代碼來測試項目的內容,因此包含的數量不斷增加。

我只想提一下,vector(和 smart_ptr)只是在原始數組(和原始指針)之上添加的一個薄層。 實際上,連續內存中向量的訪問時間比數組快。 以下代碼顯示了初始化和訪問向量和數組的結果。

#include <boost/date_time/posix_time/posix_time.hpp>
#include <iostream>
#include <vector>
#define SIZE 20000
int main() {
    srand (time(NULL));
    vector<vector<int>> vector2d;
    vector2d.reserve(SIZE);
    int index(0);
    boost::posix_time::ptime start_total = boost::posix_time::microsec_clock::local_time();
    //  timer start - build + access
    for (int i = 0; i < SIZE; i++) {
        vector2d.push_back(vector<int>(SIZE));
    }
    boost::posix_time::ptime start_access = boost::posix_time::microsec_clock::local_time();
    //  timer start - access
    for (int i = 0; i < SIZE; i++) {
        index = rand()%SIZE;
        for (int j = 0; j < SIZE; j++) {

            vector2d[index][index]++;
        }
    }
    boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time();
    boost::posix_time::time_duration msdiff = end - start_total;
    cout << "Vector total time: " << msdiff.total_milliseconds() << "milliseconds.\n";
    msdiff = end - start_acess;
    cout << "Vector access time: " << msdiff.total_milliseconds() << "milliseconds.\n"; 


    int index(0);
    int** raw2d = nullptr;
    raw2d = new int*[SIZE];
    start_total = boost::posix_time::microsec_clock::local_time();
    //  timer start - build + access
    for (int i = 0; i < SIZE; i++) {
        raw2d[i] = new int[SIZE];
    }
    start_access = boost::posix_time::microsec_clock::local_time();
    //  timer start - access
    for (int i = 0; i < SIZE; i++) {
        index = rand()%SIZE;
        for (int j = 0; j < SIZE; j++) {

            raw2d[index][index]++;
        }
    }
    end = boost::posix_time::microsec_clock::local_time();
    msdiff = end - start_total;
    cout << "Array total time: " << msdiff.total_milliseconds() << "milliseconds.\n";
    msdiff = end - start_acess;
    cout << "Array access time: " << msdiff.total_milliseconds() << "milliseconds.\n"; 
    for (int i = 0; i < SIZE; i++) {
        delete [] raw2d[i];
    }
    return 0;
}

輸出是:

    Vector total time: 925milliseconds.
    Vector access time: 4milliseconds.
    Array total time: 30milliseconds.
    Array access time: 21milliseconds.

所以如果你使用得當,速度幾乎是一樣的。 (正如其他人提到的使用reserve() 或resize())。

嗯,因為 vector::resize() 比普通的內存分配(通過 malloc)做了更多的處理。

嘗試在您的復制構造函數中放置一個斷點(定義它以便您可以斷點!)並且會增加額外的處理時間。

使用正確的選項,向量和數組可以生成相同的 asm 在這些情況下,它們當然具有相同的速度,因為無論哪種方式您都會獲得相同的可執行文件。

我不得不說我不是 C++ 專家。 但要補充一些實驗結果:

編譯:gcc-6.2.0/bin/g++ -O3 -std=c++14 vector.cpp

機器:

Intel(R) Xeon(R) CPU E5-2690 v2 @ 3.00GHz 

操作系統:

2.6.32-642.13.1.el6.x86_64

輸出:

UseArray completed in 0.167821 seconds
UseVector completed in 0.134402 seconds
UseConstructor completed in 0.134806 seconds
UseFillConstructor completed in 1.00279 seconds
UseVectorPushBack completed in 6.6887 seconds
The whole thing completed in 8.12888 seconds

這里唯一讓我感到奇怪的是“UseFillConstructor”的性能與“UseConstructor”相比。

代碼:

void UseConstructor()
{
    TestTimer t("UseConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension*dimension);
        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}


void UseFillConstructor()
{
    TestTimer t("UseFillConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension*dimension, Pixel(255,0,0));
    }
}

因此,提供的額外“值”會大大降低性能,我認為這是由於多次調用復制構造函數所致。 但是……

編譯:

gcc-6.2.0/bin/g++ -std=c++14 -O vector.cpp

輸出:

UseArray completed in 1.02464 seconds
UseVector completed in 1.31056 seconds
UseConstructor completed in 1.47413 seconds
UseFillConstructor completed in 1.01555 seconds
UseVectorPushBack completed in 6.9597 seconds
The whole thing completed in 11.7851 seconds

所以在這種情況下,gcc 優化非常重要,但是當一個值被提供為默認值時它對你沒有多大幫助。 這,其實是違背我的學費的。 希望它可以幫助新程序員選擇哪種向量初始化格式。

它似乎取決於編譯器標志。 這是一個基准代碼:

#include <chrono>
#include <cmath>
#include <ctime>
#include <iostream>
#include <vector>


int main(){

    int size = 1000000; // reduce this number in case your program crashes
    int L = 10;

    std::cout << "size=" << size << " L=" << L << std::endl;
    {
        srand( time(0) );
        double * data = new double[size];
        double result = 0.;
        std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
        for( int l = 0; l < L; l++ ) {
            for( int i = 0; i < size; i++ ) data[i] = rand() % 100;
            for( int i = 0; i < size; i++ ) result += data[i] * data[i];
        }
        std::chrono::steady_clock::time_point end   = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
        std::cout << "Calculation result is " << sqrt(result) << "\n";
        std::cout << "Duration of C style heap array:    " << duration << "ms\n";
        delete data;
    }

    {
        srand( 1 + time(0) );
        double data[size]; // technically, non-compliant with C++ standard.
        double result = 0.;
        std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
        for( int l = 0; l < L; l++ ) {
            for( int i = 0; i < size; i++ ) data[i] = rand() % 100;
            for( int i = 0; i < size; i++ ) result += data[i] * data[i];
        }
        std::chrono::steady_clock::time_point end   = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
        std::cout << "Calculation result is " << sqrt(result) << "\n";
        std::cout << "Duration of C99 style stack array: " << duration << "ms\n";
    }

    {
        srand( 2 + time(0) );
        std::vector<double> data( size );
        double result = 0.;
        std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
        for( int l = 0; l < L; l++ ) {
            for( int i = 0; i < size; i++ ) data[i] = rand() % 100;
            for( int i = 0; i < size; i++ ) result += data[i] * data[i];
        }
        std::chrono::steady_clock::time_point end   = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
        std::cout << "Calculation result is " << sqrt(result) << "\n";
        std::cout << "Duration of std::vector array:     " << duration << "ms\n";
    }

    return 0;
}

不同的優化標志給出不同的答案:

$ g++ -O0 benchmark.cpp 
$ ./a.out 
size=1000000 L=10
Calculation result is 181182
Duration of C style heap array:    118441ms
Calculation result is 181240
Duration of C99 style stack array: 104920ms
Calculation result is 181210
Duration of std::vector array:     124477ms
$g++ -O3 benchmark.cpp
$ ./a.out 
size=1000000 L=10
Calculation result is 181213
Duration of C style heap array:    107803ms
Calculation result is 181198
Duration of C99 style stack array: 87247ms
Calculation result is 181204
Duration of std::vector array:     89083ms
$ g++ -Ofast benchmark.cpp 
$ ./a.out 
size=1000000 L=10
Calculation result is 181164
Duration of C style heap array:    93530ms
Calculation result is 181179
Duration of C99 style stack array: 80620ms
Calculation result is 181191
Duration of std::vector array:     78830ms

您的確切結果會有所不同,但這在我的機器上非常典型。

根據我的經驗,有時,只是有時, vector<int>可能比int[]慢很多倍。 要記住的一件事是,向量的向量與int[][]非常不同。 由於元素在內存中可能不連續。 這意味着您可以在主向量中調整不同向量的大小,但 CPU 可能無法像int[][]那樣緩存元素。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM