使用字符串键的unordered_map中的C++〜1M查找比.NET代码慢得多

evg*_*nyp 23 .net c++ performance f#

我有一个perf测试函数的.NET和C++实现,它使用来自6838个键池的字符串键在字典中执行854,750个查找.我编写了这些函数来调查真实应用程序中的性能瓶颈.

.NET实现是用F#编写的,使用Dictionary并为.NET 4.0编译

C++实现使用std :: unordered_map,并在发布模式下使用VS2010构建.

在我的机器上,.NET代码平均运行240毫秒,C++代码运行630毫秒.你能帮我理解速度这个巨大差异的原因是什么?

如果我将C++实现中的密钥长度缩短并使用"key_"前缀而不是"key_prefix_",它将在140毫秒内运行.

我尝试的另一个技巧是用一个自定义的不可变字符串实现替换std :: string,该实现具有指向源的const char*指针和一次性计算的哈希.使用此字符串可以将C++实现的性能降低到190毫秒.

C++代码:

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;
Run Code Online (Sandbox Code Playgroud)

打印:

时间:636毫秒
查询次数:854750

F#代码

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
Run Code Online (Sandbox Code Playgroud)

打印:

时间:245毫秒
查询次数:854750

Xeo*_*Xeo 45

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);
    }
Run Code Online (Sandbox Code Playgroud)
  • size() >= 10 - 在第一个字符后使用每隔一个字符
  • size() >= 20 - 在第一个字符后使用每三个字符
  • ...

多亏了这一点,碰撞更频繁地发生,这当然会减慢代码的速度.尝试C++版本的自定义哈希函数.

  • 谢谢!!!我很尴尬,在发布这个问题之前我没有尝试过.是的,它有效.使用自定义散列函数C++ perf约为180ms.这比.NET快约50毫秒. (5认同)
  • 哇,这是一个很好的观察.谢谢! (3认同)

kar*_*ski 5

我们只能推测为什么一个版本比另一个版本更快.你绝对可以在这里使用一个分析器来告诉你热点在哪里.所以不要把这些作为明确的答案.

关于c ++版本更快,密钥长度更短的说明很有启发性,因为它可能指向以下几点:

  • 也许std :: string的哈希函数实际上是针对小字符串而不是长字符串进行优化的.
  • 也许较长的字符串需要更长的时间才能复制到unordered_set中,因为它会在VS 2010 c ++库中禁用小字符串优化.因此,复制到地图中需要分配.

关闭袖口,根据我对unordered_map的经验,这里有一些疯狂的观察结果(虽然我对微软的提升实现更熟悉).

  • 在这个例子中,没有理由使用std :: string作为密钥类型,只需使用整数值.这可能会使c ++和F#版本更快.

  • 当您将值插入到地图中时,执行查找后跟插入可能不会更快,因为两者都需要重新散列键字符串.刚刚使用了[]运算符,它自己执行查找或插入操作.我想这取决于您在地图中找到点击次数与添加新值的频率.

  • 如果分配是瓶颈并且您必须使用字符串键类型,则可以通过将共享ptr存储到字符串而不是在将字符串插入映射时复制字符串来获得更好的性能.

  • 尝试为忽略字符串"key_prefix_"部分的密钥类型提供自己的哈希函数

  • 试试boost的实现; 也许它更快.

同样,配置文件运行会很快告诉您在哪里寻找这种问题.具体来说,它会告诉您散列是否存在瓶颈与分配瓶颈有关.