Why is inserting sorted keys into std::set so much faster than inserting shuffled keys?

85 views Asked by At

I was accidentally surprised to found that inserting sorted keys into std::set is much much faster than inserting shuffled keys. This is somewhat counterintuitive since a red-black tree (I verified that std::set is implemented as a red-black tree on my system) as a self-balanced binary search tree, would need to do a lot of rebalancing opeartions to insert a sequence of sorted keys, thus inserting sorted keys should take more time than inserting shuffled keys.

But the fact is, inserting sorted keys can be up to 15 times faster than inserting shuffled keys! Here is my testing code and some results:

#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

Anyone explains this please? I guess that this has something to do with cache locality since when inserting sorted keys, rebalancing typically involves those nodes that were most recently inserted. But above is just my guess and I know very little about cache locality.

1

There are 1 answers

8
Jarod42 On

If you look at https://en.cppreference.com/w/cpp/container/set/set

you can see:

Complexity
[..]
2) N log(N) where N = std::distance(first, last) in general, linear in N if the range is already sorted by value_comp().

We can use insert in loop with end() as hint which is amortized constant with correct hint.