关于c ++:在无序元素上有效唯一

Effective unique on unordered elements

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

关于任务

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

我已经尝试过的方法向量,排序,uniqe

STD::St::排序,然后STD::唯一的。

这个方法的复杂度是O(n*log(n)),我认为在缩放方面,其他任何算法一般都不会更快。但我相信一定存在一个比这更快的代码,但具有相同的缩放属性——只有在使用常量因子时才会更快。原因是:

  • 我在向量中存储了很多相等的值,所以排序没有那么有效,向量太大了
  • 在这个方法中,我不使用实际元素和前一个元素彼此接近的信息
  • 我不需要关于这些唯一值是什么的信息,我只需要不同元素的数量
  • 设置、插入、大小

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

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

    set,emplace_提示,大小

    为了消除第二个无效性点,我不仅将每个元素放入一个集合中,而且使用函数set::emplace_提示。每次a提示将新元素放在前一个元素旁边。最后,我问了集合的大小和集合::大小

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

    问题

    你能建议任何有效的方法来计算流中不同元素(ints)的数量吗?如果知道的话,你能优化代码吗?

  • 数字之间有可测量的相关性。
  • 有些数字是重复出现的
  • 目标体系结构是现代x86或x86-64 PC处理器(带有SSE、SSE2),只有单线程代码才适用。我不喜欢使用Boost而是C++ 11。

    解决方案

    首先,感谢您的建议、耐心和理解,很抱歉我无法测试所有方法,而且我也确信有效性取决于我没有提供的Int流的细节。不过,我将与VS2013编译器共享我得到的结果。(代码是在GCC4.7下测试的,但没有经过测量。)这个主题值得花更多的时间来研究,但我有一个适合我需要的解决方案。Time statistics for different methods

    关于方法:

    • bool向量:Dieter l_cking的位向量解
    • 二进制查找:Tony D建议的方法
    • 无序集合:将所有元素简单地放入STD::unOrdEdset,然后根据IxaNeZIS提出其元素的数量
    • 向量插入排序:使用Dieter L护cking的排序向量方法
    • 集合插入:我在问题表单中描述的方法
    • 基排序:Ixanezis的建议,使用一种流行的向量排序算法
    • 设置EnPosits提示:使用STD::EMPTHORY提示,如问题表中所描述的


    由于您只处理有限范围的整数,因此可以在这里有效地使用基数排序算法,从而减少log(N)部分的复杂性。您可以在Internet的某个地方选择任何真正快速的实现。其中一些需要SSE支持,另一些需要多线程,甚至需要在GPU上运行。

    如果您有可能使用boost::unordered_setC++11std::unordered_set,那么您的第二种方法可以很容易地修改您使用的方法,也会导致线性复杂度算法。但是,如果流中至少有几百万个数字,我相信第一种方法会更快。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    #include
    #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
    "

            <<"====
    "

            <<"                Size of data:" << Size << '
    '
    ;

        std::cout
            <<"Unorderd Set
    "

            <<"============
    "
    ;
        {
            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() << '
    '
    ;
            std::cout
                <<"                      Timing:"
                << duration_cast<duration<double>>(stop - start).count()
                << '
    '
    ;
        }

        std::cout
            <<"Set
    "

            <<"===
    "
    ;
        {
            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() << '
    '
    ;
            std::cout
                <<"                      Timing:"
                << duration_cast<duration<double>>(stop - start).count()
                << '
    '
    ;
        }

        std::cout
            <<"Sorted Vector
    "

            <<"=============
    "
    ;
        {
            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() << '
    '
    ;
            std::cout
                <<"                      Timing:"
                << duration_cast<duration<double>>(stop - start).count()
                << '
    '
    ;
        }

        std::cout
            <<"BitVector
    "

            <<"=========
    "
    ;
        {
            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 << '
    '
    ;
            std::cout
                <<"                      Timing:"
                << duration_cast<duration<double>>(stop - start).count()
                << '
    '
    ;
        }

        std::cout
            <<"Sorted Data
    "

            <<"===========
    "
    ;
        {
            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() << '
    '
    ;
            std::cout
                <<"                      Timing:"
                << duration_cast<duration<double>>(stop - start).count()
                << '
    '
    ;
        }

        return 0;
    }

    用G++STD=C+++11—O3编译:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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

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


    假设32位int,最坏的情况是需要2^32位来跟踪可能看到的每个数字的已看到/未看到状态。这是40亿位,也就是5.12亿字节——5.12兆字节——对于现代台式电脑来说,这并不是什么禁忌。您基本上可以将一个字节[n/8]索引到数组中,然后使用1 << (n % 8)按位和或或来设置或测试数字的可见状态。因为您说输入中接近的数字在值上趋向于接近,所以缓存利用率应该相当好。您可以检查刚才看到的数字并绕过位数组处理。

    如果您碰巧知道在输入中要跟踪的不同数字少于2^32,那么您当然应该相应地减小位集的大小。(只需阅读您的评论"允许使用负数,但非常罕见(可能性小于1/n)。"—在这种情况下,您可以使用set表示负数,使用一半的内存表示正数)。

    (如果您担心在许多内存页上进行最后一次迭代,而这些内存页可能根本没有设置任何位,那么您可以创建一个额外的"脏页"索引(每页一位)来指导这种迭代,但是给定输入的数量,如果该输入在int的数值范围内广泛分布,则可能是无关紧要的,甚至是适得其反的。)

    按注释要求编辑/进一步解释。第一,实施:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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];
    };

    用途:

    1
    2
    3
    4
    5
    6
    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 << '
    '
    ; });

    (未测试…可能有一些小问题)

    Can you explain your answer more detailed please?

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

    I think I can bother with negative values because I can calculate the largest and smallest values for a total cost of O(n).

    嗯,也许吧,但是两次传球可能更快或更慢。根据我记录的方法,您不需要检查数据两次…您可以在第一次迭代期间准备答案。当然,如果进行初始迭代很快(例如从SSD媒体),并且您的内存足够紧,以至于只需要对实际数据范围进行实际分析,那么就开始执行。

    It also facilitates to shrink the width of the int into the correct value, so more than half of the pages will be non-empty.

    不知道你的意思。


    将当前元素与前一个元素进行比较,然后将其传递给计数方法(不管它是什么),会有帮助吗?

    还是保留一个小的/快速的缓存,比如最后10个元素,以丢弃短范围重复?

    还是分批计数(用临时计数器按100的顺序计数,然后与以前的计数合并)?


    此任务的标准数据结构是散列集,在stl中称为std::unordered_set(btw,Google的密集散列集通常性能稍好一些)

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

    像其他人建议的那样,如果你的宇宙(可能的值)不太大,你也可以使用位向量。如果你有负值,你可以直接转换成无符号,然后把它们当作真正的大数字。


    我同意尝试使用stl容器(setunordered_set…),但不幸的是,您要为它们付出代价:它们的内存稳定性和轻量级迭代器需求要求它们作为基于节点的容器来实现,并且每个元素都有很大(相对而言)开销。

    我建议采用两种方法:

  • 坚持使用vector(仅在独特物品比例较低的情况下有效)
  • 实现罗宾汉哈希
  • 使用概率方法
  • 排序向量

    对于vector方法:没有什么可以阻止您在插入时对vector进行排序,从而避免插入重复的元素。这里有一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #include <iostream>

    #include
    #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 <<"
    "
    ;

        return 0;
    }

    显示:5: 2 3 4 6 8

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

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

    这应该已经是一个很大的进步了。

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

    罗宾汉·哈辛

    更多的参与,但罗宾汉散列有很好的特点,因此性能。最值得注意的是,它是在单个动态数组(如vector)之上实现的,因此显示出良好的内存位置。

    Rust将哈希表的默认实现改为Robin Hood哈希,并对此非常满意。

    注:从快速基准来看,即使是unordered_set也比不上商店和商店,一个简单的开放地址哈希表也快25%。

    概率方法

    对于非常大的问题,一个非常著名的算法是hyperloglog。最近在Redis中实现了。

    它具有很好的内存使用率与错误率之比,并且实现起来相对简单(尤其是遵循Antirez的代码)。

    在这个问题上投入更多的硬件

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

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

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

    注意:这两种方法可以很容易地适应这种方法,这两种方法都支持有效的合并。