多线程通常也意味着您想更快地完成一些工作。 因此,首先值得回顾一下您的初始设计,并使其在单线程上更快。 然后,这是一个目标。 另外,为了在不编写精确基准的情况下比较运行时间,您需要“可见”长度的运行时间。
在我的机器上,使用“设置”
int max = 1_000_000_000;
boolean sieve[] = new boolean[max];
long sum = 0; // will be 24739512092254535 at the end
您的原始代码,
for(int i=2;i
if(!sieve[i]) {
for(int j=i*2;j
sieve[j]=true;
sum+=i;
}
持续24-28秒。 正如@Andreas帖子下方评论中所讨论的,以及稍后的内容(是的,现在我看到它被接受并且大部分讨论都已经过去了),内部循环进行了许多额外的检查(因为它始终进行一次比较,即使它实际上不会启动)。 因此,外循环可以分为两部分:首先进行筛选和求和(直到max的最后一个“未知”除数,不超过其平方根),然后对其余部分求和:
int maxunique=(int)Math.sqrt(max);
for(int i=2;i<=maxunique;i++)
if(!sieve[i]) {
for(int j=i*2;j
sieve[j]=true;
sum+=i;
}
for(int i=maxunique+1;i
if(!sieve[i])
sum+=i;
这个在我的机器上运行14-16秒。 重大收获,尚未涉及任何线程。
然后出现线程,以及if(!sieve[i]) :在计算总和时,此类检查一定不能在内部循环之前发生,因为素数低于i素数超过了i ,所以sieve[i]确实告诉它是否是素数。 因为例如,如果某个线程正在运行,例如for(int i=4;i<10001;i+=2)sieve[i]=true; ,而另一个线程同时检查sieve[10000] ,它仍然会为false ,并且10000将被误认为是质数。
第一次尝试可能是在一个线程上进行筛选(无论如何,其外循环“仅”都将转到max平方根),然后并行求和:
for(int i=2;i<=maxunique;i++)
if(!sieve[i])
for(int j=i*2;j
sieve[j]=true;
int numt=4;
Thread sumt[]=new Thread[numt];
long sums[]=new long[numt];
for(int i=0;i
long ii=i;
Thread t=sumt[i]=new Thread(new Runnable() {
public void run() {
int from=(int)Math.max(ii*max/numt,2);
int to=(int)Math.min((ii+1)*max/numt,max);
long sum=0;
for(int i=from;i
if(!sieve[i])
sum+=i;
sums[(int)ii]=sum;
}
});
t.start();
}
for(int i=0;i
sumt[i].join();
sum+=sums[i];
}
这有点整洁,所有线程(我有4个内核)检查相同数量的候选对象,并且结果更快。 有时会快一秒钟,但通常会缩短一半(〜0.4 ...〜0.8秒)。 因此,这确实不值得付出努力,筛分循环是此处最耗时的部分。
可以决定允许多余的工作,并为筛子中遇到的每个素数编号启动一个线程,即使它不是实际的素数,也尚未被剔除:
List threads=new ArrayList<>();
for(int i=2;i<=maxunique;i++)
if(!sieve[i]) {
int ii=i;
Thread t=new Thread(new Runnable() {
public void run() {
for(int j=ii*2;j
sieve[j]=true;
}
});
t.start();
threads.add(t);
}
//System.out.println(threads.size());
for(int i=0;i
threads.get(i).join();
for(int i=maxunique+1;i
if(!sieve[i])
sum+=i;
注释过的println()会告诉(在我的机器上)创建了3500-3700个线程(而如果有人在原始循环中放入一个计数器,事实证明3401是最小的,那么在单个循环中会遇到很多素数) -线程筛循环)。 尽管过冲不会造成灾难性的影响,但线程数非常高,并且增益也不算太出色,尽管它比上一次尝试更明显:运行时间为10-11秒(当然可以降低一半)通过使用并行求和循环获得更多的秒数)。
当发现循环过滤非素数时,可以通过关闭循环来解决一些冗余工作:
for(int j=ii*2;j
这实际上起到了一些作用,对我来说,运行时间为8.6-10.1秒。
由于创建3401线程并不比创建3700线程少很多,因此限制它们的数量可能是个好主意,这是向Thread告别的地方。 尽管从技术上讲可以计算出它们的数量,但是有各种内置的基础结构可以为我们做到这一点。
Executors可以帮助将线程数限制为固定数量( newFixedThreadPool() ),或者甚至更好的是,将线程数限制为可用的CPU数量( newWorkStealingPool() ):
ExecutorService es=Executors.newWorkStealingPool();
ExecutorCompletionService ecs=new ExecutorCompletionService(es);
int count=0;
for(int i=2;i<=maxunique;i++)
if(!sieve[i]) {
int ii=i;
count++;
ecs.submit(new Callable() {
public Object call() throws Exception {
// if(!sieve[ii])
for(int j=ii*2;j
sieve[j]=true;
return null;
}
});
}
System.out.println(count);
while(count-->0)
ecs.take();
es.shutdown();
long sum=0;
for(int i=2;i
if(!sieve[i])
sum+=i;
这样,它会产生与上一个(8.6-10.5s)相似的结果。 但是,对于较少的CPU数量(4个内核),条件交换会导致一定的加速(取消注释if和在/**/之间的循环中注释相同的条件),因为任务按提交顺序运行,因此大多数冗余循环可以从一开始就退出,从而使重复检查浪费时间。 那对我来说是8.5-9.3s,超过了直接线程尝试的最佳和最差时间。 但是,如果您有很高的CPU数量(根据Runtime.availableProcessors() ),我也在32位可用的超级计算节点上也运行了它),则任务将重叠更多,并且重叠的是未分类的版本(因此,该任务总是进行检查)将会更快。
而且,如果您希望以较小的速度提高可读性,则可以使用流并行化内部循环(使用Thread也是可能的,非常繁琐):
long sum=0;
for(int i=2;i<=maxunique;i++)
if(!sieve[i]) {
sum+=i;
int ii=i;
IntStream.range(1, (max-1)/i).parallel().forEach(
j -> sieve[ii+j*ii]=true);
}
for(int i=maxunique+1;i
if(!sieve[i])
sum+=i;
这非常类似于原始的优化循环对,但对我来说仍然有9.4-10.0秒的速度。 因此它比其他方法慢(约10%左右),但要简单得多。
更新:
我修复了一系列xy
创建无数个子任务困扰着我,幸运的是发现了一个倒置的设计,在这里我们不检查是否达到sqrt(max) (这是maxunique ),但是相反,我们知道如果我们完成了对某个特定数字以下的数字的筛选limit ,我们可以继续检查数字直到limit*limit ,因为在范围内( limit ... limit*limit )内仍然是质数的实际上是质数(并且我们仍然要记住,这个上限受maxunique限制)。 这样就可以并行筛选。
基本算法,仅用于检查(单线程):
ExecutorService es=Executors.newWorkStealingPool();
ExecutorCompletionService ecs=new ExecutorCompletionService<>(es);
int limit=2;
int count=0;
do {
int upper=Math.min(maxunique+1,limit*limit);
for(int i=limit;i
if(!sieve[i]) {
sum+=i;
int ii=i;
count++;
ecs.submit(new Callable() {
public Object call() throws Exception {
for(int j=ii*2;j
sieve[j]=true;
return null;
}
});
}
while(count>0) {
count--;
ecs.take();
}
limit=upper;
} while(limit<=maxunique);
es.shutdown();
for(int i=limit;i
if(!sieve[i])
sum+=i;
由于某种原因,它比原始的两个循环变量稍慢(13.8-14.5秒vs 13.7-14.0秒,最小/最大20个运行),但是无论如何,我还是对并行化感兴趣。
可能由于质数的不均匀分布,使用并行流效果不佳(我认为这只是将工作按看似相等的方式预先划分),但是基于Executor的方法效果很好:
ExecutorService es=Executors.newWorkStealingPool(); ExecutorCompletionService ecs=new ExecutorCompletionService<>(es); int limit=2; int count=0; do { int upper=Math.min(maxunique+1,limit*limit); for(int i=limit;i() { public Object call() throws Exception { for(int j=ii*2;j0) { count--; ecs.take(); } limit=upper; } while(limit<=maxunique); es.shutdown(); for(int i=limit;i
对于低CPU数量的环境,这是迄今为止最快的环境(7.4-9.0秒与“无限线程”的8.7-9.9秒和其他基于Executor的环境的8.5-9.2秒)。 但是,一开始它运行的并行任务数量很少(当limit=2 ,它仅启动两个并行循环,分别针对2和3),最重要的是,这些循环运行时间最长(步数最小) ,因此,在高CPU数量的环境中,它仅比原来的基于Executor的驱动Executor落后2.9-3.6秒和2.7-3.2秒,仅排在第二位。
当然,一个人可以在开始时实施单独的加速,明确地收集必要数量的素数以使可用核饱和,然后切换到基于limit的方法,然后无论数量多少,结果都可能胜过其他核心。 但是我认为我现在可以抵制这种诱惑。