标准::tr1::unordered_map::运算符[]的时间效率

Time efficiency on std::tr1::unordered_map::operator[]

本文关键字:效率 时间 map tr1 unordered 标准 运算符      更新时间:2023-10-16

我正在优化Visual Studio 2008 SP1中的一段代码。知道unorder_map在恒定时间插入/删除/查找时非常棒,所以我通过使用unordered_map作为我的主要数据结构来优化代码。请看下面的代码。

....
    typedef std::tr1::unordered_map <__int64, int> umap_id;
    const int text1_length = text1.GetLength();
    const int text2_length = text2.GetLength();
    const int max_d = text1_length + text2_length - 1;
    const bool doubleEnd = (64 < max_d);
    vector<set<pair<int, int> > > v_map1;
    vector<set<pair<int, int> > > v_map2;
    vector<int> v1(2 *max_d, 0);
    vector<int> v2(2 *max_d, 0);
    int x, y;
    __int64 footstep;
    umap_id footsteps(max_d * 2);
    bool done = false;
    const bool forward = ((text1_length + text2_length) % 2 == 1);
    for (int d = 0; d < max_d; ++d)
    {
        // Walk forward path one step
        v_map1.push_back(set<pair<int, int> >());
        for (int k = -d; k <= d; k += 2)
        {
            if (k == -d || (k != d && v1[k - 1 + max_d] < v1[k + 1 + max_d]))
                x = v1[k + 1 + max_d];
            else
                x = v1[k - 1 + max_d] + 1;
            y = x - k;
            if (doubleEnd)
            {
                footstep = (__int64) ((__int64)x << 32 | y);
                if (!forward)
                    footsteps[footstep] = d;
                else if (footsteps.find(footstep) != footsteps.end())
                    done = true;
            }
            ....
        }
    }
....

但事实证明它仍然很慢。鉴于我的输入相对较小(max_d =946),它运行超过 20 秒。

我对发布版本进行了探查器分析,探查器揭示了这一行:footsteps[footstep] = d;是运行447931次并花费了大约 20 秒的主要罪魁祸首。

请注意,在同一循环体中还有另一行代码:else if (footsteps.find(footstep) != footsteps.end())执行相同次数(即 447931 次),但花费的秒数要少得多。

unordered_mapoperator::[]对我来说似乎是一个黑匣子。我不知道为什么需要这么长时间。这是一个 32 位应用程序。任何帮助,不胜感激。

> 在没有 SP1 的 VS 2008 中(但使用为您提供 TR1 库的功能包),tr1::unordered_map<>的默认哈希函数仅考虑键值的较低 32 位。至少这是我对<functional>标头中template<class _Kty> class hash::operator()实现的阅读。

作为键的footstep变量使用为y计算的任何内容作为其较低的 32 位 - y 中是否有足够的变化,以至于它会自己产生一个很好的哈希值(我无法判断正在计算y的代码在做什么)? 否则,您可能会将比您想要的更多的项目放入特定的哈希桶中,并生成太多冲突。

如果是这种情况,您可能需要考虑提供自己的哈希函数。

顺便说一下,看起来VS 2010在与64位整数一起使用时具有hash::operator()的专用化,因此它将对所有64位进行哈希处理-如果您使用的是VS 2010,我的答案中的推测应该不适用。


更新:

经过一些测试,我确信这就是问题所在(VS 2008 SP1中也存在该问题)。您可以通过将编译器升级到VS 2010来解决此问题,VS 2010具有更好的64位类型的哈希函数,或者使用您自己的哈希函数自己处理此问题。以下是我在VS2008中快速测试的一个,它似乎有效:

class hash_int64
    : public unary_function<__int64, size_t>
{
public:
    typedef __int64 key_t;
    typedef unsigned int half_t;
    size_t operator()(const key_t& key) const
    {   
        // split the 64-bit key into 32-bit halfs, hash each
        // then combine them
        half_t x = (half_t) (key & 0xffffffffUL);
        half_t y = (half_t) (((unsigned __int64) key) >> 32);
        return (hash<half_t>()(x) ^ hash<half_t>()(y));
    }
};

然后将typedef更改为:

typedef std::tr1::unordered_map <__int64, int, hash_int64> umap_id;

在调试构建中,Visual Studio 附带的 STL 大量使用迭代器检查和小型嵌套函数,这些函数在发布版本中全部内联。这就是为什么与发布代码相比,使用 STL 的调试代码非常慢的原因。

可能你遇到了很多碰撞。如果unordered_map是使用哈希函数实现的,并且您遇到很多冲突,则必须遍历列表才能到达您的项目。这可能是一个原因,但我从未看过unordered_map实现。

众所周知,

哈希映射具有相当高的常量开销。只有 946 个元素,具有基本免费的比较运算符,可能应该使用 std::map 的 O(log(n)) 查找。但是,除非存在实现错误,否则operator[]没有理由花费比find()更多的时间。

在Visual Studio 2005和2008中,应手动设置_SECURE_SCL=0。它默认为启用,即使在发布版本中也是如此,这会增加大量的运行时检查,这在某些情况下可能非常昂贵。

Visual Studio 2010 修复了此问题,默认为实际快速发布。

除此之外,可能值得尝试用普通的旧std::map替换数据结构。当然,只要您在 32 位构建中使用 64 位整数键也是非最佳选择。

以 x64 为目标可能会显着改善事情,但如果您坚持使用 32 位,您可以考虑是否可以用双精度替换整数键,因为 CPU 可以本机处理这些键(我不知道双打的默认哈希函数是什么样子的,而且总体上可能会更慢, 但至少值得测试)