低于20亿的质量 - 使用STD ::列表阻碍性能

prime number below 2 billion - usage of std::list hinders performance

本文关键字:列表 性能 STD 使用 20亿 低于      更新时间:2023-10-16

问题语句是在时间范围内找到低于20亿低于20亿的质量。20秒我遵循以下方法。

  1. 将数字n与数字>列表k(k< sqrt(n)) - 花了20秒

  2. 将数字n划分为 SQRT(n)下方的质量列表>。

有人可以帮助我理解为什么第二种方法要长期花费很长时间,即使我们没有将分区减少50%(大约)?还是我选择了错误的数据结构?

方法1:

#include <iostream>
#include<list>
#include <ctime>
using namespace std;
list<long long> primeno;
void ListPrimeNumber();
int main()
{
    clock_t time_req = clock();
    ListPrimeNumber();
    time_req = clock() - time_req;
    cout << "time taken " << static_cast<float>(time_req) / CLOCKS_PER_SEC << " seconds" << endl;
    return 0;
}
void check_prime(int i);
void ListPrimeNumber()
{
    primeno.push_back(2);
    primeno.push_back(3);
    primeno.push_back(5);
    for (long long i = 6; i <= 20000000; i++)
    {
        check_prime(i);
    }
}
void check_prime(int i)
{
    try
    {
        int j = 0;
        int limit = sqrt(i);
        for (j = 2 ; j <= limit;j++)
        {
            if(i % j == 0)
            {
                break;
            }
        }
        if( j > limit)
        {
            primeno.push_back(i);
        }
    }
    catch (exception ex)
    {
        std::cout << "Message";
    }
}

方法2:

#include <iostream>
#include<list>
#include <ctime>
using namespace std;
list<long long> primeno;
int noofdiv = 0;
void ListPrimeNumber();
int main()
{
    clock_t time_req = clock();
    ListPrimeNumber();
    time_req = clock() - time_req;
    cout << "time taken " << static_cast<float>(time_req) / CLOCKS_PER_SEC << " seconds" << endl;
    cout << "No of divisions : " << noofdiv;
    return 0;
}
void check_prime(int i);
void ListPrimeNumber()
{
    primeno.push_back(2);
    primeno.push_back(3);
    primeno.push_back(5);
    for (long long i = 6; i <= 10000; i++)
    {
        check_prime(i);
    }
}
void check_prime(int i)
{
    try
    {
        int limit = sqrt(i);
        for (int iter : primeno)
        {
            noofdiv++;
            if (iter <= limit && (i%iter) == 0)
            {
                break;
            }
            else if (iter > limit)
            {
                primeno.push_back(i);
                break;
            }
        }
    }
    catch (exception ex)
    {
        std::cout << "Message";
    }
}

您的第二个示例需要更长的原因是您要迭代std::list

C 中的std::list是一个链接列表,这意味着它不使用连续的内存。这很糟糕,因为要迭代列表,您必须以无法预测的方式(到CPU/Prefetcher)以(到CPU/Prefetcher)的方式跳到节点。另外,您很可能只"使用"每个Cacheline的几个字节。公羊很慢。从RAM获取字节比从L1获取 lot 更长。这些天CPU很快,因此您的程序大部分时间都不做任何事情并等待内存到达。

改用std::vector。它一个又一个一个接一个地存储所有值,并且迭代非常便宜。由于您在内存中迭代而无需跳跃,因此您使用的是 full cacheline,您的预摘要将能够在需要之前获取更多页面,因为您可以预测内存的访问。

包括Bjarne Stroustrup在内的许多人证明,std::vector在很多情况下都比std::list快,即使在std::list在理论上具有"更好的复杂性"(随机插入,删除,...)因为缓存很有帮助。因此,始终将std::vector用作默认值。如果您思考在您的情况下,链接列表将更快, MEATUR IT,并且很惊讶 - 大多数时间 - std::vector占主导地位。

