我有一个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;
打印:
时间: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
打印:
时间:245毫秒
查询次数:854750
答案 0 :(得分: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);
}
size() >= 10
- 在第一个size() >= 20
- 在第一个由于这一点,碰撞更频繁地发生,这当然会减慢代码速度。尝试使用C ++版本的自定义哈希函数。
答案 1 :(得分:5)
我们只能推测为什么一个版本比另一个版本更快。你绝对可以在这里使用一个分析器来告诉你热点在哪里。所以不要把这些作为明确的答案。
关于c ++版本更快,密钥长度更短的说明很有启发性,因为它可能指向以下几点:
关闭袖口,根据我对unordered_map的经验,这里有一些疯狂的观察(虽然我对微软的提升实施更熟悉)。
在此示例中,没有理由使用std :: string作为键类型,只需使用整数值。这可能会使c ++和F#版本更快。
当您在地图中插入值时,执行查找后跟插入可能不会更快,因为两者都需要重新散列键字符串。刚刚使用了[]运算符,它自己执行查找或插入操作。我想这取决于您在地图中找到点击次数与添加新值的频率。
如果分配是瓶颈,并且您必须使用字符串键类型,那么将共享ptr存储到字符串中可能会获得更好的性能,而不是在将字符串插入映射时复制字符串。
尝试为忽略字符串“key_prefix_”部分的密钥类型提供自己的哈希函数
尝试使用boost的实现;也许它更快。
同样,配置文件运行会很快告诉您在哪里寻找此类问题。具体来说,它会告诉您散列是否存在瓶颈与分配瓶颈有关。
答案 2 :(得分:0)
当你处理纯数据结构代码时,速度比为2.6并不奇怪。 看看slides on this project,看看我的意思。