在无序元素上有效独特

时间:2014-04-18 09:05:29

标签: c++ performance algorithm unique

我想提高我在分形分析中使用的盒子计数方法的速度性能。

关于任务

我有一个整数流(大约n = 2 ^ 24长),我必须计算流中有多少个不同的值。没有上限,允许负值(但负值的数量可能小于sqrt(n))。流中存在小的相关性,即实际元素可能与前一元素相等或不太远。在许多情况下,我在整个范围内都有很多相等的值。

我已经尝试过的方法

vector,sort,uniqe

我的第一个实现是将所有元素放入一个向量中,然后我应用了std :: sort然后std :: unique。

这种方法的复杂性是O(n * log(n)),我认为在扩展方面,任何其他算法都不会更快。但我确信一个代码必须存在比这更快但具有相同的缩放属性 - 只有一个常数因子才能更快。原因是:

  1. 我在向量中存储了大量相等的值,因此排序效果不佳,向量过大
  2. 在这种方法中,我不使用实际元素和前者相互接近的信息
  3. 我不需要有关这些唯一值的信息,我只需要不同元素的数量
  4. 设置,插入,大小

    为了消除第一个无效点,我将每个元素放入set :: insert的集合中。最后我用set :: size来计算元素的数量。

    我的期望是这段代码必须更快,因为只有唯一值存储在集合中,并且不必将新元素与大量相等的值进行比较。但不幸的是,这种方法比前一种方法慢1.5倍。

    set,emplace_hint,size

    为了消除第二个无效点,我不仅将每个元素放入一个集合中,而且使用函数set :: emplace_hint。每当一个提示将新元素放在前一个元素旁边时。最后,我用set :: size

    询问了集合的大小

    我的期望是这段代码必须比之前的代码更快,因为我可以猜出新元素的价值,而且它总比没有好。但不幸的是,这种方法比前一种方法慢了5倍。

    问题

    您能否建议任何可以计算流中不同元素(int)数量的有效方法?如果已知

    ,您可以优化代码吗?
    1. 数字中存在可衡量的相关性
    2. 有些数字会重复出现
    3. 目标架构是现代x86或x86-64 PC处理器(带sse,sse2),只有单线程代码是合适的。我不喜欢使用boost而是使用c ++ 11.

      解决方案

      首先,感谢许多建议,耐心和理解,我很抱歉我无法测试所有方法,我也确信有效性取决于我不知道的内容流的细节提供。但是我分享了VS2013编译器的结果。 (代码在gcc4.7下测试但未测量。)这个主题值得花很多时间去研究,但我有一个符合我需求的解决方案。 Time statistics for different methods

      关于方法:

      • bool的载体:来自DieterLücking的BitVector解决方案
      • 二进制查找:Tony D
      • 建议的方法
      • 无序集合:将所有元素简单地放入std :: unordered_set,然后按照Ixanezis的建议询问其元素的数量
      • 矢量插入排序:使用DieterLücking的Sorted Vector方法
      • set insert:我在问题表格中描述的方法
      • 基数排序:Ixanezis的建议,在向量上使用流行的排序算法
      • 设置emplace提示:使用问题表格
      • 中描述的std :: emplace_hint

6 个答案:

答案 0 :(得分:5)

由于您只处理有限范围的整数,因此可以有效地使用radix sort算法,从而减少复杂性的log(N)部分。您可以在互联网的任何地方选择任何真正快速的实施。其中一些需要SSE支持,另一些需要多线程甚至编码才能在GPU上运行。

如果您可以使用boost::unordered_setC++11 std::unordered_set,那么您可以轻松修改第二种方法,并使用线性复杂度算法。但是,如果您在流中至少有数百万个数字,我相信第一种方法会更快。

答案 1 :(得分:4)

只是比较不同的方法(不考虑基数排序):

#include <algorithm>
#include <deque>
#include <iostream>
#include <unordered_set>
#include <set>
#include <vector>
#include <chrono>

