【问题标题】:Why is inserting sorted keys into std::set so much faster than inserting shuffled keys?为什么将排序键插入 std::set 比插入随机键快得多?
【发布时间】:2021-02-23 11:20:52
【问题描述】:

我意外地发现将排序的键插入std::set 比插入随机键快得多。这有点违反直觉,因为作为自平衡二叉搜索树的红黑树(我验证std::set 在我的系统上实现为红黑树)需要进行大量重新平衡操作才能插入排序键的序列,因此插入排序键应该比插入随机键花费更多的时间。

但事实是,插入排序的键可以比插入随机键快 15 倍!这是我的测试代码和一些结果:

#include <algorithm>
#include <chrono>
#include <iostream>
#include <random>
#include <set>
#include <vector>
using namespace std;

int64_t insertion_time(const vector<int> &keys) {    
        auto start = chrono::system_clock::now();
        set<int>(keys.begin(), keys.end());
        auto stop = chrono::system_clock::now();
        auto elapsed = chrono::duration_cast<chrono::milliseconds>(stop - start);
        return elapsed.count(); 
}

int main() {
    size_t test_size;
    cout << "test size: ";
    cin >> test_size;
    vector<int> keys(test_size);
    for (int i = 0; i < test_size; ++i) {
        keys[i] = i;
    }
    
    // whether shuffled case or sorted case took first was irrelevant and results were similar
    auto rng = std::default_random_engine {};
    shuffle(keys.begin(), keys.end(), rng);
    cout << "shuffled: " << insertion_time(keys) << endl;

    sort(keys.begin(), keys.end());
    cout << "sorted: " << insertion_time(keys) << endl;

    return 0;
}
// i7 8700, 32 GB RAM, WIN10 2004, g++ -O3 main.cpp
// An interesting observation is that the difference becomes larger as test_size being larger.
// Similar results showed up for my handwritten red-black tree and other
// machines( or other compilers, operating systems etc)

C:\Users\Leon\Desktop\testSetInsertion>a
test size: 1000000
shuffled: 585
sorted: 96

C:\Users\Leon\Desktop\testSetInsertion>a
test size: 3000000
shuffled: 2480
sorted: 296

C:\Users\Leon\Desktop\testSetInsertion>a
test size: 5000000
shuffled: 4805
sorted: 484

C:\Users\Leon\Desktop\testSetInsertion>a
test size: 10000000
shuffled: 11537
sorted: 977

C:\Users\Leon\Desktop\testSetInsertion>a
test size: 30000000
shuffled: 46239
sorted: 3076

有人解释一下吗?我想这与 cache locality 有关,因为在插入排序键时,重新平衡通常涉及最近插入的那些节点。但以上只是我的猜测,我对缓存局部性知之甚少。

【问题讨论】:

    标签: c++ stl red-black-tree stdset cache-locality


    【解决方案1】:

    如果你看https://en.cppreference.com/w/cpp/container/set/set

    你可以看到:

    复杂性
    [..]
    2) N log(N) 其中N = std::distance(first, last) 通常是N 如果范围已按value_comp() 排序,则为线性。

    我们可以在循环中使用insertend() 作为提示,它是具有正确提示的摊销常数。

    【讨论】:

    • 但我仍然不明白为什么会这样,以及它与hint 有什么关系。 set 的 ctor 对键的排序一无所知。
    • 如果已排序,则只需进行一次检查即可找到新元素的位置(最后位置是否正常)。出错时,进行二分搜索(所以log(N))来查找位置。
    • @LeonCruz 与实际排序 (O(N log(N)) 相比,检查序列是否已排序非常便宜 (O(N))
    • @Jarod42 但是我手写的红黑树的结果是相似的,它在插入键时总是从根开始搜索。另外,我刚刚发现 unordered_set 给出了类似的结果,尽管差异较小并且 int 类型可能在这种情况下发挥重要作用
    • @largest_prime_is_463035818 但它与检查排序有什么关系?
    猜你喜欢
    • 1970-01-01
    • 2014-03-30
    • 2011-01-27
    • 2011-12-31
    • 2023-02-05
    • 2016-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多