【问题标题】:sort huge array with small number of repeating keys用少量重复键对巨大的数组进行排序
【发布时间】:2015-10-10 18:17:22
【问题描述】:

我想对一个巨大的数组进行排序,比如 10^8 个 X 类型的条目,最多 N 不同的键,其中 N 约为 10^2。因为我不知道元素的范围或间距,所以计数排序不是一个选项。所以到目前为止我最好的猜测是使用哈希映射来计算这样的计数

std::unordered_map< X, unsigned > counts;
for (auto x : input)
    counts[x]++;

这很好用,比 3 路快速排序快 4 倍,但我很紧张,仍然不够快。

我想知道:我错过了什么吗?我可以更好地利用N 提前知道的事实吗?或者是否可以根据我的需要调整哈希映射?


EDIT另外一个前提条件是输入的序列排序不好,并且按键的频率大致相同。

【问题讨论】:

  • 在开始添加到哈希表之前可以调用reserve(N)吗?
  • 键是否连续?你是只知道他们的数量,还是知道他们的价值观?
  • @IVlad 我试过了。但是,对我的地图实现几乎没有影响。
  • for (auto const&amp; x : input) 会避免复制它应该更快
  • 你可以试试比std::unordered_map更优化的哈希表,比如谷歌的dense_hash_map。有时使用比std::hash 更好的哈希函数也有帮助。

标签: algorithm performance sorting c++11 stdvector


【解决方案1】:

我希望将项目存储在排序的向量中,因为大约 100 个键意味着插入向量中只会出现 10^6 个条目中的 1 个。查找将是向量中的处理器高效搜索

【讨论】:

    【解决方案2】:

    STL 实现在性能方面通常并不完美(请不要圣战)。

    如果您知道唯一元素数量的有保证且合理的上限 (N),那么您可以轻松实现自己的大小为 2^s 的哈希表> N。以下是我自己通常的做法:

    int size = 1;
    while (size < 3 * N) size <<= 1;
    //Note: at least 3X size factor, size = power of two
    //count = -1 means empty entry
    std::vector<std::pair<X, int>> table(size, make_pair(X(), -1));
    auto GetHash = [size](X val) -> int { return std::hash<X>()(val) & (size-1); };
    
    for (auto x : input) {
      int cell = GetHash(x);
      bool ok = false;
      for (; table[cell].second >= 0; cell = (cell + 1) & (size-1)) {
        if (table[cell].first == x) { //match found -> stop
          ok = true;
          break;
        }
      }
      if (!ok) {             //match not found -> add entry on free place
        table[cell].first = x;
        table[cell].second = 0;
      }
      table[cell].second++;  //increment counter
    }
    

    在 MSVC2013 上,与您的代码相比,它将时间从 0.62 秒缩短到 0.52 秒,因为 int 被用作类型 X

    另外,我们可以选择更快的哈希函数。但是请注意,哈希函数的选择很大程度上取决于输入的属性。让我们来Knuth's multiplicative hash

    auto GetHash = [size](X val) -> int { return (val*2654435761) & (size-1); };
    

    它进一步将时间缩短到 0.34 秒。

    作为结论:您真的要重新实现标准数据结构以实现 2 倍的速度提升吗?

    注意:在其他编译器/机器上的加速可能完全不同。如果你的类型 X 不是 POD,你可能需要做一些修改。

    【讨论】:

    • 使用dense_hash_map 比使用自己的更好。
    【解决方案3】:

    计数排序确实最好,但由于范围或间距未知,因此不适用。

    似乎很容易与 fork-join 并行化,例如boost::thread.

    您还可以尝试更高效的手动哈希映射。 Unorded_map 通常使用链表来对抗潜在的不良哈希函数。如果哈希表不适合 L1 缓存,则链表的内存开销可能会损害性能。 Closed Hashing 可能会使用更少的内存。一些优化提示:

    • 带有线性探测但不支持删除的封闭哈希
    • 两种大小的哈希表的能力,用于位移而不是模(除法需要多个周期,每个内核只有一个硬件除法器)
    • 低 LoadFactor(按大小输入)以最大程度地减少冲突。这是内存使用和冲突次数之间的权衡。应避免 LoadFactor 超过 0.5。 256 的哈希表大小似乎适合 100 个条目。
    • 便宜的哈希函数。您还没有显示 X 的类型,所以也许更便宜的哈希函数可能会超过更多的冲突。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-07-16
      • 1970-01-01
      • 1970-01-01
      • 2019-03-09
      • 2012-02-19
      • 1970-01-01
      • 2013-05-10
      相关资源
      最近更新 更多