[英]C++ Optimizing this Algorithm
看了一些 Terence Tao 的视频后,我想尝试在 c++ 代码中实现算法,以查找到 n 的所有素数。 在我的第一个版本中,我只是简单地测试了从 2 到 n 的每个 integer 以查看它们是否可以被从 2 到 sqrt(n) 的任何值整除,我让程序在约 52 秒内找到 1-10,000,000 之间的素数。
尝试优化程序,并实施我现在知道的埃拉托色尼筛法,我认为该任务的完成速度会比 51 秒快得多,但遗憾的是,事实并非如此。 即使达到 1,000,000 也需要相当长的时间(虽然没有计时)
#include <iostream>
#include <vector>
using namespace std;
void main()
{
vector<int> tosieve = {};
for (int i = 2; i < 1000001; i++)
{
tosieve.push_back(i);
}
for (int j = 0; j < tosieve.size(); j++)
{
for (int k = j + 1; k < tosieve.size(); k++)
{
if (tosieve[k] % tosieve[j] == 0)
{
tosieve.erase(tosieve.begin() + k);
}
}
}
//for (int f = 0; f < tosieve.size(); f++)
//{
// cout << (tosieve[f]) << endl;
//}
cout << (tosieve.size()) << endl;
system("pause");
}
是向量的重复引用还是什么? 为什么这么慢? 即使我完全忽略了某些东西(可能是完全的初学者:我)我认为用这种可怕的低效方法找到 2 到 1,000,000 之间的素数会比我从 2 到 10,000,000 找到它们的原始方法更快。
希望有人对此有明确的答案 - 希望我可以在将来使用大量递归优化程序时使用收集到的任何知识。
问题是'erase'将向量中的每个元素向下移动一个,这意味着它是一个O(n)操作。
有三种替代选择:
1)只需将已删除的元素标记为“空”(例如,将它们设为0)。 这意味着未来的通行证必须通过那些空头位置,但这并不昂贵。
2)创建一个新的载体,和push_back
新的值到那里。
3)使用std :: remove_if:这将向下移动元素,但是在一次通过中执行它会更有效。 如果你使用std :: remove_if,那么你必须记住它不会调整向量本身的大小。
大多数vector
运算(包括erase()
具有O(n)
线性时间复杂度。
既然你有大小两个循环10^6
,和vector
大小的10^6
,你的算法执行多达10^18
的操作。
如此大的N
Qubic算法将花费大量的时间。
对于二次算法, N
= 10^6
甚至足够大。
请仔细阅读有关Eratosthenes筛选的信息 。 事实上,完全搜索和Eratosthenes算法的Sieve花费了相同的时间,这意味着你完成了第二个错误。
我在这里看到两个性能问题:
首先, push_back()
必须偶尔重新分配动态内存块。 使用reserve()
:
vector<int> tosieve = {};
tosieve.resreve(1000001);
for (int i = 2; i < 1000001; i++)
{
tosieve.push_back(i);
}
第二个erase()
必须将所有元素移动到您尝试移除的元素后面。 您将元素设置为0,然后在向量上运行向量(未经测试的代码):
for (auto& x : tosieve) {
for (auto y = tosieve.begin(); *y < x; ++y) // this check works only in
// the case of an ordered vector
if (y != 0 && x % y == 0) x = 0;
}
{ // this block will make sure, that sieved will be released afterwards
auto sieved = vector<int>{};
for(auto x : tosieve)
sieved.push_back(x);
swap(tosieve, sieved);
} // the large memory block is released now, just keep the sieved elements.
考虑使用标准算法而不是手写循环。 它们可以帮助您说明您的意图。 在这种情况下,我看到std::transform()
用于筛子的外部循环, std::any_of()
用于内部循环, std::generate_n()
用于在开始时填充tosieve
和std::copy_if()
用于填充sieved
(未经测试的代码):
vector<int> tosieve = {};
tosieve.resreve(1000001);
generate_n(back_inserter(tosieve), 1000001, []() -> int {
static int i = 2; return i++;
});
transform(begin(tosieve), end(tosieve), begin(tosieve), [](int i) -> int {
return any_of(begin(tosieve), begin(tosieve) + i - 2,
[&i](int j) -> bool {
return j != 0 && i % j == 0;
}) ? 0 : i;
});
swap(tosieve, [&tosieve]() -> vector<int> {
auto sieved = vector<int>{};
copy_if(begin(tosieve), end(tosieve), back_inserter(sieved),
[](int i) -> bool { return i != 0; });
return sieved;
});
编辑:
完成这项工作的另一种方法:
vector<int> tosieve = {};
tosieve.resreve(1000001);
generate_n(back_inserter(tosieve), 1000001, []() -> int {
static int i = 2; return i++;
});
swap(tosieve, [&tosieve]() -> vector<int> {
auto sieved = vector<int>{};
copy_if(begin(tosieve), end(tosieve), back_inserter(sieved),
[](int i) -> bool {
return !any_of(begin(tosieve), begin(tosieve) + i - 2,
[&i](int j) -> bool {
return i % j == 0;
});
});
return sieved;
});
现在不是标记元素,而是我们不想在之后复制,而只是直接复制我们想要复制的元素。 这不仅比上述建议更快,而且更好地陈述了意图。
你有非常有趣的任务。 谢谢!
很高兴我从头开始实施我自己的解决方案。
我创建了 3 个独立的(独立的)函数,全部基于Sieve of Eratosthenes 。 这 3 个版本的复杂性和速度各不相同。
快速说明一下,我最简单(最慢)的版本仅在0.025 sec
(即 25 毫秒)内找到所有低于您期望的10'000'000
限制的素数。
我还测试了所有 3 个版本以找到低于2^32
的素数( 4'294'967'296
),“简单”版本在 47 秒内解决,“中级”版本在 30 秒内解决,“高级”版本在 12 秒内解决秒。 所以在短短 12 秒内,它找到了所有低于 40 亿的素数(有 203'280'221 个这样的素数低于 2^32,请参阅OEIS 序列)!!!
为简单起见,我将仅详细描述 3 中的简单版本。这是代码:
template <typename T>
std::vector<T> GenPrimes_SieveOfEratosthenes(size_t end) {
// https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes
if (end <= 2)
return {};
size_t const cnt = end >> 1;
std::vector<u8> composites((cnt + 7) / 8);
auto Get = [&](size_t i){ return bool((composites[i / 8] >> (i % 8)) & 1); };
auto Set = [&](size_t i){ composites[i / 8] |= u8(1) << (i % 8); };
std::vector<T> primes = {2};
size_t i = 0;
for (i = 1; i < cnt; ++i) {
if (Get(i))
continue;
size_t const p = 2 * i + 1, start = (p * p) >> 1;
primes.push_back(p);
if (start >= cnt)
break;
for (size_t j = start; j < cnt; j += p)
Set(j);
}
for (i = i + 1; i < cnt; ++i)
if (!Get(i))
primes.push_back(2 * i + 1);
return primes;
}
这段代码实现了最简单但快速的求素数算法,称为埃拉托色尼筛法。 作为速度和 memory 的小优化,我只搜索奇数。 这种奇数优化使我能够存储 2 倍的 memory 并减少 2 倍的步数,因此将速度和 memory 的消耗量提高了 2 倍。
算法很简单,我们分配位数组,这个数组在 position K 如果 K 是合数则为 1,如果 K 可能是素数则为 0。 最后,数组中的所有 0 位表示确定的素数(肯定是素数)。 同样由于奇数优化,这个位数组只存储奇数,所以第 K 位实际上是一个数字2 * K + 1
。
然后从左到右我们在这个位数组上 go 如果我们在 position K 处遇到 0 位,那么这意味着我们找到了一个素数P = 2 * K + 1
现在从 Z4757FE07FD492A8BE3 标记开始 * PED60EA6A760D6/2 (第 P 位为 1。这意味着我们将所有大于 P*P 的数字标记为合数,因为它们可以被 P 整除。
我们只在 P * P 变得大于或等于我们的极限 End 之前执行此过程(我们发现所有素数 < End)。 这个限制保证在达到它之后,数组内的所有零位都表示素数。
第二版代码只对这个简单版做了一个优化,它使所有的多核(多线程)。 但是这种唯一的优化使代码变得更大更复杂。 基本上,它将整个位范围切成所有内核,以便它们将位并行写入 memory。
我将只解释我的第三个高级版本,它是 3 个版本中最复杂的。 它不仅进行多线程优化,还进行所谓的Primorial优化。
什么是Primorial ,它是第一个最小素数的乘积,例如我取 primorial 2 * 3 * 5 * 7 = 210
。
我们可以看到,任何原初都通过这个原初的模将无限范围的整数分裂成轮子。 例如,原始 210 分成范围 [0; 210), [210; 2 210), [2 210; 3*210) 等。
现在很容易在数学上证明,在所有原始范围内,我们可以将相同位置的数字标记为复数,确切地说,我们可以将所有是 2 或 3 或 5 或 7 的倍数的数字标记为合数。
我们可以看到,在 210 个余数中,有 162 个肯定是合余数,而可能只有 48 个余数是质数。
因此,我们只需检查整个搜索空间的 48/210=22.8% 的素数就足够了。 这种搜索空间的减少使任务速度提高了 4 倍以上,并且 memory 消耗减少了 4 倍。
可以看出,我的第一个简单版本实际上是由于仅奇数优化,实际上是使用 Primorial 等于 2 优化。 是的,如果我们采用原始 2 而不是原始 210,那么我们获得的正是第一个版本(简单)算法。
我所有的 3 个版本都经过了正确性和速度测试。 尽管仍然可以保留一些小错误。 注意。 但建议不要在生产中直接使用我的代码,除非经过彻底测试。
所有 3 个版本都通过重复使用彼此的答案来测试正确性。 我通过提供从 0 到 2^18 的所有限制( end
值)来彻底测试正确性。 这样做需要一些时间。
请参阅 main() function 以了解如何使用我的函数。
源代码在这里。 由于 StackOverflow 限制每个帖子 30K 符号,我不能在这里内联源代码,因为它的大小几乎是 30K,加上上面的英文帖子需要超过 30K。 所以我在单独的 Github Gist 服务器上提供源代码,链接如下。 请注意, Try it online!
上面的链接也包含完整的源代码,但由于 GodBolt 的运行时间限制为 3 秒,我将搜索限制 2^32 减少到更小的一个。
Output:
10M time 'Simple' 0.024 sec
Time 2^32 'Simple' 46.924 sec, number of primes 203280221
Time 2^32 'Intermediate' 30.999 sec
Time 2^32 'Advanced' 11.359 sec
All checked till 0
All checked till 5000
All checked till 10000
All checked till 15000
All checked till 20000
All checked till 25000
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.