簡體   English   中英

為什么遍歷 std::set 比遍歷 std::vector 慢得多?

[英]Why is iterating over a std::set so much slower than over a std::vector?

在優化性能關鍵代碼時,我注意到迭代 std::set 有點慢。

然后我編寫了一個基准測試程序,並通過迭代器( auto it : vector )測試了向量的迭代速度,通過迭代器迭代了一個集合,並通過索引迭代了一個向量( int i = 0; i < vector.size(); ++i )。

容器的構造相同,具有 1024 個隨機整數。 (當然,每個 int 都是唯一的,因為我們使用的是集合)。 然后,對於每次運行,我們循環遍歷容器並將它們的整數相加為一個長整數。 每次運行都有 1000 次迭代進行求和,測試平均超過 1000 次運行。

這是我的結果:

Testing vector by iterator
✓           
Maximum duration: 0.012418
Minimum duration: 0.007971
Average duration: 0.008354

Testing vector by index
✓           
Maximum duration: 0.002881
Minimum duration: 0.002094
Average duration: 0.002179

Testing set by iterator
✓           
Maximum duration: 0.021862
Minimum duration: 0.014278
Average duration: 0.014971

正如我們所看到的,通過迭代器迭代一個集合比按向量慢 1.79 倍,比按索引的向量慢 6.87 倍。

這里發生了什么? 集合不只是一個結構化的向量,它在插入時檢查每個項目是否唯一? 為什么要慢這么多?

編輯:感謝您的回復! 很好的解釋。 根據要求,這里是基准測試的代碼。

#include <chrono>
#include <random>
#include <string>
#include <functional>
#include <set>
#include <vector>

void benchmark(const char* name, int runs, int iterations, std::function<void(int)> func) {
    printf("Testing %s\n", name);

    std::chrono::duration<double> min = std::chrono::duration<double>::max();
    std::chrono::duration<double> max = std::chrono::duration<double>::min();
    std::chrono::duration<double> run = std::chrono::duration<double>::zero();
    std::chrono::duration<double> avg = std::chrono::duration<double>::zero();

    std::chrono::high_resolution_clock::time_point t1;
    std::chrono::high_resolution_clock::time_point t2;

    // [removed] progress bar code
    for (int i = 0; i < runs; ++i) {
        t1 = std::chrono::high_resolution_clock::now();

        func(iterations);

        t2 = std::chrono::high_resolution_clock::now();

        run = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);

        // [removed] progress bar code

        if (run < min) min = run;
        if (run > max) max = run;   
        avg += run / 1000.0;
    }
    // [removed] progress bar code

    printf("Maximum duration: %f\n", max.count());
    printf("Minimum duration: %f\n", min.count());
    printf("Average duration: %f\n", avg.count());

    printf("\n");
}

int main(int argc, char const *argv[]) {
    const unsigned int arrSize = 1024;

    std::vector<int> vector; vector.reserve(arrSize);
    std::set<int> set;

    for (int i = 0; i < arrSize; ++i) {
        while (1) {
            int entry = rand() - (RAND_MAX / 2);
            auto ret = set.insert(entry);
            if (ret.second) {
                vector.push_back(entry);
                break;          
            }
        }
    }

    printf("Created vector of size %lu, set of size %lu\n", vector.size(), set.size());

    benchmark("vector by iterator", 1000, 1000, [vector](int runs) -> void {
        for (int i = 0; i < runs; ++i) {
            long int sum = 0;

            for (auto it : vector) {
                sum += it;
            }
        }
    });

    benchmark("vector by index", 1000, 1000, [vector, arrSize](int runs) -> void {
        for (int i = 0; i < runs; ++i) {
            long int sum = 0;

            for (int j = 0; j < arrSize; ++j) {
                sum += vector[j];
            }
        }
    });

    benchmark("set by iterator", 1000, 1000, [set](int runs) -> void {
        for (int i = 0; i < runs; ++i) {
            long int sum = 0;

            for (auto it : set) {
                sum += it;
            }
        }
    });

    return 0;
}

我正在使用 O2 發布結果,但我試圖讓編譯器避免優化總和。

集合不只是一個結構化的向量,它在插入時檢查每個項目是否唯一?

不,到目前為止還沒有。 這些數據結構完全不同,這里的主要區別在於內存布局: std::vector將其元素放在內存中的一個連續位置,而std::set是基於節點的容器,其中每個元素都被單獨分配和駐留在內存中的不同位置,可能彼此相距很遠,並且肯定是以處理器不可能為快速遍歷預取數據的方式。 這與std::vector完全相反 - 因為下一個元素總是恰好在內存中的當前元素的“旁邊”,CPU 會將元素加載到其緩存中,並且在實際處理元素時,它只需要去到緩存以檢索值 - 與 RAM 訪問相比,這非常快。

請注意,通常需要在內存中連續布置一個排序的、唯一的數據集合,而 C++2a 或之后的版本實際上可能附帶一個flat_set ,看看P1222

Matt Austern 的“為什么你不應該使用 set(以及你應該使用什么)”也是一本有趣的讀物。

主要原因是,當您遍歷將其元素存儲在連續內存卡盤中std::vector ,您基本上會執行以下操作:

++p;

其中pT*原始指針。 stl代碼是:

 __normal_iterator&
 operator++() _GLIBCXX_NOEXCEPT
 {
    ++_M_current;                            // <--- std::vector<>: ++iter
    return *this;
 }

對於std::set ,底層對象更復雜,在大多數實現中,您迭代樹狀結構 最簡單的形式是這樣的:

p=p->next_node;

其中p是樹節點結構上的指針:

struct tree_node {
   ...
   tree_node *next_node;
};

但實際上,“真正的”stl 代碼要復雜得多:

_Self&
operator++() _GLIBCXX_NOEXCEPT
{
    _M_node = _Rb_tree_increment(_M_node);   // <--- std::set<> ++iter
    return *this;
}

// ----- underlying code \/\/\/

static _Rb_tree_node_base*
local_Rb_tree_increment(_Rb_tree_node_base* __x) throw ()
{
  if (__x->_M_right != 0) 
    {
      __x = __x->_M_right;
      while (__x->_M_left != 0)
        __x = __x->_M_left;
    }
  else 
    {
      _Rb_tree_node_base* __y = __x->_M_parent;
      while (__x == __y->_M_right) 
        {
          __x = __y;
          __y = __y->_M_parent;
        }
      if (__x->_M_right != __y)
        __x = __y;
    }
  return __x;
}

_Rb_tree_node_base*
_Rb_tree_increment(_Rb_tree_node_base* __x) throw ()
{
  return local_Rb_tree_increment(__x);
}

const _Rb_tree_node_base*
_Rb_tree_increment(const _Rb_tree_node_base* __x) throw ()
{
  return local_Rb_tree_increment(const_cast<_Rb_tree_node_base*>(__x));
}

(參見: bits/stl_tree.h 中_Rb_tree_increment 的定義是什么?

首先,您應該注意, std::set已排序。 這通常是通過將數據存儲在樹狀結構中來實現的。

向量通常存儲在可以緩存的連續內存區域(如簡單數組)中。 這就是它更快的原因。

std::vector是一個連續的結構。 元素都按順序排列在內存中,因此迭代它只需要對每個元素進行添加和單個指針查找。 此外,它對緩存非常友好,因為檢索元素通常會導致整個向量塊加載到緩存中。

std::set是一個基於節點的結構; 一般是紅黑樹。 迭代它更復雜,需要為每個元素追蹤多個指針。 它也不是很適合緩存,因為元素在內存中不一定彼此靠近。

暫無
暫無

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

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