![](/img/trans.png)
[英]Why is iterating an std::array much faster than iterating an 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;
其中p
是T*
原始指針。 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));
}
首先,您應該注意, std::set
已排序。 這通常是通過將數據存儲在樹狀結構中來實現的。
向量通常存儲在可以緩存的連續內存區域(如簡單數組)中。 這就是它更快的原因。
std::vector
是一個連續的結構。 元素都按順序排列在內存中,因此迭代它只需要對每個元素進行添加和單個指針查找。 此外,它對緩存非常友好,因為檢索元素通常會導致整個向量塊加載到緩存中。
std::set
是一個基於節點的結構; 一般是紅黑樹。 迭代它更復雜,需要為每個元素追蹤多個指針。 它也不是很適合緩存,因為元素在內存中不一定彼此靠近。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.