简体   繁体   中英

Python deque compared to C++ list: insertion and deletion at a random position

The problem I'm trying to solve is to implement a queue where objects arrive with a position in that queue and they must be inserted. Objects in the beginning or in the middle of the queue (the beginning is the head) can also be deleted.

For every insertion or delete, the position of the objects later in the queue must be updated. To solve this, I'm using the index of the queue to track the position.

So I extensively have to insert and delete objects in the nth element of data structure. From what I've read so far, the most optimal way of doing this is with doubly-linked lists, because they are O(1) for such purpose.

In python, the doubly-linked list is part of the collections module and called deque; in C++, it's std::list.

Let's consider two scripts:

script.py:

from collections import deque
import random

class Event():
    def __init__(self):
        self.a = random.randint(0, 9)
        self.b = random.randint(0, 9)


structure = deque()

for i in range(0, 5000000, 1):
    event = Event()
    structure.insert(random.randint(0, 1000), event)

for i in range(0, 1000000, 1):
    del structure[random.randint(0, 1000)]

script.cpp:

#include <iostream>
#include <list>
#include <random>

class Event {
        public:
                Event() {
                        std::random_device rd;
                        std::mt19937 gen(rd());
                        std::uniform_int_distribution<> dis(0, 9);
                        a = dis(gen);
                        b = dis(gen);
                }
                int a;
                int b;
};

int main() {
        std::list<Event> structure;

        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<> dis(0, 1000);

        for (int i = 0; i < 5000000; ++i) {
                Event event;

                auto it = structure.begin();
                std::advance(it, dis(gen));
                structure.insert(it, event);
        }

        for (int i = 0; i < 1000000; ++i) {
                auto it = structure.begin();
                std::advance(it, dis(gen));


                structure.erase(it);
        }
        return 0;
}

g++ -O3 script.cpp -o cpp_impl && time./cpp_impl -> Takes 28.28 seconds time python3.11 script.py -> takes 9.43 seconds

I know that the collections module is pure C under the hood but still, I was expecting C++ to outperform.

I micro-benchmarked the C++ version a bit and most of the cost is in the std::advance method.

I know that in the iterators don't get invalidated once an object is inserted or deleted, however - unless I'm missing something - this is not of great use for the problem I'm trying to solve. I cannot create a map with the position of the iterator in the queue and the iterator itself because the position is invalidated once I insert or delete an element in a position which happens before.

How can I optimize the C++ for performance? What am I missing in my implementation?

I'm not an expert in Python, so I can't guarantee this does exactly the same thing as your Python code, but it's working code with defined behavior that's at least reasonably similar.

  • I changed from using a list to using a std::unordered_multimap to simulate a sparse array, so we can (for example) insert an element 576 into a collection that currently only contains 3 items.
  • I encapsulated the random number generation into a class, so we can generate the numbers without constantly creating new random number generation objects.

Compiled with g++, this runs in about 2.5 seconds on an old 2.4 GHz Broadwell (and probably faster on newer processor).

My uncertainty about behavior compared to Python is mostly about ordering. For example, if I have an empty container, then insert an element that says it should be in position 500, followed by one that says it should be in position 200, this will insert element 200 before element 500--but if (for example) Python just limits the upper bound, so it inserts element 500 at the end, then because there's only 1 element, inserts element 200 at the end, it'll have (what was specified as) element 200 after element 500. I think what I've done makes more sense, but I'm not sure whether it matches Python's behavior or not.

#include <iostream>
#include <random>
#include <chrono>
#include <algorithm>
#include <unordered_map>

class Rnd {
    std::minstd_rand gen;
    std::uniform_int_distribution<> dis;

public:
    Rnd(int lower = 0, int upper = 9)
        : gen(1)
        , dis(lower, upper)
    {
    }

    std::size_t operator()()
    {
        return dis(gen);
    }
};

class Event {
    Rnd rnd;
    int a;
    int b;
    bool valid = true;

public:
    Event()
        : a(rnd())
        , b(rnd())
    {
    }
    Event(void*)
        : valid(false)
    {
    }
};

int main() {
    using namespace std::chrono;

    static const int max = 1000;
    std::unordered_multimap<std::size_t, Event> structure;

    Rnd rnd(0, max-1);

    auto start = high_resolution_clock::now();
    for (int i = 0; i < 5'000'00; ++i) {
        structure.emplace(rnd(), Event());
    }

    for (int i = 0; i < 1'000'00; ++i) {
        structure.erase(rnd());
    }
    auto stop = high_resolution_clock::now();

    std::cout << "time: " << duration_cast<milliseconds>(stop - start).count() << " ms\n";
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM