如果在调试配置中编译,C++ hash_map.clear() 会很慢

BoB*_*Dev 1 c++ performance hashmap visual-studio-2010

我有一个使用工具集 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 托管 C++(C++ 和托管 C# 对象之间的混合),工具集是 v10。当我使用调试配置编译代码时,销毁 hash_map 需要 5 分钟(比插入数据本身慢)。当我使用发布配置编译代码时,它是即时的。

我该如何解决?谢谢。

这是我使用 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.
Run Code Online (Sandbox Code Playgroud)

更多更新:

这似乎也取决于硬件?30K和40K之间的插入性能差异很大。插入在 32K 停留了一段时间,然后快速插入其余项目。当这种情况发生时,map.clear()或析构函数会变得非常慢。

不同机器上初始容量可能不同?我在 64 位 Windows7 上使用具有 4GB RAM 的虚拟机。该程序在调试配置下为 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..
Run Code Online (Sandbox Code Playgroud)

在VS2015和64位Win10上测试

Testing with 4M items:

TestClear()
Insertion: 8.988s
Clear: 0.878s

TestDestructor()
Insertion: 9.669s
Destruction: 0.869s
Run Code Online (Sandbox Code Playgroud)

结论:VS2010 很可能有问题。我会将其标记为已解决,因为 VS2010 太旧了。我会尝试将整个解决方案升级到更高的 VS。谢谢你,科比。

Kob*_*uck 5

TL;DR: std::hash_map不是适合这项工作的工具。这是一个非标准扩展,几年前在 MSVC 中已被弃用。阅读std::unordered_map,它应该满足您的一般性能要求。另外,请参阅

在撰写本文时,我正在使用 VS 2017 15.3.5,因此我的 std 库标头副本相当是最新的。我不再拥有 VS 2010 头文件的副本,因此我的答案可能并不完全适用于您的版本。如果可能的话你真的应该升级。

首先我们来分析一下hash_map.clear()

void clear() _NOEXCEPT
    {   // erase all
    _List.clear();
    _Init();
    }
Run Code Online (Sandbox Code Playgroud)

_List是一个std::list。清除它需要在内存破坏对象周围弹跳,直到所有节点都被清理干净。虽然对于链表来说是必要的,但它对性能来说很糟糕。链接列表通常会导致许多缓存未命中。这是问题的大部分。

现在让我们看一下_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;
    }
Run Code Online (Sandbox Code Playgroud)

_Vec是一个std::vector迭代器。_Min_buckets在我的副本中是 8,并且reserve()只会增长,不会缩小,因此我们可以假设大多数情况下都不会发生重新分配(至少从清除状态来看)。然而,通过调用,它执行多个分支、可能大量的析构函数调用和 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;
}
Run Code Online (Sandbox Code Playgroud)

这些是我在多次运行中测量的平均时间:
ClearSample()
插入:0.45 秒
清除:0.021 秒

DestructorSample()
插入:0.4s
销毁:0.013s