如果在调试配置中编译,C++ hash_map.clear() 很慢
C++ hash_map.clear() is slow if compiled in debug configuration
我有一个使用工具集 V10 的托管 VS2010 C++ 项目。我想不通的是,如果我使用调试配置编译我的项目,hash_map 析构函数会非常慢。 hash_map.clear() 稍快一些,但同样痛苦。
注意:VS2015 + Win10无法复现。很可能是 VS2010 问题。
我在网上四处张望,但没有任何东西能解释我得到的结果。
1) 我检查了 _NO_DEBUG_HEAP=1 环境设置。这对我不起作用,因为我没有通过 VS 进行调试。我只是在调试配置中编译代码,运行它没有调试器。
2) 这不是关于插入大量数据。插入没问题。这只是简单地从 hash_map.
中清除数据
3) 我以为如果我在 C++ 代码生成设置下关闭 C++ 异常,我可以解决问题,但事实并非如此。
如果我在发布配置中编译代码,销毁是即时的。如果我在调试配置中编译代码,破坏大约需要 5 分钟或更长时间,这取决于数据有多大。
我确定这只是我需要更正的某种 C++ 项目设置。有人知道如何解决这个问题吗?
回顾一下,我的项目是 VS2010 Managed C++(C++ 和 Managed C# 对象的混合),工具集是 v10。当我使用调试配置编译代码时,销毁 hash_map 花费了 5 分钟(比插入数据本身慢)。当我使用 Release Configuration 编译代码时,它是即时的。
我该如何解决这个问题?谢谢。
这是我使用 VS2010 制作的新项目的完整代码。
我用了不到 5 秒的时间来插入物品。
myMap.clear() 需要 202 秒。
myMap 析构函数需要 280 秒。
这就是我所做的。
1) 使用 VS2010 创建新的 C++ 控制台应用程序...
2) 设置配置属性...
2.1) 常规 > 公共语言运行时支持 > 不支持....
2.2) 常规 > 字符集 > 使用多字节...
2.3) C/C++ > 常规 > 通用语言运行时支持 > 是 /clr...
2.4) C/C++ > 常规 > 调试信息格式 > 程序数据库/Zi...
2.5) C/C++ > 代码生成 > 启用最小重建 > 否 /Gm-...
2.6) C/C++ > 代码生成 > 启用 C++ 异常 > 是,使用 SEH /EHa...
Use Koby code below.
更多更新:
这好像也跟硬件有关? 30K 和 40K 之间的插入性能差异很大。插入停留在 32K 一段时间,然后快速插入其余项目。发生这种情况时,map.clear() 或析构函数会变得非常慢。
不同机器的初始容量可能不同?我在 64 位 Windows7 上使用具有 4GB RAM 的 VM。该程序在Debug配置中为32位。
Koby code result. Running compiled exe without VS2010.
Testing with 30K items:
TestClear()
Insertion: 0.163s
Clear: 0.075s
TestDestructor()
Insertion: 0.162s
Destruction: 4.262s
Testing with 40K items:
TestClear()
Insertion: 4.552s
Clear: 197.363s
TestDestructor()
Insertion: 4.49s
I gave up since destructor is much slower in 30K result..
在 VS2015 和 64 位 Win10 上测试
Testing with 4M items:
TestClear()
Insertion: 8.988s
Clear: 0.878s
TestDestructor()
Insertion: 9.669s
Destruction: 0.869s
结论:
VS2010 很可能有问题。我将其标记为已解决,因为 VS2010 太旧了。我将尝试将整个解决方案升级到更高的 VS。谢谢科比。
TL;DR: std::hash_map
isn't the right tool for the job. It's a non-standard extension that was deprecated in MSVC years ago. Read up on std::unordered_map
, it should meet your general performance requirements. Also, see this.
我在撰写本文时使用的是 VS 2017 15.3.5,因此我的标准库 headers 副本相当最新。我不再拥有 VS 2010 header 文件的副本,因此我的回答可能不完全适用于您的版本。如果可能的话,你真的应该升级。
先分析一下hash_map.clear()
:
void clear() _NOEXCEPT
{ // erase all
_List.clear();
_Init();
}
_List
是一个 std::list
。清除它需要在内存破坏 objects 周围弹跳,直到所有节点都被清理干净。虽然对于链表来说是必需的,但它对性能来说很糟糕。链表通常会产生许多 cache misses。这是大部分问题。
现在让我们来看看_Init()
:
void _Init(size_type _Buckets = _Min_buckets)
{ // initialize hash table with _Buckets buckets, leave list alone
_Vec.reserve(2 * _Buckets); // avoid curdling _Vec if exception occurs
_Vec.assign(2 * _Buckets, _Unchecked_end());
_Mask = _Buckets - 1;
_Maxidx = _Buckets;
}
_Vec
是迭代器的 std::vector
。_Min_buckets
在我的副本中是 8,而 reserve()
只增长,从不缩小,所以我们可以假设重新分配大部分时间都不会发生(至少从清除开始)。但是,通过调用,它会执行多个 b运行ches、潜在的大量析构函数调用和 8 个迭代器分配。这可能看起来不多,但可以很快加起来。
如果不编写自己的散列映射或使用替代实现,您对此无能为力。幸运的是,标准为我们提供了 std::unordered_map
.
更新:
我 运行 你在 VS 2015 和 VS 2017 中的例子,改变了你提到的所有设置,试图重现缓慢的场景。在所有情况下,它都很快完成,不会超过一秒钟。
您的示例并未独立测量 clear()
或销毁时间。这是一个针对 C++/CLI(托管 C++ 的后继者)的修改版本。如果它不为 VS 2010 编译,修改 main
.
的签名
#include "stdafx.h"
#define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS
#include <hash_map>
#include <iostream>
#include <sstream>
#include <time.h>
using namespace System;
namespace Test
{
typedef stdext::hash_map<unsigned int, float> Collection;
typedef std::pair<unsigned int, float> Pair;
const int Iterations = 40000;
const int Multiple = 1000;
const float Increment = 0.004;
clock_t startTime = 0;
std::string ToDisplayString(double count)
{
const double Million = 1000000;
const double Thousand = 1000;
std::stringstream stream;
if (count > Million) {
stream << (count / Million) << "M";
} else if (count > Thousand) {
stream << (count / Thousand) << "K";
} else {
stream << count;
}
return stream.str();
}
void ResetClock()
{
startTime = clock();
}
double GetElapsedSeconds()
{
return (clock() - startTime) / (double)CLOCKS_PER_SEC;
}
void ClearSample()
{
std::cout << "TestClear()\n";
Collection map;
double duration;
ResetClock();
for (int i = 0; i < Iterations; i++) {
if (i % Multiple == 0) {
if (i != 0) {
std::cout << ' ';
}
std::cout << i;
}
map.insert(Pair(i, i + Increment));
}
duration = GetElapsedSeconds();
std::cout << "\nInsertion: " << duration << "s";
ResetClock();
map.clear();
duration = GetElapsedSeconds();
std::cout << "\nClear: " << duration << "s";
}
void DestructorSample()
{
std::cout << "TestDestructor()\n";
double duration;
{
// Moved to a block so we can time destruction.
Collection map;
ResetClock();
for (int i = 0; i < Iterations; i++) {
if (i % Multiple == 0) {
if (i != 0) {
std::cout << ' ';
}
std::cout << i;
}
map.insert(Pair(i, i + Increment));
}
duration = GetElapsedSeconds();
std::cout << "\nInsertion: " << duration << "s";
ResetClock();
}
duration = GetElapsedSeconds();
std::cout << "\nDestruction: " << duration << "s";
}
}
int main(array<String^>^ args)
{
std::cout << "Testing with " << Test::ToDisplayString(Test::Iterations) << " items:\n\n";
Test::ClearSample();
std::cout << "\n\n";
Test::DestructorSample();
std::cin.get();
return 0;
}
这些是我在多次运行中测得的平均时间:
ClearSample()
插入:0.45s
清除:0.021s
DestructorSample()
插入:0.4s
破坏:0.013s
我有一个使用工具集 V10 的托管 VS2010 C++ 项目。我想不通的是,如果我使用调试配置编译我的项目,hash_map 析构函数会非常慢。 hash_map.clear() 稍快一些,但同样痛苦。
注意:VS2015 + Win10无法复现。很可能是 VS2010 问题。
我在网上四处张望,但没有任何东西能解释我得到的结果。
1) 我检查了 _NO_DEBUG_HEAP=1 环境设置。这对我不起作用,因为我没有通过 VS 进行调试。我只是在调试配置中编译代码,运行它没有调试器。
2) 这不是关于插入大量数据。插入没问题。这只是简单地从 hash_map.
中清除数据3) 我以为如果我在 C++ 代码生成设置下关闭 C++ 异常,我可以解决问题,但事实并非如此。
如果我在发布配置中编译代码,销毁是即时的。如果我在调试配置中编译代码,破坏大约需要 5 分钟或更长时间,这取决于数据有多大。
我确定这只是我需要更正的某种 C++ 项目设置。有人知道如何解决这个问题吗?
回顾一下,我的项目是 VS2010 Managed C++(C++ 和 Managed C# 对象的混合),工具集是 v10。当我使用调试配置编译代码时,销毁 hash_map 花费了 5 分钟(比插入数据本身慢)。当我使用 Release Configuration 编译代码时,它是即时的。
我该如何解决这个问题?谢谢。
这是我使用 VS2010 制作的新项目的完整代码。 我用了不到 5 秒的时间来插入物品。 myMap.clear() 需要 202 秒。 myMap 析构函数需要 280 秒。
这就是我所做的。
1) 使用 VS2010 创建新的 C++ 控制台应用程序...
2) 设置配置属性...
2.1) 常规 > 公共语言运行时支持 > 不支持....
2.2) 常规 > 字符集 > 使用多字节...
2.3) C/C++ > 常规 > 通用语言运行时支持 > 是 /clr...
2.4) C/C++ > 常规 > 调试信息格式 > 程序数据库/Zi...
2.5) C/C++ > 代码生成 > 启用最小重建 > 否 /Gm-...
2.6) C/C++ > 代码生成 > 启用 C++ 异常 > 是,使用 SEH /EHa...
Use Koby code below.
更多更新:
这好像也跟硬件有关? 30K 和 40K 之间的插入性能差异很大。插入停留在 32K 一段时间,然后快速插入其余项目。发生这种情况时,map.clear() 或析构函数会变得非常慢。
不同机器的初始容量可能不同?我在 64 位 Windows7 上使用具有 4GB RAM 的 VM。该程序在Debug配置中为32位。
Koby code result. Running compiled exe without VS2010.
Testing with 30K items:
TestClear()
Insertion: 0.163s
Clear: 0.075s
TestDestructor()
Insertion: 0.162s
Destruction: 4.262s
Testing with 40K items:
TestClear()
Insertion: 4.552s
Clear: 197.363s
TestDestructor()
Insertion: 4.49s
I gave up since destructor is much slower in 30K result..
在 VS2015 和 64 位 Win10 上测试
Testing with 4M items:
TestClear()
Insertion: 8.988s
Clear: 0.878s
TestDestructor()
Insertion: 9.669s
Destruction: 0.869s
结论: VS2010 很可能有问题。我将其标记为已解决,因为 VS2010 太旧了。我将尝试将整个解决方案升级到更高的 VS。谢谢科比。
TL;DR: std::hash_map
isn't the right tool for the job. It's a non-standard extension that was deprecated in MSVC years ago. Read up on std::unordered_map
, it should meet your general performance requirements. Also, see this.
我在撰写本文时使用的是 VS 2017 15.3.5,因此我的标准库 headers 副本相当最新。我不再拥有 VS 2010 header 文件的副本,因此我的回答可能不完全适用于您的版本。如果可能的话,你真的应该升级。
先分析一下hash_map.clear()
:
void clear() _NOEXCEPT
{ // erase all
_List.clear();
_Init();
}
_List
是一个 std::list
。清除它需要在内存破坏 objects 周围弹跳,直到所有节点都被清理干净。虽然对于链表来说是必需的,但它对性能来说很糟糕。链表通常会产生许多 cache misses。这是大部分问题。
现在让我们来看看_Init()
:
void _Init(size_type _Buckets = _Min_buckets)
{ // initialize hash table with _Buckets buckets, leave list alone
_Vec.reserve(2 * _Buckets); // avoid curdling _Vec if exception occurs
_Vec.assign(2 * _Buckets, _Unchecked_end());
_Mask = _Buckets - 1;
_Maxidx = _Buckets;
}
_Vec
是迭代器的 std::vector
。_Min_buckets
在我的副本中是 8,而 reserve()
只增长,从不缩小,所以我们可以假设重新分配大部分时间都不会发生(至少从清除开始)。但是,通过调用,它会执行多个 b运行ches、潜在的大量析构函数调用和 8 个迭代器分配。这可能看起来不多,但可以很快加起来。
如果不编写自己的散列映射或使用替代实现,您对此无能为力。幸运的是,标准为我们提供了 std::unordered_map
.
更新:
我 运行 你在 VS 2015 和 VS 2017 中的例子,改变了你提到的所有设置,试图重现缓慢的场景。在所有情况下,它都很快完成,不会超过一秒钟。
您的示例并未独立测量 clear()
或销毁时间。这是一个针对 C++/CLI(托管 C++ 的后继者)的修改版本。如果它不为 VS 2010 编译,修改 main
.
#include "stdafx.h"
#define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS
#include <hash_map>
#include <iostream>
#include <sstream>
#include <time.h>
using namespace System;
namespace Test
{
typedef stdext::hash_map<unsigned int, float> Collection;
typedef std::pair<unsigned int, float> Pair;
const int Iterations = 40000;
const int Multiple = 1000;
const float Increment = 0.004;
clock_t startTime = 0;
std::string ToDisplayString(double count)
{
const double Million = 1000000;
const double Thousand = 1000;
std::stringstream stream;
if (count > Million) {
stream << (count / Million) << "M";
} else if (count > Thousand) {
stream << (count / Thousand) << "K";
} else {
stream << count;
}
return stream.str();
}
void ResetClock()
{
startTime = clock();
}
double GetElapsedSeconds()
{
return (clock() - startTime) / (double)CLOCKS_PER_SEC;
}
void ClearSample()
{
std::cout << "TestClear()\n";
Collection map;
double duration;
ResetClock();
for (int i = 0; i < Iterations; i++) {
if (i % Multiple == 0) {
if (i != 0) {
std::cout << ' ';
}
std::cout << i;
}
map.insert(Pair(i, i + Increment));
}
duration = GetElapsedSeconds();
std::cout << "\nInsertion: " << duration << "s";
ResetClock();
map.clear();
duration = GetElapsedSeconds();
std::cout << "\nClear: " << duration << "s";
}
void DestructorSample()
{
std::cout << "TestDestructor()\n";
double duration;
{
// Moved to a block so we can time destruction.
Collection map;
ResetClock();
for (int i = 0; i < Iterations; i++) {
if (i % Multiple == 0) {
if (i != 0) {
std::cout << ' ';
}
std::cout << i;
}
map.insert(Pair(i, i + Increment));
}
duration = GetElapsedSeconds();
std::cout << "\nInsertion: " << duration << "s";
ResetClock();
}
duration = GetElapsedSeconds();
std::cout << "\nDestruction: " << duration << "s";
}
}
int main(array<String^>^ args)
{
std::cout << "Testing with " << Test::ToDisplayString(Test::Iterations) << " items:\n\n";
Test::ClearSample();
std::cout << "\n\n";
Test::DestructorSample();
std::cin.get();
return 0;
}
这些是我在多次运行中测得的平均时间:
ClearSample()
插入:0.45s
清除:0.021s
DestructorSample()
插入:0.4s
破坏:0.013s