繁体   English   中英

C++ 优化此算法

[英]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 找到它们的原始方法更快。

希望有人对此有明确的答案 - 希望我可以在将来使用大量递归优化程序时使用收集到的任何知识。

问题是'era​​se'将向量中的每个元素向下移动一个,这意味着它是一个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()用于在开始时填充tosievestd::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 减少到更小的一个。

Github 要点代码

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM