【问题标题】:Fastest way to find counts of integral values (C++)查找整数值计数的最快方法 (C++)
【发布时间】:2016-11-19 22:38:45
【问题描述】:

我需要一个无符号整数列表中每个出现值的出现计数。 IE。如果通过序列 [ 3, 6, 9, 3, 9 ] 我想要 [ { 3, 2}, {6, 1}, {9, 2} ]。

这些值是随机的 32 位无符号整数(范围为 1 到 1,000,000,000)。结果可以存储在任何数据结构中(只要它们可以线性迭代),虽然值排序是理想的,但这是速度之后的次要问题。

目前我有 -

T UniqueCount(std::vector<unsigned> &A)
{
    std::unordered_map<unsigned,unsigned> value_counts;

    for(unsigned val : A) {
        value_counts[val]++;
    }

    A.clear();

    ...
}

分析显示 std::unordered_map 比 std::map 快。

有没有更好的方法呢? /更快的方式?还有一点值得注意,因为用例(count > 4)可以记为4。

这是目前的一个瓶颈,因此虽然首选标准容器,但如果性能提升值得额外的维护成本,则可以考虑定制一些容器。

【问题讨论】:

  • PS 你可以直接将计数存储到_vals,比如用_vals[*it] ++(或value_counts[*it] ++或其他什么,很难说)替换循环中的所有内容,因为operator []@987654321 @(在您的情况下为 0)并返回对该值的引用。
  • 谢谢 - @krzaq 提出了同样的建议。更新了我的代码。
  • 您可能想解释为什么这段代码是一个瓶颈。是否经常重新生成一组新的随机值?如果是这样,为什么不在生成随机值的过程中生成计数,而不是之后呢?另外(次要)请记住,unsigned 不能保证能够表示 32 位值,因此在移植代码时可能存在正确性问题。
  • A 作为非常量引用传递——是否允许修改?具体来说,是否允许排序?
  • @ildjarn 是的 - 可以修改它。但是排序将是 nlogn,而当前代码是 O(N)。说我还没有对它进行分类。

标签: c++ performance c++11


【解决方案1】:

在我的系统(Win10 x64,MSVC daily package x64 发布版本)上,使用输入向量中的 100,000 个随机未排序值进行测试,以下使用 std::sort + std::adjacent_find 的执行时间约为 10 毫秒,而使用 @987654326 的时间约为 27 毫秒@ 和@krzaq 答案中的代码(现在在 OP 中):

std::vector<std::pair<unsigned, unsigned>> unique_count(std::vector<unsigned>& a) {
    auto it = begin(a);
    auto const last = end(a);

    std::vector<std::pair<unsigned, unsigned>> value_counts;
    std::sort(it, last);
    while (it != last) {
        auto const prev = it;
        it = std::adjacent_find(it, last, std::not_equal_to<unsigned>{});
        if (it != last) {
            ++it;
        }
        value_counts.emplace_back(*prev, static_cast<unsigned>(it - prev));
    }
    return value_counts;
}

Online Demo

经验教训:缓存一致性通常胜过算法复杂性。

【讨论】:

  • +1 用于进行实际基准测试。您使用adjacent_find 而不是upper_bound 有什么原因吗?后者似乎是我的自然选择。
  • @krzaq : upper_bound 在所有剩余的输入上反弹,破坏了原始点的缓存一致性。也就是说,它的性能非常接近——this code 在我的系统上运行大约 13 毫秒,而adjacent_find 大约为 10 毫秒。编辑:这可能是 MSVC 乏善可陈的unordered_map 实现的一个更大的指标。
  • 谢谢。对于典型的数据集,这似乎减少了大约 20%。可能会尝试使用 radixsort 来看看我是否可以进一步降低它并修复复杂性以使其进一步扩展!
  • @ildjarn Boost 的扩展排序基于 MSD 基数排序,因此可能比经过良好调整的 LSD 基数排序慢(他们自己承认)。相比之下,基数排序当然不会相形见绌。
【解决方案2】:

如果你的值的范围是合理的(即你没有用完我即将建议的内存),你可以使用一个数组或vector,例如对于范围 [0, max_value] (未经测试,但你明白了):

// init
vector<int> counts(max_value + 1, 0);

// increment:
counts[value] ++;

或者您可以根据需要动态调整大小:

// init
vector<int> counts;

// increment:
if (value >= counts.size())
    counts.resize(value + 1, 0);
counts[value] ++;

如果范围合理但为负数,您可以添加偏移量以使所有值都为非负数,或者为负数维护一个单独的向量并使用它们的绝对值。

否则,哈希映射几乎是要走的路,所以你几乎已经达到了你的极限——你可以继续尝试unordered_map,但提供一个不同的hash function,它可以提供更均匀的哈希分布典型数据的值。

其他想法:

  • 并行化计数 - 在多个线程上计算向量的块,然后 a) 在最后将它们组合或 b) 使用原子增量计数器测试性能(例如 Windows 上的InterlockedIncrement,尽管......你' d 仍然需要新值的线程安全插入,所以可能坚持使用 A)。无法告诉您 a 或 b 是否会更快,您必须进行测试。使用线程池或其他一些预先创建的线程,因为您可能不希望每次都启动和停止线程的全部开销。

  • 如果您获得相同值的长时间运行,或多次短运行,您也许可以将映射迭代器缓存到前一个值。然后,如果您要查看的值与该迭代器相同,则重用该迭代器并为自己保存哈希查找。虽然我看不出这有什么不同,但我不知道,您必须使用您的特定数据集进行尝试。

我真的想不出别的了。

【讨论】:

  • 谢谢。不幸的是,范围高达 1,000,000,000,所以这并不可行。
  • @user2036256 啊,是的,这有点多。您是否达到了所有 1,000,000,000 个值,还是在该范围内分布非常稀疏?
  • 平均约 100,000 个条目,在该范围内相当随机。按照目前的情况,不太可能超过 1,000,000 个条目。
  • @user2036256 这些值是非常随机的顺序(它们在您的示例中,但只是确认)还是您倾向于获得相同值的长期运行(甚至是短期运行,但其中很多)?
  • 值以随机顺序显示。尽管对于真正的随机序列,重复项比您预期的要多。
猜你喜欢
  • 2018-02-10
  • 1970-01-01
  • 2011-07-08
  • 1970-01-01
  • 1970-01-01
  • 2012-02-16
  • 1970-01-01
  • 1970-01-01
  • 2017-03-15
相关资源
最近更新 更多