为什么迭代器调试会导致 std::unordered_map 在调试构建中变慢 200 倍?

4
我知道代码会变慢,但为什么会这么慢?我该如何编写代码以避免这种减速?
std::unordered_map在内部使用其他容器,并且这些容器使用迭代器。在构建调试时,默认情况下_ITERATOR_DEBUG_LEVEL=2。这会打开迭代器调试。有时我的代码受影响不大,有时它运行得非常慢。
我可以通过在项目属性>> C++ >> 预处理器 >> 预处理器定义中设置_ITERATOR_DEBUG_LEVEL=0来加快示例的速度。但是,正如此链接所建议的那样,我不能在我的真实项目中这样做。在我的情况下,我与包含_ITERATOR_DEBUG_LEVEL=2的std::basic_string的MSVCMRTD.lib发生冲突。我知道我可以通过静态链接到CRT来解决问题。但如果我能修复代码以避免出现问题,我更愿意这样做。
我可以做出改进情况的更改。但我只是尝试着做事而不了解为什么它们有效。例如,目前为止,前1000次插入以全速运行。但如果我将O_BYTE_SIZE更改为1,则第一次插入与其他所有操作一样慢。这看起来是一个小变化(不一定是好的变化)。 这里, 这里, 和 这里 也有所启示,但没有回答我的问题。
我正在使用 Visual Studio 2010 (这是传统代码)。我创建了一个 Win32 控制台应用程序并添加了这段代码。

Main.cpp

#include "stdafx.h"


#include "OString.h"
#include "OTHashMap.h"

#include <cstdio>
#include <ctime>
#include <iostream>

// Hash and equal operators for map
class CRhashKey {
public:
   inline unsigned long operator() (const OString* a) const { return a->hash(); }
};

class CReqKey {
public:
    inline bool operator() (const OString& x, const OString& y) const { return strcmp(x.data(),y.data()) != 0; }
    inline bool operator() (const OString* x, const OString& y) const { return operator()(*x,y); }
    inline bool operator() (const OString& x, const OString* y) const { return operator()(x,*y); }
    inline bool operator() (const OString* x, const OString* y) const { return operator()(*x,*y); }
};


int _tmain(int argc, _TCHAR* argv[])
{
    const int CR_SIZE = 1020007;

    CRhashKey h;
    OTPtrHashMap2<OString, int, CRhashKey, CReqKey> *code_map = 
        new OTPtrHashMap2 <OString, int, CRhashKey, CReqKey>(h, CR_SIZE);

    const clock_t begin_time = clock();

    for (int i=1; i<=1000000; ++i)
    {
        char key[10];
        sprintf(key, "%d", i);

        code_map->insert(new OString(key), new int(i));

        //// Check hash values
        //OString key2(key);
        //std::cout << i << "\t" << key2.hash() << std::endl;

        // Check timing
        if ((i % 100) == 0)
        {
            std::cout << i << "\t" << float(clock() - begin_time) / CLOCKS_PER_SEC << std::endl;
        }
    }

    std::cout << "Press enter to exit" << std::endl;
    char buf[256];
    std::cin.getline(buf, 256);

    return 0;
}

OTHashMap.h

#pragma once

#include <fstream>
#include <unordered_map>    

template <class K, class T, class H, class EQ>
class OTPtrHashMap2
{
    typedef typename std::unordered_map<K*,T*,H,EQ>                     OTPTRHASHMAP_INTERNAL_CONTAINER;
    typedef typename OTPTRHASHMAP_INTERNAL_CONTAINER::iterator          OTPTRHASHMAP_INTERNAL_ITERATOR;

public:
    OTPtrHashMap2(const H& h, size_t defaultCapacity) : _hashMap(defaultCapacity, h) {}

    bool insert(K* key, T* val)
    {
        std::pair<OTPTRHASHMAP_INTERNAL_ITERATOR,T> retVal = _hashMap.insert(std::make_pair<K*,T*>(key, val));
        return retVal.second != NULL;
    }

    OTPTRHASHMAP_INTERNAL_CONTAINER _hashMap;

private:
};

OString.h

#pragma once

#include <string>

class OString
{
public:
    OString(const std::string& s) : _string (s) { } 
    ~OString(void) {}

    static unsigned hash(const OString& s) { return unsigned (s.hash()); }
    unsigned long hash() const
    {
        unsigned hv = static_cast<unsigned>(length());
        size_t i = length() * sizeof(char) / sizeof(unsigned);
        const char * p = data();
        while (i--) {
            unsigned tmp;
            memcpy(&tmp, p, sizeof(unsigned));
            hashmash(hv, tmp);
            p = p + sizeof(unsigned);
        } 
        if ((i = length() * sizeof(char) % sizeof(unsigned)) != 0)  {
            unsigned h = 0;
            const char* c = reinterpret_cast<const char*>(p);
            while (i--)
            {
                h = ((h << O_BYTE_SIZE*sizeof(char)) | *c++);
            }
            hashmash(hv, h);
        }
        return hv; 
    }

    const char* data() const { return _string.c_str(); }
    size_t length() const    { return _string.length(); }


private:
    std::string _string;

    //static const unsigned O_BYTE_SIZE = 1;
    static const unsigned O_BYTE_SIZE = 8;
    static const unsigned O_CHASH_SHIFT = 5;

