С++ ~ 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
Ответы
Ответ 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 не так уж и странно.
Взгляните на слайды в этом проекте, чтобы понять, что я имею в виду.