Подтвердить что ты не робот

С++ ~ 1M look-ups в unordered_map со строковым ключом работает намного медленнее, чем .NET-код

У меня есть .NET и С++ реализации перфекционной тестовой функции, которая делает 854,750 поисков в словаре, используя строковые ключи из пула из 6838 ключей. Я написал эти функции, чтобы исследовать "узкое место" в реальном приложении.

.NET-реализация написана в F #, использует словарь и скомпилирована для .NET 4.0

Реализация С++ использует std:: unordered_map и встроена в VS2010 в режиме Release.

На моей машине .NET-код работает в среднем 240 мс, а код С++ работает через 630 мс. Не могли бы вы помочь мне понять, что может быть причиной этой огромной разницы в скорости?

Если я делаю ключевую длину в реализации С++ короче и использую префикс "key_" вместо "key_prefix_", он будет работать через 140 мс.

Другим трюком, который я пытался, является замена std::string на пользовательскую неизменяемую строковую реализацию с указателем const char * на источник и одноразовый вычисленный хеш. Использование этой строки позволило получить производительность реализации С++ до 190 мс.

Код С++:

struct SomeData
{
public:
    float Value;
};

typedef std::string KeyString;
typedef std::unordered_map<KeyString, SomeData> DictionaryT;

const int MaxNumberOfRuns = 125;
const int MaxNumberOfKeys = 6838;

DictionaryT dictionary;
dictionary.rehash(MaxNumberOfKeys);

auto timer = Stopwatch::StartNew();

int lookupCount = 0;

char keyBuffer[100] = "key_prefix_";
size_t keyPrefixLen = std::strlen(keyBuffer);

/// run MaxNumberOfRuns * MaxNumberOfKeys iterations
for(int runId = 0; runId < MaxNumberOfRuns; runId++)
{
    for(int keyId = 0; keyId < MaxNumberOfKeys; keyId++)
    {
        /// get a new key from the pool of MaxNumberOfKeys keys           
        int randomKeySuffix = (std::rand() % MaxNumberOfKeys);
        ::itoa(randomKeySuffix, keyBuffer + keyPrefixLen, 10);

        KeyString key = keyBuffer;

        /// lookup key in the dictionary         
        auto dataIter = dictionary.find(key);
        SomeData* data;

        if(dataIter != dictionary.end())
        {
            /// get existing value           
            data = &dataIter->second;
        }
        else
        {
            /// add a new value
            data = &dictionary.insert(dataIter, DictionaryT::value_type(key, SomeData()))->second;
        }

        /// update corresponding value in the dictionary
        data->Value += keyId * runId;
        lookupCount++;
    }
}

timer.Stop();
std::cout << "Time: " << timer.GetElapsedMilleseconds() << " ms" << std::endl;
std::cout << "Lookup count: " << lookupCount << std::endl;

Печать

Время: 636 мс
Количество просмотров: 854750

F # code

open System
open System.Diagnostics
open System.Collections.Generic

type SomeData =
    struct
        val mutable Value : float
    end

let dictionary = new Dictionary<string, SomeData>()
let randomGen = new Random()

let MaxNumberOfRuns = 125
let MaxNumberOfKeys = 6838

let timer = Stopwatch.StartNew()

let mutable lookupCount = 0

/// run MaxNumberOfRuns * MaxNumberOfKeys iterations
for runId in 1 .. MaxNumberOfRuns do
    for keyId in 1 .. MaxNumberOfKeys do

        /// get a new key from the pool of MaxNumberOfKeys keys
        let randomKeySuffix = randomGen.Next(0, MaxNumberOfKeys).ToString()        
        let key = "key_prefix_" + randomKeySuffix

        /// lookup key in the dictionary
        let mutable found, someData = dictionary.TryGetValue (key)
        if not(found) then
            /// add a new value
            someData <- new SomeData()
            dictionary.[key] <- someData

        /// update corresponding value in the dictionary
        someData.Value <- someData.Value + float(keyId) * float(runId)

        lookupCount <- lookupCount + 1

timer.Stop()

printfn "Time: %d ms" timer.ElapsedMilliseconds
printfn "Lookup count: %d" lookupCount

Печать

Время: 245 мс
Количество просмотров: 854750

4b9b3361

Ответ 1

Visual Studio 2010 использует хеш-функцию для std::string, а не точную. В принципе, если ключевая строка больше 10 символов, хэш-функция останавливается с использованием каждого символа для хэша и имеет шаг больше, чем 1.

size_t operator()(const _Kty& _Keyval) const
    {   // hash _Keyval to size_t value by pseudorandomizing transform
    size_t _Val = 2166136261U;
    size_t _First = 0;
    size_t _Last = _Keyval.size();
    size_t _Stride = 1 + _Last / 10;

    for(; _First < _Last; _First += _Stride)
        _Val = 16777619U * _Val ^ (size_t)_Keyval[_First];
    return (_Val);
    }
  • size() >= 10 - используйте каждый второй символ после первого
  • size() >= 20 - используйте каждый третий символ после первого
  • ...

Благодаря этому, столкновения происходят чаще, что, конечно, замедляет код. Попробуйте пользовательскую хэш-функцию для версии С++.

Ответ 2

Мы можем только предположить, почему одна версия быстрее, чем другая. Вы могли бы определенно использовать профайлер здесь, чтобы рассказать вам, где находится "горячая точка". Поэтому не принимайте это в качестве окончательного ответа.

Ваша заметка о более быстрой версии С++ с более короткой длиной ключа освещается, поскольку она может указывать на пару вещей:

  • Возможно, хеш-функция для std::string действительно оптимизирована для небольших строк, а не для длинных строк.
  • Возможно, более длинная строка занимает больше времени, чтобы скопировать в unordered_set, потому что она отключает небольшую оптимизацию строк в VS 2010 библиотеке С++. Таким образом, копия на карту требует выделения.

Откиньте манжету, вот несколько диких наблюдений, основанных на моем опыте с unordered_map (хотя я больше знаком с повышающей реализацией Microsoft).

  • В этом примере нет причин использовать std::string в качестве типа ключа, просто используйте целочисленное значение. Вероятно, это приведет к ускорению как С++, так и версий F #.

  • Когда вы вставляете значения в карту, скорее всего, не быстрее выполнять поиск, за которым следует вставка, поскольку для этого потребуется повторная хэширование строки ключа. Просто использовал оператор [], который выполняет операцию поиска или вставки. Я думаю, это будет зависеть от того, как часто вы находите хит на карте и добавляете новое значение.

  • Если распределение является узким местом, и вы должны использовать тип строкового ключа, вы можете получить лучшую производительность из хранения общего ptr для строки, а не копировать строку, когда вы вставляете ее в карту.

  • Попробуйте предоставить собственную хэш-функцию для типа ключа, который игнорирует часть "key_prefix_" строки

  • Попробуйте повысить эффективность; возможно, это быстрее.

Опять же, прогон профиля быстро подскажет вам, где искать эту проблему. В частности, он расскажет вам, есть ли узкое место в хешировании и узком месте в распределении.

Ответ 3

Когда вы имеете дело с чистым кодом структуры данных, соотношение сторон 2.6 не так уж и странно. Взгляните на слайды в этом проекте, чтобы понять, что я имею в виду.