我正试图解决a coding problem in C++的问题,该问题计算素数的数量少于非负数n
的数量。
所以我首先想出了一些代码:
int countPrimes(int n) {
vector<bool> flag(n+1,1);
for(int i =2;i<n;i++)
{
if(flag[i]==1)
for(long j=i;i*j<n;j++)
flag[i*j]=0;
}
int result=0;
for(int i =2;i<n;i++)
result+=flag[i];
return result;
}
需要88毫秒并使用8.6 MB的内存。然后我将代码更改为:
int countPrimes(int n) {
// vector<bool> flag(n+1,1);
bool flag[n+1] ;
fill(flag,flag+n+1,true);
for(int i =2;i<n;i++)
{
if(flag[i]==1)
for(long j=i;i*j<n;j++)
flag[i*j]=0;
}
int result=0;
for(int i =2;i<n;i++)
result+=flag[i];
return result;
}
需要28毫秒和9.9 MB。我真的不明白为什么在运行时间和内存消耗上都存在这样的性能差距。我读过this one和that one之类的相关问题,但我仍然感到困惑。
编辑:将vector<bool>
替换为vector<char>
后,我将运行时间减少到40 ms,具有11.5 MB的内存。
答案 0 :(得分:29)
std::vector<bool>
与其他向量不同。 documentation说:
std::vector<bool>
是可能的节省空间的专业化std::vector
代表类型bool
。
这就是为什么它可能比数组占用更少的内存的原因,因为它可能表示一个字节的多个布尔值,例如位集。它还解释了性能差异,因为访问它不再那么简单了。根据文档,它甚至不必将其存储为连续数组。
答案 1 :(得分:16)
std::vector<bool>
是特例。它是专门的模板。每个值都存储在单个位中,因此需要位操作。这种内存结构紧凑,但有一些缺点(例如无法在此容器中指向bool
的指针)。
现在bool flag[n+1];
编译器通常将以与char flag[n+1];
相同的方式分配相同的内存,并且它将在堆栈而不是堆上进行分配。
现在,根据页面大小,缓存未命中和i
值,一个可以比其他更快。很难预测(对于较小的n
数组会更快,但是对于较大的n
可能会改变结果。)
作为一个有趣的实验,您可以将std::vector<bool>
更改为std::vector<char>
。在这种情况下,您将具有与数组类似的内存映射,但是它将位于堆而不是堆栈中。
答案 2 :(得分:6)
我想在已经发布的好的答案中添加一些评论。
std::vector<bool>
和std::vector<char>
之间的性能差异可能在不同的库实现和向量的不同大小之间有所不同。
例如参见那些快速的替补席:clang++ / libc++(LLVM)与g++ / libstdc++(GNU)。
此:bool flag[n+1];
声明了一个可变长度数组,该数组(由于蜂分配在堆栈中,因此具有一些性能优势)从未成为C ++标准的一部分,即使由一些(符合C99的)编译器。
提高性能的另一种方法可能是仅考虑奇数来减少计算量(和内存占用),因为除2外的所有素数都是奇数。
如果您可以隐藏不太可读的代码,则可以尝试分析以下代码段。
int countPrimes(int n)
{
if ( n < 2 )
return 0;
// Sieve starting from 3 up to n, the number of odd number between 3 and n are
int sieve_size = n / 2 - 1;
std::vector<char> sieve(sieve_size);
int result = 1; // 2 is a prime.
for (int i = 0; i < sieve_size; ++i)
{
if ( sieve[i] == 0 )
{
// It's a prime, no need to scan the vector again
++result;
// Some ugly transformations are needed, here
int prime = i * 2 + 3;
for ( int j = prime * 3, k = prime * 2; j <= n; j += k)
sieve[j / 2 - 1] = 1;
}
}
return result;
}
修改
如评论中的Peter Cordes所述,对变量j
使用无符号类型
编译器可以尽可能便宜地实现j / 2。 C的除以2的幂的除法运算(对于负红利)的舍入语义与右移具有不同的舍入语义,并且编译器并不总是传播足够的值范围证明以证明j总是非负的。
利用所有素数(过去2和3)都小于或大于6的倍数这一事实,也可以减少候选人的数量。
答案 3 :(得分:0)
使用g++-7.4.0 -g -march=native -O2 -Wall
进行编译并在Ryzen 5 1600 CPU上运行时,我得到的时序和内存使用情况与问题中提到的时序和内存使用情况不同:
vector<bool>
:0.038秒,3344 KiB内存,IPC 3.16 vector<char>
:0.048秒,12004 KiB内存,IPC 1.52 bool[N]
:0.050秒,12644 KiB内存,IPC 1.69 结论:vector<bool>
是最快的选择,因为它具有更高的IPC(每个时钟的指令数)。
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <vector>
size_t countPrimes(size_t n) {
std::vector<bool> flag(n+1,1);
//std::vector<char> flag(n+1,1);
//bool flag[n+1]; std::fill(flag,flag+n+1,true);
for(size_t i=2;i<n;i++) {
if(flag[i]==1) {
for(size_t j=i;i*j<n;j++) {
flag[i*j]=0;
}
}
}
size_t result=0;
for(size_t i=2;i<n;i++) {
result+=flag[i];
}
return result;
}
int main() {
{
const rlim_t kStackSize = 16*1024*1024;
struct rlimit rl;
int result = getrlimit(RLIMIT_STACK, &rl);
if(result != 0) abort();
if(rl.rlim_cur < kStackSize) {
rl.rlim_cur = kStackSize;
result = setrlimit(RLIMIT_STACK, &rl);
if(result != 0) abort();
}
}
printf("%zu\n", countPrimes(10e6));
return 0;
}