【问题标题】:C++ STL: most efficient way to erase the last / first n elements of a std::dequeC ++ STL:擦除std :: deque的最后/前n个元素的最有效方法
【发布时间】:2021-07-17 22:27:43
【问题描述】:

我正在编写一个 C++ 程序,我必须在其中维护一个偏移列表(在数组或文件中),为此我使用了 std::deque<size_t>。该列表在读取文件时被填满,其中包含特定字符的最后一次出现的位置 - 比如说“a”。因此,在读取文件时,每次出现字符“a”时,我都会将其位置推到队列的末尾。

现在在某些时候,我必须删除此列表中小于给定最大大小的所有条目(或者换句话说:距离当前读取位置超过 x 个字节)。由于队列已排序(按创建方式),最简单的方法可能是在值较小时将元素从队列中弹出:

void remove_smaller_than(size_t minimum)
{
    size_t element = my_queue.first(); // yes I know, should check if empty here too
    while (element < minimum && !my_queue.empty())
    {
        my_queue.pop_front();
        element = my_queue.first();
    }

}

现在的问题是,每次我必须这样做时,我都必须从另一端迭代队列(出于另一个原因),在 第一个元素的位置结束太远了。只是为了澄清(因为这很难解释),这里又是一个简短的 sn-p:

void iterate_over_matches(size_t minimum)
{
    for (auto it = my_queue.rbegin(), end = my_queue.rend(); it != end && *it > minimum; ++it)
        do_something(*it);
}

(有点像,这只是为了演示)。

所以我现在的想法是,如果我已经有了 last valid(而不是 first invalid)条目的迭代器,是否有更有效的方法来“剪辑”这个位置的双端队列而不是弹出所有条目?

TL;DR:

如果我有一个给定的 std::deque 迭代器,有没有比在 while 循环中弹出它们更有效的方法来擦除从该迭代器开始直到结束的所有元素?

【问题讨论】:

  • 听起来很像erase's job
  • 旁注:在迭代容器时从容器中删除项目是错误的主要来源。使用容器内置的功能或erase-remove idiom 来完成这项工作。
  • 是的,我想这将是我能得到的最好的......

标签: c++ performance stl std deque


【解决方案1】:

因为这是一个包含离散元素的容器,所以对这些元素的任何移除都​​将涉及对它们进行迭代。甚至标准库中用于擦除元素的函数也确实在内部使用了循环。

如果要删除的元素数量是恒定的(例如,如果您知道总是要删除 5 个元素),那么您可以省略循环,只编写 5 条单独的指令,这样效率会更高一点(您将避免循环生成的条件跳转指令)。

【讨论】:

  • 由于deque 是一个块列表,它可以一次删除整个块,而不是遍历每个元素。是否这样做,我不能说。任何具有非平凡破坏的东西肯定需要完成所有的破坏工作,但是一个整数块......噗。走了。
  • 这正是我最初有这个想法的原因。但我想在这种情况下,deque 模板必须专门用于简单数据类型,仅出于这个目的 - 可能不是这样。
  • 使用连续数组(C 风格的数组),你一次只能delete 整个东西。您可以实现自己的内存管理器,只允许删除分配的内存块的一部分(仅在末尾),但 std::queue 不是连续的内存块,而是单独的元素。并且逻辑上独立的元素(即使一个接一个地存储在内存中)是单独分配的,这意味着每个元素都有单独的分配信息(地址和大小)。即使您想一次删除 n 个这样的元素,您也必须首先迭代(循环!)它们的大小。
【解决方案2】:

如果我有一个给定的 std::deque 迭代器,有没有更高效的 擦除从该迭代器开始直到结束的所有元素的方法 在 while 循环中弹出它们?

是的——使用带有两个迭代器参数的覆盖调用erase member function of std::deque。第一个是你的“给定迭代器”,第二个是你的双端队列对象的 end() 迭代器。

这是一个基于链接的 cppreference 页面中给出的示例:

#include <deque>
#include <iostream>

void print_container(const std::deque<int>& c)
{
    for (auto& i : c) {
        std::cout << i << " ";
    }
    std::cout << '\n';
}