template <template <typename ...> class Container, typename T, typename ... A, typename Comp>
inline bool insert_sorted(Container<T, A...>& container, T const& e, Comp const& comp) {
    auto const it = std::lower_bound(container.begin(), container.end(), e, comp);
    if (it != container.end() and not comp(e, *it)) { return false; }
    container.insert(it, e);
    return true;
}

template <template <typename ...> class Container, typename T, typename ... A>
inline bool insert_sorted(Container<T, A...>& container, T const& e) {
    return insert_sorted(container, e, std::less<T>{});
}

int main() {
    using namespace std::chrono;
    typedef std::vector<int> data_type;

    const unsigned Size = unsigned(1) << 24;
    const unsigned Limit = 1000;
    data_type data;
    data.reserve(Size);
    for(unsigned i = 0; i < Size; ++i) {
        int value = double(Limit) * std::rand() / RAND_MAX - 0.1;
        data.push_back(value);
        while(i < Size - 1 && rand() < RAND_MAX * 0.25) {
            data.push_back(value);
            ++i;
        }
    }

    std::cout
        << "Data\n"
        << "====\n"
        << "                Size of data: " << Size << '\n';

    std::cout
        << "Unorderd Set\n"
        << "============\n";
    {
        auto start = system_clock::now();

        typedef std::unordered_set<int> set_type;
        set_type set;
        unsigned i = 0;
        for( ; i < Size - 1; ++i) {
            // Ignore a range of equal values
            while(data[i] == data[i+1]) ++i;
            set.insert(data[i]);
        }
        if(i < Size)
            set.insert(data[i]);

        auto stop = system_clock::now();

        std::cout
            << "Number of different elements: "
            << set.size() << '\n';
        std::cout
            << "                      Timing: "
            << duration_cast<duration<double>>(stop - start).count()
            << '\n';
    }

    std::cout
        << "Set\n"
        << "===\n";
    {
        auto start = system_clock::now();

        typedef std::set<int> set_type;
        set_type set;
        unsigned i = 0;
        for( ; i < Size - 1; ++i) {
            // Ignore a range of equal values
            while(data[i] == data[i+1]) ++i;
            set.insert(data[i]);
        }
        if(i < Size)
            set.insert(data[i]);

        auto stop = system_clock::now();

        std::cout
            << "Number of different elements: "
            << set.size() << '\n';
        std::cout
            << "                      Timing: "
            << duration_cast<duration<double>>(stop - start).count()
            << '\n';
    }

    std::cout
        << "Sorted Vector\n"
        << "=============\n";
    {
        auto start = system_clock::now();

        typedef std::vector<int> set_type;
        set_type set;
        unsigned i = 0;
        for( ; i < Size - 1; ++i) {
            // Ignore a range of equal values
            while(data[i] == data[i+1]) ++i;
            insert_sorted(set, data[i]);
        }
        if(i < Size)
            insert_sorted(set, data[i]);

        auto stop = system_clock::now();

        std::cout
            << "Number of different elements: "
            << set.size() << '\n';
        std::cout
            << "                      Timing: "
            << duration_cast<duration<double>>(stop - start).count()
            << '\n';
    }

    std::cout
        << "BitVector\n"
        << "=========\n";
    {
        auto start = system_clock::now();

        typedef std::vector<bool> set_type;
        set_type set(Limit);
        unsigned i = 0;
        unsigned elements = 0;
        for( ; i < Size; ++i) {
            if( ! set[data[i]]) {
                set[data[i]] = true;
                ++elements;
            }
        }

        auto stop = system_clock::now();

        std::cout
            << "Number of different elements: "
            << elements << '\n';
        std::cout
            << "                      Timing: "
            << duration_cast<duration<double>>(stop - start).count()
            << '\n';
    }

    std::cout
        << "Sorted Data\n"
        << "===========\n";
    {
        auto start = system_clock::now();

        std::sort(data.begin(), data.end());
        auto last = std::unique(data.begin(), data.end());

        auto stop = system_clock::now();

        std::cout
            << "Number of different elements: "
            << last - data.begin() << '\n';
        std::cout
            << "                      Timing: "
            << duration_cast<duration<double>>(stop - start).count()
            << '\n';
    }

    return 0;
}

