Effective unique on unordered elements
我想提高我在分形分析中使用的盒计数方法的速度性能。
关于任务我有一个整数流(大约n=2^24长),我必须计算流中有多少不同的值。没有上限,允许负值(但负值的数量可能小于sqrt(n))。流中存在一个小的相关性,即实际元素可能等于或不太远。在很多情况下,我在整个范围内有很多相同的值。
我已经尝试过的方法向量,排序,uniqeSTD::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下测试的,但没有经过测量。)这个主题值得花更多的时间来研究,但我有一个适合我需要的解决方案。。
关于方法:
- bool向量:Dieter l_cking的位向量解
- 二进制查找:Tony D建议的方法
- 无序集合:将所有元素简单地放入STD::unOrdEdset,然后根据IxaNeZIS提出其元素的数量
- 向量插入排序:使用Dieter L护cking的排序向量方法
- 集合插入:我在问题表单中描述的方法
- 基排序:Ixanezis的建议,使用一种流行的向量排序算法
- 设置EnPosits提示:使用STD::EMPTHORY提示,如问题表中所描述的
由于您只处理有限范围的整数,因此可以在这里有效地使用基数排序算法,从而减少
如果您有可能使用
只是比较不同的方法(不考虑基数排序):
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位
如果您碰巧知道在输入中要跟踪的不同数字少于2^32,那么您当然应该相应地减小位集的大小。(只需阅读您的评论"允许使用负数,但非常罕见(可能性小于1/n)。"—在这种情况下,您可以使用
(如果您担心在许多内存页上进行最后一次迭代,而这些内存页可能根本没有设置任何位,那么您可以创建一个额外的"脏页"索引(每页一位)来指导这种迭代,但是给定输入的数量,如果该输入在
按注释要求编辑/进一步解释。第一,实施:
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?
号
所有这一切都是创建一个概念上只是一个
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中称为
您不需要对唯一的值进行排序,因此对于您的用例来说,
像其他人建议的那样,如果你的宇宙(可能的值)不太大,你也可以使用位向量。如果你有负值,你可以直接转换成无符号,然后把它们当作真正的大数字。
我同意尝试使用stl容器(
我建议采用两种方法:
排序向量
对于
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; } |
显示:
显然,这仍然是O(n log n),但是它需要更少的内存:
- 内存越少,缓存中的向量就越多
- 前一个接近其后继元素的元素被
lower_bound 的二进制搜索所利用,该搜索通过几乎相同的缓存线。
这应该已经是一个很大的进步了。
注:已经指出,在向量中间插入是无效的。当然,因为它涉及到改变现有元素的一半(平均)。不过基准测试表明,当唯一元素的数量很小时(0.1%是我的基准),它可以击败当前的
罗宾汉·哈辛
更多的参与,但罗宾汉散列有很好的特点,因此性能。最值得注意的是,它是在单个动态数组(如
Rust将哈希表的默认实现改为Robin Hood哈希,并对此非常满意。
注:从快速基准来看,即使是
概率方法
对于非常大的问题,一个非常著名的算法是hyperloglog。最近在Redis中实现了。
它具有很好的内存使用率与错误率之比,并且实现起来相对简单(尤其是遵循Antirez的代码)。
在这个问题上投入更多的硬件
请注意,这是一个令人尴尬的并行问题,因此您可以轻松地拥有多个线程:
- 从流中选择一束ID(例如:2**10一次)
- 将它们合并为一组线程本地唯一ID(无论其实现如何)
- 循环直到流为空
- 最后将结果合并在一起
你可以得到一个接近线程数量的加速(显然有一些开销)。
注意:这两种方法可以很容易地适应这种方法,这两种方法都支持有效的合并。