std::unordered_map::clear() 做啥?

Posted

技术标签:

【中文标题】std::unordered_map::clear() 做啥?【英文标题】:What does std::unordered_map::clear() do?std::unordered_map::clear() 做什么? 【发布时间】:2020-09-02 17:04:02 【问题描述】:

我有一段简单的代码:

#pragma GCC optimize ("O0")
#include <unordered_map>
int main()

    std::unordered_map<int, int> map;
        
    static constexpr const int MaxN = 2e6 + 69;
    map.reserve(MaxN);
    
    int t = 1000;
    while (t--)
    
        map.clear();
    
    
    return 0;

这段代码所做的只是创建一个巨大的std::unordered_map,在堆上保留大量内存,同时保持它为空,并清除它1000次。令我惊讶的是,执行这个程序需要一秒钟多的时间。

根据cppreference,std::unordered_map::clear元素数中是线性的,即0,而不是桶数。因此,这个函数在我的程序中应该什么都不做,并且应该花费不到一毫秒。

为了进一步分析代码,我写了这个:

#pragma GCC optimize ("O0")
#include <chrono>
#include <iostream>
#include <unordered_map>

#include <map>
template <typename T>
struct verbose_pointer

    using element_type = T;
    T* value = nullptr;
    static std::map<T*, std::size_t> accessTimes;   
//  T & operator[](std::size_t n)
//  
//      ++(*count);
//      return value[n];
//  
    T * operator ->() const
    
        ++accessTimes[value];
        return value;
    
//  T & operator *() const
//  
//      ++(*count);
//      return *value;
//  
    static void operator delete(void * ptr)
    
        T * toErase = (static_cast<verbose_pointer *>(ptr))->value;
        std::cerr << "Deleted " << toErase << std::endl;
        std::cerr << "Address " << toErase << " accessed " << accessTimes[toErase] << " times." << std::endl;
        accessTimes.erase(toErase);
        ::operator delete(toErase);
    
    verbose_pointer(void* ptr) : value(static_cast <T*>(ptr)) 
    
        std::cerr << "I'm constructed from pointer: " << ptr << std::endl;
    
    
    static verbose_pointer pointer_to(T & t)  return verbose_pointer(&t); 
    ~verbose_pointer()
    
    
;
template <typename T>
std::map<T*, std::size_t> verbose_pointer<T>::accessTimes;
template <typename T>
class verbose_allocator

    public:
        using value_type = T;
        using pointer = verbose_pointer<T>;
        constexpr verbose_allocator() noexcept = default;
        constexpr verbose_allocator(const verbose_allocator & other) noexcept = default;
        template <typename U>
        constexpr verbose_allocator(const verbose_allocator<U> & other) noexcept 
        pointer allocate(std::size_t n)
        
            std::cout << (n * sizeof(T)) << " bytes allocated." << std::endl;
            return static_cast<pointer>(::operator new(n * sizeof(T)));
        
        void deallocate(pointer p, std::size_t n)
        
            std::cout << (n * sizeof(T)) << " bytes deallocated." << std::endl;
            pointer::operator delete(&p);
        
;
int main()

    std::unordered_map<int, int, std::hash<int>, std::equal_to<int>, verbose_allocator<std::pair<const int, int>>>
        verbose_map;
        
    static constexpr const int MaxN = 2e6 + 69;
    verbose_map.reserve(MaxN);
    
    auto start = std::chrono::high_resolution_clock::now(); 
        
    int t = 1000;
    while (t--)
    
        verbose_map.clear();
    
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "1000 clear() runs take " << duration.count() << " milliseconds." << std::endl;
    
    return 0;

我的代码的输出是:

8579908 bytes allocated.
I'm constructed from pointer: 0xd09020
1000 clear() runs take 1139 milliseconds.
I'm constructed from pointer: 0xd09020
8579908 bytes deallocated.
Deleted 0xd09020
Address 0xd09020 accessed 1 times.

似乎在reserve() 语句中分配了一次巨大的内存块,并在映射超出范围时释放了一次,就像我预期的那样。而且,指针只被访问一次。

那么为什么 1000 次 std::unordered_map::clear() 操作需要这么多时间呢? GCC 的实现在这里做了什么?

【问题讨论】:

请说明您如何测量各种操作所花费的时间——我在显示的代码中看不到任何内容。另请注意,衡量未优化代码的性能在很大程度上是没有意义的。删除#pragma GCC optimize ("O0") 指令。 我想reserve 可能会占用大部分时间。 你试过调试吗?另见gcc.gnu.org/bugzilla/show_bug.cgi?id=67922 在禁用优化的情况下,您无法可靠地分析任何内容... 您是在测试优化的构建还是未优化的调试构建?未优化的构建(大多数编译器的默认值)只关心生成易于调试的代码,而性能是次要的,调试构建非常缓慢,并且执行许多优化器通常只是删除的事情。如果您想知道您的代码将/可以在现实世界中如何执行,请始终测试优化的构建。为未优化的调试构建计时是浪费时间。 "#pragma GCC optimize ("O0")" 似乎指向了一种奇怪的、非标准的方式来确保非最优的未优化构建。 【参考方案1】:

reserve(N)对于无序关联容器的定义是它分配足够的桶,使得如果容器中有N元素,表的负载因子将小于或等于最大负载因子。最大负载因子的默认值为 1,因此reserve 必须至少分配 2,000,069 个桶。

确实clear()被指定为线性时间,复杂度要求是根据容器中元素的数量。但更准确地说:复杂性要求指定容器元素上的操作数。例如,如果一个容器包含 1 个元素,那么当调用 clear() 时,容器可以对该元素执行的操作数必须有一个恒定的上限。但是除了这些操作之外,容器可以在“记账”上花费多少时间是没有限制的。因此,很有可能clear() 用于基于哈希的容器可能会在不违反标准的情况下花费与桶数呈线性关系的额外时间。

我查看了libstdc++ implementation of clear()。它遍历所有元素并销毁它们,然后使用memset 将所有存储桶指针重置为空。所以实际上它总是花费额外的时间与桶的数量成线性关系,即使这是“不必要的”,因为首先没有元素。因此,您的程序的 1000 次 clear() 迭代将通过这样的实现执行至少 2,000,069,000 次操作(假设它需要一个操作将一个指针大小的内存位置归零)。

【讨论】:

以上是关于std::unordered_map::clear() 做啥?的主要内容,如果未能解决你的问题,请参考以下文章