c++ ~ 1M在unordered_map中查找字符串键要比.net代码慢得多
C++ ~ 1M look-ups in unordered_map with string key works much slower than .NET code
我有一个perf test函数的。net和c++实现,该函数使用6838个键池中的字符串键在字典中进行854,750次查找。我编写这些函数是为了调查真实应用程序中的性能瓶颈。
。.NET实现是用f#编写的,使用字典,并为。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 ms
查找计数:854750
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
打印:
时间:245ms
查找计数:854750
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++版本的自定义哈希函数
我们只能推测为什么一个版本比另一个更快。你绝对可以让侧写师告诉你热点在哪里。所以,不要把这些作为一个确定的答案。
你关于c++版本用更短的键长度更快的注释很有启发性,因为它可以指向两件事:
- 也许std::string的哈希函数真的是为小字符串而不是长字符串优化的。 也许较长的字符串需要更长的时间来复制到unordered_set,因为它禁用了VS 2010 c++库中的一个小字符串优化。因此,复制到map中需要分配。
根据我使用unordered_map的经验(尽管我对Microsoft的boost实现更熟悉),这里有一些即兴的观察。
-
在这个例子中,没有理由使用std::string作为键类型,只使用整数值。这可能会使c++和f#版本更快。
-
当您向映射中插入值时,执行查找操作后再执行插入操作可能不会更快,因为两者都需要重新散列键字符串。只是使用了[]操作符,它自己执行查找或插入操作。我想这取决于你在地图上找到命中目标的频率,而不是添加新值。
-
如果分配是瓶颈,并且您必须使用字符串键类型,那么在将字符串插入映射中时,将共享ptr存储到字符串中而不是复制字符串,可能会获得更好的性能。
-
尝试为忽略字符串的"key_prefix_"部分的密钥类型提供自己的哈希函数
-
尝试boost的实现;
同样,运行概要文件会很快告诉您在哪里查找这类问题。具体来说,它会告诉你在散列中是否存在瓶颈,在分配中是否存在瓶颈。
处理纯数据结构代码时,2.6的速比并不奇怪。看一下这个项目的幻灯片,你就会明白我的意思了。
- 是否可以在Linux上使用.Net Core 3.1创建C++/CLI代码的C#DLL
- 终止调用本机代码的 .Net 线程
- 可以通过代码(使用 C、C++ 或 .NET)重新启动 PCIe 卡
- .NET CORE 2 DLL引用带有C 代码运行时错误
- AccessViolationException从托管C (ASP.NET的包装器)调用本机C 代码
- 在 之前执行代码。NET 的 Main() 方法
- 如何从协议缓冲区 .NET 代码生成 .proto 文件
- 从C#.NET应用程序调用C/C 代码
- 如何从com库中打印到主机,并从.NET代码召唤激活器
- 编译为托管的C++代码.NET(C++/CLI),否则作为本机C++
- 从 VB.NET 调用非托管代码C++ dll
- VC++ VB.NET 代码转换
- 通过 .net 互操作类调试到 COM C++代码
- 为什么我想使用托管(C#和.NET)或本机代码进行Windows API开发
- DllImport 未在 asp.net 代码隐藏文件中定义,即使包含 System.Runtime.InteropSe
- 将 c# asp.net Web 应用程序与C++ dll 链接时,是在服务器或客户端上运行的 dll 代码
- 对于多少种类型的 x86 处理器,Ngen 和 .net 本机一次创建本机代码
- 从非托管代码调用托管.NET代码的最佳方式
- c++ ~ 1M在unordered_map中查找字符串键要比.net代码慢得多
- 有可能在vb.net代码中使用c++类吗?