用g ++编译-std = c ++ 11 -O3给出:

Data
====
                Size of data: 16777216
Unorderd Set
============
Number of different elements: 1000
                      Timing: 0.269752
Set
===
Number of different elements: 1000
                      Timing: 1.23478
Sorted Vector
=============
Number of different elements: 1000
                      Timing: 1.13783
BitVector
=========
Number of different elements: 1000
                      Timing: 0.038408
Sorted Data
===========
Number of different elements: 1000
                      Timing: 1.32827

因此,如果内存没有问题或数字范围有限,设置一个位是最佳选择。否则,unordered_set是一个很好的。

答案 2 :(得分:3)

假设32位int,最糟糕的情况是你需要2 ^ 32位来跟踪你可能看到的每个数字的看/未看状态。这个40亿比特,即512百万字节 - 512兆字节 - 对现代台式电脑来说并不高。您基本上可以将一个字节[n/8]索引到数组中,然后按位 - 或 - 或 - 或1 << (n % 8)来设置或测试数字的状态。因为你说输入中关闭的数字往往在值上相近,所以缓存利用率应该非常好。您可以检查刚刚看到的数字并绕过位数组处理。

如果您碰巧知道在输入中要跟踪的数字少于2 ^ 32个不同的数字,那么您当然应该减小相应位集的大小。 (只需阅读您的评论&#34;允许使用否定数字,但这种情况非常罕见(可能性小于1 / n)。&#34; - 在这种情况下,您可以使用set负数,并使用一半的记忆积极。)

(如果您担心可能没有任何位设置的许多内存页面的最后一次迭代,您可以创建一个额外的&#34;脏页&#34;索引,每页一位,以指导这样的迭代,但是考虑到输入的数量,如果该输入在int的数值范围内广泛传播,这可能是微不足道的,甚至适得其反。)

编辑/ - 评论中要求的进一步解释。首先,实施:

template <size_t N>
class Tracker
{
  public:
    Tracker() { std::fill_n(&data_[0], words, 0); }
    void set(int n) { data_[n / 32] |= (1u << (n % 8)); }
    bool test(int n) const { return data_[n / 32] &= (1u << (n % 8)); }

    template <typename Visitor>
    void visit(Visitor& visitor)
    {
        for (size_t word = 0, word_n = 0; word < words; ++word, word_n += 32)
             if (data_[word])
                  for (uint32_t n = 0, value = 1; n < 32; ++n, value *= 2)
                      if (data_[word] & value)
                          visitor(word_n + n);
    }
  private:
    static const int words = N / 32 + (N % 32 ? 1 : 0);
    uint32_t data_[words];
};

用法:

Tracker<size_t(std::numeric_limits<int>::max()) + 1> my_tracker;
int n;
while (std::cin >> n)
    my_tracker.set(n);
my_tracker.visit([](unsigned n) { std::cout << n << '\n'; });

(未经测试......可能是一些小问题)

  

你能解释一下你的答案更详细吗?

所有这一切都是在概念上创建一个bool have_seen[]数组,可以直接用你感兴趣的任何整数索引:你只需要通过输入设置索引处的布尔元素你在输入中看到的是真的。如果你把事情设置成真实两次或更多次 - 谁在乎呢?纯粹为了节省内存并获得搜索设置位的速度(例如填充/清除),它会将bool值手动打包成更大的整数数据类型的位。

  

我想我可以打扰负值,因为我可以计算总成本为O(n)的最大值和最小值。

嗯,也许,但是两次通过可能会更快或更慢。根据我已经记录的方法,您不需要两次检查数据......您可以在第一次迭代期间准备答案。当然,如果进行初始迭代很快(例如来自SSD介质),并且你对内存足够紧,你只想对实际数据范围进行实际分析,那就去吧。

  

它还有助于将int的宽度缩小到正确的值,因此超过一半的页面将是非空的。

不确定你的意思。

答案 3 :(得分:1)

此任务的标准数据结构是一个哈希集,在stl中也称为std::unordered_set(顺便说一句,谷歌的密集哈希设置通常表现稍好)

您不需要对唯一值进行排序,因此std::set对于您的用例来说速度非常慢。

像其他人建议的那样,如果你的宇宙(可能的值)不是太大,你也可以使用位向量如果你有负值,你可以转换为无符号并将它们视为非常大的数字。

答案 4 :(得分:1)

将当前元素与之前的元素进行比较,然后将其传递给计数方法,无论它是什么都有帮助吗?

或保留最后10个元素的小/快速缓存来丢弃短程重复项?

或者批量计数(依据临时计数器计算100的序列,然后与之前的计数合并)?

答案 5 :(得分:-1)

我赞成尝试使用STL容器(setunordered_set,...)但不幸的是你为它们付出了代价:它们的内存稳定性和轻量级迭代器要求要求它们被实现为基于节点的容器,对每个元素都有巨大的(相对而言)开销。

我会提出两种方法:

  1. 坚持vector(仅对低比例的独特物品有效)
  2. 实施Robin-Hood哈希
  3. 使用概率方法

  4. 排序的矢量

    对于vector方法:没有什么可以阻止您在插入时保持vector排序,从而避免插入重复元素。 An example here

    #include <iostream>
    
    #include <algorithm>
    #include <vector>
    
    template <typename T, typename Comp>
    void insert_sorted(std::vector<T>& vec, T const& e, Comp const& comp) {
        auto const it = std::lower_bound(vec.begin(), vec.end(), e, comp);
    
        if (it != vec.end() and not comp(e, *it)) { return; }
    
        vec.insert(it, e);
    }
    
    template <typename T>
    void insert_sorted(std::vector<T>& vec, T const& e) {
        insert_sorted(vec, e, std::less<T>{});
    }
    
    int main() {
        int const array[] = { 4, 3, 6, 2, 3, 6, 8, 4 };
    
        std::vector<int> vec;
        for (int a: array) {
            insert_sorted(vec, a);
        }
    
        std::cout << vec.size() << ":";
        for (int a: vec) { std::cout << " " << a; }
        std::cout << "\n";
    
        return 0;
    }
    

    显示:5: 2 3 4 6 8

    显然,这仍然是O(n log n),但它需要更少的内存:

    • 内存越少,向量中的更多内容就在缓存中
    • 前一个元素接近其后继元素被lower_bound的二进制搜索通过几乎相同的缓存行来利用

    这应该是一个很大的改进。

    注意:已经指出在向量中间插入效率不高。当然,因为它涉及改变现有元素的一半(平均而言)。仍然benchmark表明,当唯一元素的数量很少(0.1%是我的基准)时,它可以击败当前的vector解决方案。


    Robin Hood Hashing

    更多参与,但Robin Hood哈希具有非常好的特征,thus performance。最值得注意的是,它是在单个动态数组(如vector)之上实现的,因此具有良好的内存位置。

    Rust转而使用Robin Hood哈希来实现哈希表的默认实现,对此非常满意。

    注意:快速benchmarks甚至unordered_set击败裤子和商店store,以及一个天真的开放式哈希表is 25% faster


    概率方法

    对于非常大的问题,一个众所周知的算法是HyperLogLog。它最近在Redis实施。

    它具有非常好的已用内存与错误率的比率,并且实现起来相对简单(特别是遵循Antirez的代码)。


    在问题上投入更多硬件

    请注意,这是一个令人尴尬的并行问题,因此您可以轻松地分别拥有多个线程:

    • 从流中挑选一组ID(例如:2 ** 10)
    • 将它们合并到一个线程本地的唯一ID集合(无论其实现如何)
    • 循环直到流为空
    • 最后将他们的结果合并在一起

    你可以获得接近线程数的加速(显然有一些开销)。

    注意:并非巧合,这两种方法可以很容易地用这种方法进行调整,并且都支持高效的合并。