Почему отладка итератора медленная std :: unordered_map 200x в отладочных сборках? - PullRequest
3 голосов
/ 10 июля 2019

Я понимаю, что код будет медленнее, но почему так много? Как мне написать код, чтобы избежать этого замедления?

std :: unordered_map использует другие контейнеры внутри, и эти контейнеры используют итераторы. При встроенной отладке _ITERATOR_DEBUG_LEVEL = 2 по умолчанию. Это включает отладку итератора . Иногда мой код не сильно подвержен влиянию, а иногда он работает очень медленно.

Я могу ускорить мой пример, установив _ITERATOR_DEBUG_LEVEL = 0 в свойствах моего проекта >> C ++ >> Препроцессор >> Определения препроцессора. Но, как подсказывает эта ссылка , я не могу сделать это в моем реальном проекте. В моем случае я получаю конфликты с MSVCMRTD.lib, который содержит std :: basic_string, созданный с _ITERATOR_DEBUG_LEVEL = 2. Я понимаю, что могу обойти проблему, статически связываясь с ЭЛТ. Но я бы предпочел не делать этого, если смогу исправить код, чтобы проблема не возникала.

Я могу внести изменения, которые улучшат ситуацию. Но я просто пробую вещи, не понимая, почему они работают. Например, как есть, первые 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))));
    }
};

1 Ответ

2 голосов
/ 10 июля 2019

Я нашел достаточно ответа.Столкновения являются источником замедления.

Редактировать : - Исправлено переключение на boost :: unordered_map.-

std :: unordered_map определено в .Он наследуется от _Hash, определенного в .

_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 имеет итератор для начала и конца каждого сегмента.Таким образом, если карта содержит 1M сегментов (различные ключевые хэши), _Vec имеет 2M итераторов.

Когда пара ключ / значение вставляется в карту, обычно создается новый сегмент.Значение помещается в начало списка.Хеш ключа - это местоположение в _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 - Никогда не сталкиваться - 1M вставок за 15,3 с
  • hash3 - Почти никогда - 1M вставок за 206 с
  • hash4 - Лучшая практика - 100K вставок за 132сек, и становится медленнее, поскольку столкновения стали более частыми.Вставки 1M заняли бы> 1 час
  • hash5 - Всегда сталкиваться - вставки 1k за 48 секунд или вставки 1M за ~ 13 часов

Мой выбор

  • Выпуск сборки, отладка символов, оптимизация отключена, как предлагает отставной ниндзя
  • Статически ссылка на MSVCMRTD, чтобы я мог отключить _ITERATOR_DEBUG_LEVEL.Также решите некоторые другие подобные проблемы.
  • Измените unordered_map на отсортированный вектор.
  • Что-то еще.Предложения приветствуются.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...