    inline void hashmash(unsigned& hash, unsigned chars) const
    {
        hash = (chars ^
                ((hash << O_CHASH_SHIFT) |
                 (hash >> (O_BYTE_SIZE*sizeof(unsigned) - O_CHASH_SHIFT))));
    }
};

我自己在调试版本和发布版本之间看到了100倍的速度下降。不一定是你指出的特定情况。 - drescherjm
1
如何编写代码以避免这种减速?为什么需要这样做?代码在生产中将始终以发布模式编译,因此用户永远不会看到这种减速。 - Algirdas Preidžius
1
发布版本,调试符号,通过编译指示选择性地关闭全局优化。 - Retired Ninja
1
你能否将那个有点恶心的部分打包成一个模块,使用优化编译,并为那些需要深入了解其内部工作的人提供“此处有龙!”的警告? - user4581301
@RetiredNinja - 谢谢。有帮助。 - mmesser314
显示剩余4条评论
1个回答

3
我找到了足够的答案。碰撞是减速的来源。 编辑2:--另一个解决方法是在main.cpp中的#include周围添加以下内容--
// Iterator debug checking makes the Microsoft implementation of std containers 
// *very* slow in debug builds for large containers. It must only be undefed around 
// STL includes. Otherwise we get linker errors from the debug C runtime library, 
// which was built with _ITERATOR_DEBUG_LEVEL set to 2. 
#ifdef _DEBUG
#undef _ITERATOR_DEBUG_LEVEL
#endif

#include <unordered_map>

#ifdef _DEBUG
#define _ITERATOR_DEBUG_LEVEL 2
#endif

编辑: -- 解决方法是切换到boost::unordered_map。--

std::unordered_map定义在<unordered_map>中。它继承自_Hash,_Hash定义在<xhash>中。

_Hash包含以下内容(高度简略)

template<...> 
class _Hash
{
    typedef list<typename _Traits::value_type, ...> _Mylist;
    typedef vector<iterator, ... > _Myvec;

    _Mylist _List;  // list of elements, must initialize before _Vec
    _Myvec _Vec;    // vector of list iterators, begin() then end()-1
};

所有的值都存储在_List中。
_Vec是指向_List迭代器的向量。它将_List分成桶。_Vec有每个桶开头和结尾的迭代器。因此,如果map有1M个桶(不同的键哈希),_Vec有2M个迭代器。
当将键/值对插入到map中时,通常会创建一个新的桶。该值被推送到列表的开头。键的哈希是_Vec中放置两个新迭代器的位置。这很快,因为它们指向列表的开头。
如果桶已经存在,则必须在_List中现有值的旁边插入新值。这需要在列表中插入一个项目。必须更新现有迭代器。显然,当启用迭代器调试时,这需要大量工作。代码在中,但我没有逐步执行它。
为了了解需要多少工作,我使用了一些无意义的哈希函数,这些哈希函数使用起来很糟糕,但在插入时会产生许多冲突或很少的冲突。
添加到OString.h
static unsigned hv2;

// Never collides. Always uses the next int as the hash
unsigned long hash2() const
{
    return ++hv2;
}

// Almost never collides. Almost always gets the next int. 
// Gets the same int 1 in 200 times. 
unsigned long hash3() const
{
    ++hv2;
    unsigned long lv = (hv2*200UL)/201UL;
    return (unsigned)lv;
}

// A best practice hash
unsigned long hash4() const
{
    std::hash<std::string> hasher;
    return hasher(_string);
}

// Always collides. Everything into bucket 0. 
unsigned long hash5() const
{
    return 0;
}

添加到main.cpp中

// Hash and equal operators for map
class CRhashKey {
public:
   //inline unsigned long operator() (const OString* a) const { return a->hash(); }
   //inline unsigned long operator() (const OString* a) const { return a->hash2(); }
   //inline unsigned long operator() (const OString* a) const { return a->hash3(); }
   //inline unsigned long operator() (const OString* a) const { return a->hash4(); }
   inline unsigned long operator() (const OString* a) const { return a->hash5(); }
};

unsigned OString::hv2 = 0;

结果非常显著。没有任何实际可行的哈希算法可以工作。

  • hash2 - 几乎不会发生冲突 - 15.3秒内插入1百万次
  • hash3 - 几乎不会发生冲突 - 206秒内插入1百万次
  • hash4 - 最佳实践 - 132秒内插入100k次,但随着冲突越来越频繁,速度变慢。插入1百万次需要超过1小时
  • hash5 - 总是会发生冲突 - 48秒内插入1千次,或者插入1百万次需要约13个小时

我的选择:

  • 像Retired Ninja建议的那样,发布版本,关闭调试符号和优化
  • 静态链接到MSVCMRTD,这样我就可以关闭_ITERATOR_DEBUG_LEVEL,并解决其他类似的问题。
  • 从unordered_map更改为排序向量。
  • 其他建议欢迎。

1
改用std::map怎么样?后者不使用哈希,因此不会受到哈希冲突的影响,但在其他方面与std::unordered_map的行为相似(主要区别是它使用键类型的<运算符将其条目保持在排序树中)。 - Jeremy Friesner
@JeremyFriesner - 谢谢。好想法。 - mmesser314

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接