int main()
{
    std::deque<int> c{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    print_container(c);
    auto it = c.begin();
    while (*it != 5) ++it;
    c.erase(it, c.end()); // Erase from a given iterator to the end of the container
//  c.erase(c.begin(), it); // Or to erase the other part
    print_container(c);
    return 0;
}

STL 实现是否比通过容器迭代自己“更有效”将取决于几个因素(如所包含对象的性质),但快速查看 MSVC/clang-cl 实现表明STL 可以使用对std::move() 的调用来实现这一点(至少对于原始包含类型)。

【讨论】:

    【解决方案3】:

    虽然从std::deque 中删除元素的最快方法确实是erase 方法,但std::deque 似乎不是适合您情况的理想数据结构。由于您的元素已排序(它们不断增加),您可以在O(log(n)) 时间内找到需要擦除的索引。不幸的是,std::deque::erase 的复杂度为 O(n),因此快速搜索的收益完全被削弱了。我们可以做得更好。

    由于您存储在数据结构中的元素不需要被破坏(因为它们只是整数),因此可以创建一个在恒定时间内擦除这些元素的数据结构。例如,这里有一个环形缓冲区实现:

    template <typename T>
    class RingBuffer
    {
    public:
        static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable!");
        static_assert(std::is_trivially_destructible_v<T>, "T must be trivially destructable!");
    
        RingBuffer() :
            m_Items(nullptr),
            m_Capacity(0),
            m_StartIndex(0),
            m_EndIndex(0),
            m_Version(0)
        {
        }
    
        RingBuffer(const RingBuffer&) = delete;
        RingBuffer& operator=(const RingBuffer&) = delete;
    
        ~RingBuffer()
        {
            delete[] m_Items;
        }
    
        void Add(T item)
        {
            m_Version++;
    
            if (m_EndIndex + 1 == m_StartIndex || (m_EndIndex >= m_StartIndex && m_EndIndex >= m_Capacity && m_StartIndex == 0))
                Grow();
    
            size_t elementIndex;
            if (m_EndIndex >= m_Capacity)
            {
                elementIndex = 0;
                m_EndIndex = 1;
            }
            else
            {
                elementIndex = m_EndIndex++;
            }
    
            m_Items[elementIndex] = std::move(item);
        }
    
        void EraseFirstN(size_t eraseCount)
        {
            m_Version++;
    
            auto count = size();
            assert(eraseCount <= count);
    
            if (eraseCount == count)
            {
                m_StartIndex = m_EndIndex = 0;
                return;
            }
    
            m_StartIndex += eraseCount;
            if (m_StartIndex >= m_Capacity)
                m_StartIndex -= m_Capacity;
        }
    
        size_t size() const
        {
            if (m_EndIndex >= m_StartIndex)
                return m_EndIndex - m_StartIndex;
    
            return m_Capacity - m_StartIndex + m_EndIndex;
        }
    
        struct Iterator : std::iterator<std::random_access_iterator_tag, T>
        {
            Iterator() :
                m_Container(nullptr),
                m_Index(0),
                m_Version(0)
            {
            }
    
            Iterator(RingBuffer<T>* container, size_t index, size_t version) :
                m_Container(container),
                m_Index(index),
                m_Version(version)
            {
            }
    
            Iterator& operator+=(ptrdiff_t value) { m_Index += value; return *this; }
            Iterator& operator-=(ptrdiff_t value) { m_Index -= value; return *this; }
    
            T& operator*() { assert(m_Version == m_Container->m_Version); return (*m_Container)[m_Index]; }
            const T& operator*() const { assert(m_Version == m_Container->m_Version); return (*m_Container)[m_Index]; }
    
            T* operator->() { assert(m_Version == m_Container->m_Version); return &(*m_Container)[m_Index]; }
            const T* operator->() const { assert(m_Version == m_Container->m_Version); return &(*m_Container)[m_Index]; }
    
            T& operator[](const int& index) { assert(m_Version == m_Container->m_Version); return (*m_Container)[m_Index + index]; }
            const T& operator[](const int& index) const { assert(m_Version == m_Container->m_Version); return (*m_Container)[m_Index + index]; }
    
            Iterator& operator++() { m_Index++; return *this; }
            Iterator& operator--() { m_Index--; return *this; }
            Iterator operator++(int) { return Iterator(m_Container, m_Index++, m_Version); }
            Iterator operator--(int) { return Iterator(m_Container, m_Index--, m_Version); }
            ptrdiff_t operator-(const Iterator& other) const { assert(m_Container == other.m_Container && m_Version == other.m_Version); return static_cast<ptrdiff_t>(m_Index - other.m_Index); }
            Iterator operator+(const ptrdiff_t& index) const { return Iterator(m_Container, m_Index + index, m_Version); }
            Iterator operator-(const ptrdiff_t& index) const { return Iterator(m_Container, m_Index - index, m_Version); }
            friend Iterator operator+(const ptrdiff_t& index, const Iterator& other) { return other + index; }
            friend Iterator operator-(const ptrdiff_t& index, const Iterator& other) { return Iterator(other.m_Container, index - other.m_Index, other.m_Version); }
    
            bool operator==(const Iterator& other) const { assert(m_Container == other.m_Container && m_Version == other.m_Version); return m_Index == other,m_Index; }
            bool operator!=(const Iterator& other) const { assert(m_Container == other.m_Container && m_Version == other.m_Version); return m_Index != other.m_Index; }
            bool operator>(const Iterator& other) const { assert(m_Container == other.m_Container && m_Version == other.m_Version); return m_Index > other.m_Index; }
            bool operator<(const Iterator& other) const { assert(m_Container == other.m_Container && m_Version == other.m_Version); return m_Index < other.m_Index; }
            bool operator>=(const Iterator& other) const { assert(m_Container == other.m_Container && m_Version == other.m_Version); return m_Index >= other.m_Index; }
            bool operator<=(const Iterator& other) const { assert(m_Container == other.m_Container && m_Version == other.m_Version); return m_Index <= other.m_Index; }
    
        private:
            RingBuffer<T>* m_Container;
            size_t m_Index;
            size_t m_Version;
        };
    
        Iterator begin()
        {
            return Iterator(this, 0, m_Version);
        }
    
        Iterator end()
        {
            return Iterator(this, size(), m_Version);
        }
    
        T& operator[](size_t index)
        {
            assert(index < size());
    
            index += m_StartIndex;
            if (index >= m_Capacity)
                index -= m_Capacity;
    
            return m_Items[index];
        }
    
    private:
        void Grow()
        {
            auto newCapacity = std::max(m_Capacity * 2, m_Capacity + 4);
            auto newItems = new T[newCapacity];
    
            if (m_EndIndex >= m_StartIndex)
            {
                memcpy(newItems, m_Items + m_StartIndex, (m_EndIndex - m_StartIndex) * sizeof(T));
                m_EndIndex = m_EndIndex - m_StartIndex;
            }
            else
            {
                memcpy(newItems, m_Items + m_StartIndex, (m_Capacity - m_StartIndex) * sizeof(T));
                memcpy(newItems + m_Capacity - m_StartIndex, m_Items, m_EndIndex * sizeof(T));
    
                m_EndIndex = m_Capacity - m_StartIndex + m_EndIndex;
            }
    
            delete[] m_Items;
    
            m_StartIndex = 0;
            m_Items = newItems;
            m_Capacity = newCapacity;
    
        }
    
    private:
        T* m_Items;
        size_t m_Capacity;
        size_t m_StartIndex;
        size_t m_EndIndex;
        size_t m_Version;
    };
    

    从这个数据结构中擦除比使用std::deque 快得多。这是我使用的基准测试代码:

    constexpr size_t kCount = 100 * 1000 * 1000;
    
    size_t everIncreasingIndex = 0;
    size_t elementToEraseUpTo = 5674112;
    
    std::deque<size_t> deque;
    RingBuffer<size_t> ringBuffer;
    
    std::cout.setf(std::ios::fixed);
    std::cout.precision(5);
    
    for (size_t u = 0; u < 10; u++)
    {
        for (size_t i = 0; i < kCount; i++)
        {
            deque.push_back(everIncreasingIndex);
            ringBuffer.Add(everIncreasingIndex);
    
            everIncreasingIndex++;
        }
    
        {
            Stopwatch stopwatch;
    
            auto it = std::lower_bound(deque.begin(), deque.end(), elementToEraseUpTo);
            deque.erase(deque.begin(), it);
    
            auto elapsed = stopwatch.ElapsedSeconds();
            std::cout << "std::deque took " << elapsed * 1000 << " ms." << std::endl;
        }
    
        {
            Stopwatch stopwatch;
    
            auto it = std::lower_bound(ringBuffer.begin(), ringBuffer.end(), elementToEraseUpTo);
            ringBuffer.EraseFirstN(it - ringBuffer.begin());
    
            auto elapsed = stopwatch.ElapsedSeconds();
            std::cout << "Ring buffer took " << elapsed * 1000 << " ms." << std::endl;
        }
    
        elementToEraseUpTo *= 2;
    }
    

    在我的机器上,结果如下:

    std::deque took 6.43080 ms.
    Ring buffer took 0.00220 ms.
    std::deque took 6.40030 ms.
    Ring buffer took 0.00170 ms.
    std::deque took 12.83000 ms.
    Ring buffer took 0.00160 ms.
    std::deque took 25.61430 ms.
    Ring buffer took 0.00240 ms.
    std::deque took 51.16480 ms.
    Ring buffer took 0.00280 ms.
    std::deque took 102.56710 ms.
    Ring buffer took 0.00270 ms.
    std::deque took 205.54640 ms.
    Ring buffer took 0.00250 ms.
    std::deque took 411.39580 ms.
    Ring buffer took 0.00250 ms.
    std::deque took 170.58990 ms.
    Ring buffer took 0.00300 ms.
    std::deque took 98.51280 ms.
    Ring buffer took 0.00230 ms.
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-02-11
      • 1970-01-01
      • 2013-09-12
      • 1970-01-01
      • 2011-05-06
      • 2014-01-26
      • 2011-09-11
      • 2012-02-18
      相关资源
      最近更新 更多