编辑:正如其他人所指出的那样,您查找素数的方法不是很有效。我只是玩了一下,并使用bitset实现了eRatosthenes的筛子。

constexpr int max_prime = 1000000000;
std::bitset<max_prime> *bitset = new std::bitset<max_prime>{};
// Note: Bit SET means NO prime
bitset->set(0);
bitset->set(1);
for(int i = 4; i < max_prime ; i += 2)
    bitset->set(i); // set all even numbers
int max = sqrt(max_prime);
for(int i = 3; i < max; i += 2) { // No point testing even numbers as they can't be prime
    if(!bitset->test(i)) { // If i is prime
        for(int j = i * 2; j < no_primes; j += i)
            bitset->set(j); // set all multiples of i to non-prime
    }
}

这需要 4.2和4.5秒之间我的机器上的十亿(1,000,000,000)。您的方法甚至花了1亿美元。我大约两分钟后取消了10亿次搜索。

比较1亿:

time taken:                63.515 seconds
time taken bitset:         1.874 seconds
No of divisions :          1975961174
No of primes found:        5761455
No of primes found bitset: 5761455

我不是数学家,所以我很确定仍然有进一步优化的方法,我只对数字进行优化。

首先要做的是确保您正在启用优化。C 标准库模板类别在生成大量函数调用时,使用不优化的代码往往会表现较差。优化器插入了大多数这些功能调用,使它们便宜得多。

std::list是链接列表。在您要随机插入或删除元素的情况下,它最有用(即不是从末端)。

对于仅附加到列表末尾的情况, std::list有以下问题:

  • 通过列表进行迭代相对昂贵,因为代码必须遵循节点指针,然后检索数据
  • 列表使用了更多的内存,除了实际数据外,每个元素还需要指向上一个和下一个节点的指针。在64位系统上,对于int的列表
  • ,它等于每个元素的20个字节,而不是4个字节。
  • 由于列表中的元素在内存中不连续,因此编译器无法执行那么多的SIMD优化,您将遭受CPU缓存的更多障碍

std::vector将解决上述所有内容,因为它的内存是连续的,并且通过它迭代基本上只是一种增加数组索引的情况。您确实需要确保在开始时以足够大的值在向量上调用reserve,以便将矢量附加到矢量不会导致整个数组复制到新的较大较大数组中。

比上述更大的优化是使用eratosthenes的筛子来计算您的素数。由于生成此灯需要随机删除(取决于您的确切实现)std::list的性能可能比std::vector更好,尽管即使在这种情况下,std::list的开销也可能不会超过其成本。

iDeOne (几乎没有表面更改的OP代码)的测试完全矛盾在这个问题中:

/* check_prime__list:
              time taken       No of divisions         No of primes
    10M:    0.873 seconds        286144936                664579
    20M:    2.169 seconds        721544444               1270607   */
    2B:       projected time: at least 16 minutes but likely much more   (*)
/* check_prime__nums:
              time taken       No of divisions         No of primes
    10M:    4.650 seconds       1746210131                664579
    20M:   12.585 seconds       4677014576               1270607   */
    2B:       projected time: at least 3 hours but likely much more      (*)

我还将分区数量计数器数的类型更改为long int,因为它围绕数据类型限制了。因此,他们可能一直在误解

但是运行时间没有受到影响。壁时钟是壁钟。

最有可能的解释似乎是OP的草率测试,并且在每个测试案例中使用了不同的值。


(*)时间投影是通过生长分析的经验顺序进行的:

100**1.32 * 2.169 / 60 = 15.8
100**1.45 * 12.585 / 3600 = 2.8

在给定尺寸范围内测量的经验增长顺序对于列表算法, n 1.32 vs.> 1.45 用于所有数字测试。从理论上的复杂性中,这完全可以预期,因为在 log n 上,所有数字的数量少于所有数字,而总复杂性为 o。(n 1.5 /log n) vs. o(n 1.5 。击败实际算法优势的任何实施差异也极不可能。