将程序拆分为4个线程比单个线程要慢
Splitting up a program into 4 threads is slower than a single thread
我在过去的一周里一直在写一个光线跟踪器,并且已经到了一个地步,它已经做得足够多了,多线程是有意义的。我曾尝试使用OpenMP来并行化它,但用更多线程运行它实际上比用一个线程运行它慢。
仔细阅读其他类似的问题,特别是关于OpenMP的问题,有一个建议是gcc可以更好地优化串行代码。但是,使用export OMP_NUM_THREADS=1
运行下面的编译代码的速度是使用export OMP_NUM_THREADS=4
的两倍。也就是说,两次运行都是相同的编译代码。
使用time
:运行程序
> 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);
}
}
}
}
当我读到这个问题时,我以为我已经找到了答案。它讨论了gclib rand()的实现,它将调用同步到自身,以保留线程之间随机数生成的状态。我经常使用rand()进行蒙特卡罗采样,所以我认为这就是问题所在。我去掉了对rand的调用,用一个值替换它们,但使用多个线程仍然较慢编辑:哎呀原来我没有正确测试,这是随机值!
既然这些都已经解决了,我将讨论对computePixel
的每次调用所做的工作的概述,因此希望能够找到解决方案。
在我的光线跟踪器中,我基本上有一个场景树,里面有所有的对象。在computePixel
期间,当测试对象的交集时,这个树会被遍历很多次,但是,不会对这个树或任何对象进行写入。computePixel
基本上读取场景多次,调用对象上的方法(所有这些都是const方法),并在最后将单个值写入自己的像素阵列。这是我所知道的唯一一个部分,其中有多个线程将尝试向同一个成员变量写入。任何地方都没有同步,因为没有两个线程可以写入像素阵列中的同一个单元。
有人能提出可能存在某种争论的地方吗?要尝试的东西?
提前谢谢。
编辑:对不起,没有在我的系统上提供更多信息是愚蠢的。
- 编译器gcc 4.6(带有-O2优化)
- Ubuntu Linux 11.10
- OpenMP 3
- 英特尔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
中线程的随机状态,然后在OpenMP中使用多个线程时使用threadrand()
获取随机数。
答案是,如果不知道在哪台机器上运行,也不真正看到computePixel
函数的代码,这取决于它。
有很多因素可能会影响代码的性能,其中一件事就是缓存对齐。也许你的数据结构,以及你提到的树,并不是真正适合缓存的,CPU最终会等待来自RAM的数据,因为它无法将数据放入缓存。错误的缓存线对齐可能会导致类似的情况。如果CPU必须等待来自RAM的东西,那么线程很可能会被上下文切换掉,然后运行另一个线程。
您的操作系统线程调度程序是不确定的,因此,线程何时运行是不可预测的,因此如果碰巧您的线程没有大量运行,或者正在争夺CPU核心,这也可能会减慢速度。
线程亲和性,也起到一定作用。线程将被安排在特定的核心上,通常会尝试将此线程保持在同一个核心上。如果您的多个线程在单个核心上运行,则它们必须共享同一个核心。事情可能放缓的另一个原因。出于性能原因,一旦某个特定线程在一个核心上运行,它通常会保留在那里,除非有充分的理由将其交换到另一个核心。
还有一些其他因素,我已经记不清了,不过,我建议读一些关于线程的文章。这是一个复杂而广泛的课题。外面有很多材料。
最后写入的数据是其他线程需要能够执行computePixel
的数据吗?
一种很强的可能性是错误共享。看起来你是在按顺序计算像素,因此每个线程可能都在处理交错的像素。这通常是一件非常糟糕的事情
可能发生的情况是,每个线程都试图在另一个线程中写入的像素旁边写入一个像素的值(它们都写入传感器阵列)。如果这两个输出值共享相同的CPU缓存行,则会迫使CPU在处理器之间刷新缓存。这导致CPU之间的冲洗量过大,这是一个相对缓慢的操作。
要解决这个问题,您需要确保每个线程真正在一个独立的区域上工作。现在看来你是按行划分的(我不确定,因为我不知道OMP)。这是否有效取决于行的大小,但每一行的末尾仍将与下一行的开头重叠(就缓存行而言)。你可能想尝试将图像分成四个块,并让每个线程处理一系列连续的行(例如1..1011..2021..3031..40)。这将大大减少共享。
不要担心读取常量数据。只要数据块没有被修改,每个线程就可以有效地读取这些信息。但是,要警惕常量数据中的任何可变数据。
我刚刚看了一下,英特尔i3-2310M实际上没有4核,它有2核和超线程。试着只用两个线程运行代码,看看它会有什么帮助。我发现,通常情况下,当你有很多计算时,超线程是完全无用的,在我的笔记本电脑上,我关闭了它,我的项目的编译时间要好得多。
事实上,只需进入BIOS并关闭HT——这对开发/计算机器没有用处。
- 在C++中使用线程查找友好号码比单个线程花费更多时间
- 如何使用 TBB 在单个线程中运行函数
- 有人可以解释为什么使用 OpenMP 部分的运行速度比单个线程慢吗?
- 为什么向量"emplace_back"在多个线程中的行为比单个线程慢得多
- 通过创建单个线程来运行一段代码可加快执行速度
- 线程安全 std::map:锁定整个映射和单个值
- 是多个线程渲染到单个OpenGL上下文线程安全的线程
- 在C 中将多个客户端连接到Windows中的单个服务器中的多线程
- 为多个对象创建单个线程
- 在 C++98 中写入单个字符串以"线程安全"吗
- asio::strand 上的任务在单个线程上运行
- C 如果一个线程写入Bool曾经完成,则可以安全地阅读单个线程中的循环中的Bool
- C 中单个对象的线程安全容器
- 线程安全:多个线程从单个常量源读取
- 单个变量的线程安全
- 在单个链表中擦除和插入线程安全吗
- 避免单个线程中的死锁
- 期货是检查单个线程完成情况的安全方法吗
- 在C++中将所有TLS(线程本地存储)变量设置为新的单个值
- 我们是否需要每个线程多个io_service用于具有单个接受器的线程 boost::asio 服务器