过去一周我一直在编写一个光线追踪器,并且已经达到了这样一个地步,它已经足够多线程有意义了。我曾尝试使用 OpenMP 对其进行并行化处理,但使用更多线程运行它实际上比使用一个线程运行它要慢。
阅读其他类似问题,尤其是关于 OpenMP 的问题,一个建议是 gcc 可以更好地优化串行代码。但是,使用 export OMP_NUM_THREADS=1
运行下面的编译代码的速度是使用 export OMP_NUM_THREADS=4
的两倍。 IE。两次运行的编译代码相同。
用时间
运行程序:
> export OMP_NUM_THREADS=1; time ./raytracer
real 0m34.344s
user 0m34.310s
sys 0m0.008s
> export OMP_NUM_THREADS=4; time ./raytracer
real 0m53.189s
user 0m20.677s
sys 0m0.096s
用户时间比真实时间小很多,这在使用多核时不常见 - 用户应该比真实多核多同时运行。
我使用 OpenMP 并行化的代码
void Raytracer::render( Camera& cam ) {
// let the camera know to use this raytracer for probing the scene
cam.setSamplingFunc(getSamplingFunction());
int i, j;
#pragma omp parallel private(i, j)
{
// Construct a ray for each pixel.
#pragma omp for schedule(dynamic, 4)
for (i = 0; i < cam.height(); ++i) {
for (j = 0; j < cam.width(); ++j) {
cam.computePixel(i, j);
}
}
}
}
阅读时this question我以为我找到了答案。它讨论了 gclib rand() 同步调用自身以保存线程间随机数生成状态的实现。我经常使用 rand() 进行蒙特卡洛采样,所以我认为这就是问题所在。我摆脱了对 rand 的调用,将它们替换为单个值,但使用多线程仍然较慢。 编辑:糟糕原来我没有正确测试这个,这是随机值!
既然这些都已解决,我将概述每次调用 computePixel
时所执行的操作,希望能找到解决方案。
在我的光线追踪器中,我基本上有一个场景树,其中包含所有对象。这棵树在 computePixel
期间遍历了很多,当对象被测试是否相交时,但是,没有对这棵树或任何对象进行写入。 computePixel
本质上是多次读取场景,调用对象的方法(所有这些都是常量方法),最后将单个值写入它自己的像素数组。这是我所知道的唯一一个部分,多个线程将尝试写入同一个成员变量。任何地方都没有同步,因为没有两个线程可以写入像素阵列中的同一个单元格。
任何人都可以建议可能存在某种争论的地方吗?要尝试什么?
提前谢谢你。
编辑: 抱歉,没有在我的系统上提供更多信息是愚蠢的。
- 编译器 gcc 4.6(带 -O2 优化)
- Ubuntu Linux 11.10
- OpenMP 3
- Intel i3-2310M 四核 2.1Ghz(目前在我的笔记本电脑上)
计算像素代码:
class Camera {
// constructors destructors
private:
// this is the array that is being written to, but not read from.
Colour* _sensor; // allocated using new at construction.
}
void Camera::computePixel(int i, int j) const {
Colour col;
// simple code to construct appropriate ray for the pixel
Ray3D ray(/* params */);
col += _sceneSamplingFunc(ray); // calls a const method that traverses scene.
_sensor[i*_scrWidth+j] += col;
}
从建议来看,可能是树遍历导致速度变慢。其他方面:一旦调用采样函数,就会涉及到相当多的递归(光线的递归弹跳),是否会导致这些问题?
最佳答案
感谢大家的建议,但在进一步分析并排除其他影响因素后,随机数生成确实是罪魁祸首。
如上述问题所述,rand() 需要跟踪其从一次调用到下一次调用的状态。如果多个线程试图修改此状态,则会导致竞争条件,因此 glibc 中的默认实现是在每次调用时锁定,以使函数线程安全。这对性能来说很糟糕。
不幸的是,我在 stackoverflow 上看到的这个问题的解决方案都是本地的,即在调用 rand() 的范围内处理问题。相反,我提出了一个“快速而肮脏”的解决方案,任何人都可以在他们的程序中使用它来为每个线程实现独立的随机数生成,不需要同步。
我已经测试了代码,它可以工作——没有锁定,并且调用 threadrand 也没有明显的减速。请随时指出任何明显的错误。
threadrand.h
#ifndef _THREAD_RAND_H_
#define _THREAD_RAND_H_
// max number of thread states to store
const int maxThreadNum = 100;
void init_threadrand();
// requires openmp, for thread number
int threadrand();
#endif // _THREAD_RAND_H_
threadrand.cpp
#include "threadrand.h"
#include <cstdlib>
#include <boost/scoped_ptr.hpp>
#include <omp.h>
// can be replaced with array of ordinary pointers, but need to
// explicitly delete previous pointer allocations, and do null checks.
//
// Importantly, the double indirection tries to avoid putting all the
// thread states on the same cache line, which would cause cache invalidations
// to occur on other cores every time rand_r would modify the state.
// (i.e. false sharing)
// A better implementation would be to store each state in a structure
// that is the size of a cache line
static boost::scoped_ptr<unsigned int> randThreadStates[maxThreadNum];
// reinitialize the array of thread state pointers, with random
// seed values.
void init_threadrand() {
for (int i = 0; i < maxThreadNum; ++i) {
randThreadStates[i].reset(new unsigned int(std::rand()));
}
}
// requires openmp, for thread number, to index into array of states.
int threadrand() {
int i = omp_get_thread_num();
return rand_r(randThreadStates[i].get());
}
现在您可以使用 init_threadrand()
为 main
中的线程初始化随机状态,然后使用 threadrand()
获取随机数在 OpenMP 中使用多个线程时。
关于c++ - 将程序拆分为 4 个线程比单个线程慢,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8980056/