我在C中实施了一个小程序,使用蒙特卡罗方法计算PI(主要是因为个人兴趣和培训)。在实现了基本代码结构之后,我添加了一个命令行选项,允许执行线程化的计算。
我预计会有大幅加速,但我很失望。命令行概要应该是清楚的。用于近似PI的最终迭代次数是通过命令行传递的-iterations
和-threads
的数量的乘积。保留-threads
空白会将其默认为1
线程,从而导致在主线程中执行。
下面的测试总共进行了80万次迭代测试。
使用Cygwin GCC 4.5.3编译:{{1}}
使用GCC 4.6.3编译:{{1}}
在Windows上,线程版本比无线程版本快几毫秒。说实话,我期待更好的表现。在Linux上,哇!有没有搞错?为什么它甚至需要2000%的时间?当然,这在很大程度上取决于实现,所以在这里。完成命令行参数解析后的摘录并开始计算:
gcc-4 pi.c -o pi.exe -O3
gcc pi.c -lm -lpthread -O3 -o pi
实现为:
// Begin computation.
clock_t t_start, t_delta;
double pi = 0;
if (args.threads == 1) {
t_start = clock();
pi = pi_mc(args.iterations);
t_delta = clock() - t_start;
}
else {
pthread_t* threads = malloc(sizeof(pthread_t) * args.threads);
if (!threads) {
return alloc_failed();
}
struct PIThreadData* values = malloc(sizeof(struct PIThreadData) * args.threads);
if (!values) {
free(threads);
return alloc_failed();
}
t_start = clock();
for (i=0; i < args.threads; i++) {
values[i].iterations = args.iterations;
values[i].out = 0.0;
pthread_create(threads + i, NULL, pi_mc_threaded, values + i);
}
for (i=0; i < args.threads; i++) {
pthread_join(threads[i], NULL);
pi += values[i].out;
}
t_delta = clock() - t_start;
free(threads);
threads = NULL;
free(values);
values = NULL;
pi /= (double) args.threads;
}
您可以在http://pastebin.com/jptBTgwr找到完整的源代码。
这是为什么?为什么Linux上存在这种极端差异?我预计计算的时间至少是原始时间的3/4。当然,我可能只是错误地使用了pi_mc_threaded()
库。在这种情况下如何做正确的澄清将是非常好的。
答案 0 :(得分:5)
问题是在glibc的实现中,rand()
调用了__random()
,而那
long int
__random ()
{
int32_t retval;
__libc_lock_lock (lock);
(void) __random_r (&unsafe_state, &retval);
__libc_lock_unlock (lock);
return retval;
}
锁定对执行实际工作的函数__random_r
的每次调用。
因此,只要您使用rand()
有多个线程,就会使每个线程在几乎每次调用rand()
时等待其他线程。在每个线程中直接使用random_r()
和自己的缓冲区应该要快得多。
答案 1 :(得分:1)
性能和线程是一种黑色艺术。答案取决于用于执行线程的编译器和库的具体情况,内核处理它的程度等等。基本上,如果* nix的库在切换,移动对象等方面效率不高,那么线程实际上是慢点 。这是我们很多线程工作现在使用JVM或JVM类语言的原因之一。我们可以信任运行时JVM的行为 - 它的整体速度可能因平台而异,但它在该平台上是一致的。此外,您可能会遇到一些隐藏的等待/竞争条件,这些条件是由于Windows上可能未显示的时间而发现的。
如果你能够改变你的语言,可以考虑使用Scala或D. Scala是Java的演员驱动模型继承者,D是C的继承者。两种语言都显示了它们的根源 - 如果你能用C语言写的话,D应该没问题。但是,这两种语言都实现了actor模型。没有更多的线程池,没有更多的比赛条件等等!!!!!!
答案 2 :(得分:1)
为了比较,我刚刚在Windows Vista上尝试过使用Borland C ++编译的应用程序,而2线程版本的执行速度几乎是单线程的两倍。
pi.exe -iterations 20000000 -stats -threads 1
3.141167
Number of iterations: 20000000
Method: Monte Carlo
Evaluation time: 12.511000 sec
Threads: Main
pi.exe -iterations 10000000 -stats -threads 2
3.142397
Number of iterations: 20000000
Method: Monte Carlo
Evaluation time: 6.584000 sec
Threads: 2
这是针对线程安全的运行时库编译的。使用单线程库,两个版本的运行速度都是线程安全速度的两倍。
pi.exe -iterations 20000000 -stats -threads 1
3.141167
Number of iterations: 20000000
Method: Monte Carlo
Evaluation time: 6.458000 sec
Threads: Main
pi.exe -iterations 10000000 -stats -threads 2
3.141314
Number of iterations: 20000000
Method: Monte Carlo
Evaluation time: 3.978000 sec
Threads: 2
所以2线程版本的速度仍然是原来的两倍,但是带有单线程库的1线程版本实际上比线程安全库中的2线程版本更快。
看看Borland的rand实现,他们在线程安全实现中使用线程本地存储作为种子,因此它不会像glibc的锁一样对线程代码产生相同的负面影响,但是线程安全的实现显然是比单线程实现慢。
但最重要的是,编译器的rand
实现可能是两种情况下的主要性能问题。
<强>更新强>
我刚尝试使用种子的局部变量替换你的rand_01
调用Borland的rand
函数的内联实现,结果在2线程情况下的结果始终是两倍。 / p>
更新的代码如下所示:
#define MULTIPLIER 0x015a4e35L
#define INCREMENT 1
double pi_mc(int iterations) {
unsigned seed = 1;
long long inner = 0;
long long outer = 0;
int i;
for (i=0; i < iterations; i++) {
seed = MULTIPLIER * seed + INCREMENT;
double x = ((int)(seed >> 16) & 0x7fff) / (double) RAND_MAX;
seed = MULTIPLIER * seed + INCREMENT;
double y = ((int)(seed >> 16) & 0x7fff) / (double) RAND_MAX;
double d = sqrt(pow(x, 2.0) + pow(y, 2.0));
if (d <= 1.0) {
inner++;
}
else {
outer++;
}
}
return ((double) inner / (double) iterations) * 4;
}
我不知道rand
实现有多好,但至少值得尝试在Linux上查看它是否会对性能产生影响。