将程序拆分为4个线程比单个线程慢

时间:2012-01-23 23:21:37

标签: c++ multithreading openmp contention

过去一周我一直在写一个光线跟踪器,并且已经达到了足够多线程有意义的程度。我已经尝试使用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

用户时间远远小于实际,这在使用多个核心时很不寻常 - 用户应该比 real 大几个核心正在同时运行。

我使用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()的实现,它同步对自身的调用以保持线程之间随机数生成的状态。我对monte carlo采样使用了很多rand(),所以我认为这就是问题所在。我摆脱了对rand的调用,用单个值替换它们,但使用多个线程仍然比较慢。 编辑:oops 结果我没有正确测试,这是随机值!

现在已经完成了这些工作,我将讨论每次调用computePixel时所做的工作的概述,希望能够找到解决方案。

在我的光线跟踪器中,我基本上有一个场景树,其中包含所有对象。当对象进行交叉测试时,computePixel期间会遍历此树,但是不会对此树或任何对象进行写入。 computePixel基本上会多次读取场景,调用对象上的方法(所有这些都是const方法),并在最后将单个值写入其自己的像素数组。这是我所知道的唯一一部分,多个线程会尝试写入同一个成员变量。在任何地方都没有同步,因为没有两个线程可以写入像素阵列中的同一个单元格。

有人可以建议可能存在某种争用的地方吗?要尝试的事情?

提前谢谢。

修改 抱歉,愚蠢的是不提供有关我系统的更多信息。

  • 编译器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;
}

根据建议,可能是树遍历导致减速。其他一些方面:一旦调用采样函数(光线的递归弹跳),就会涉及相当多的递归 - 这会导致这些问题吗?

4 个答案:

答案 0 :(得分:4)

感谢大家的建议,但经过进一步的分析,并摆脱其他因素后,随机数生成确实结果是罪魁祸首。

如上面的问题所述,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());
}

现在,您可以使用main初始化来自init_threadrand()的线程的随机状态,然后在OpenMP中使用多个线程时使用threadrand()获取随机数。

答案 1 :(得分:2)

答案是,在不知道你正在运行什么机器的情况下,并且没有真正看到你所依赖的computePixel函数的代码。

有很多因素会影响代码的性能,有一点可以想到缓存对齐。也许你的数据结构,你确实提到了一个树,并不是真正理想的缓存,并且CPU最终等待来自RAM的数据,因为它无法将内容放入缓存中。错误的缓存行对齐可能会导致类似的情况。如果CPU必须等待来自RAM的事情,则可能是线程将被上下文切换,而另一个将被运行。

你的操作系统线程调度程序是非确定性的,因此,当一个线程运行时不是一个可预测的事情,所以如果你的线程没有运行很多,或者正在争夺CPU核心,这也可能减慢速度。

线程亲和力,也起作用。线程将在特定核心上进行调度,通常将尝试将此线程保留在同一核心上。如果多于一个线程在单个核心上运行,则它们必须共享相同的核心。事情可能放缓的另一个原因。出于性能原因,一旦特定线程在核心上运行,它通常会保留在那里,除非有充分的理由将其交换到另一个核心。

还有其他一些因素,我不记得这些因素,但是,我建议对线程进行一些阅读。这是一个复杂而广泛的主题。那里有很多材料。

数据是否在最后写入,其他线程需要能够执行的数据computePixel

答案 2 :(得分:1)

一个很大的可能性是虚假分享。看起来你是按顺序计算像素,因此每个线程可能正在处理交错像素。这通常是一件非常糟糕的事情。

可能发生的是每个线程都试图在另一个线程中写入的一个像素的值旁边写入一个像素的值(它们都写入传感器阵列)。如果这两个输出值共享相同的CPU缓存行,则会强制CPU刷新处理器之间的缓存。这导致CPU之间的过多刷新,这是一个相对较慢的操作。

要解决此问题,您需要确保每个线程真正适用于独立区域。现在它看起来你划分了行(我不是肯定的,因为我不知道OMP)。这是否有效取决于行的大小 - 但每行的结尾仍然会与下一行的开头重叠(就缓存行而言)。您可能想尝试将图像分成四个块,并使每个线程在一系列连续行上工作(对于类似1..10 11..20 21..30 31..40)。这将大大减少共享。

不要担心读取常量数据。只要数据块没有被修改,每个线程就可以有效地读取这些信息。但是,要对常量数据中的任何可变数据保持谨慎。

答案 3 :(得分:1)

我只是看了Intel i3-2310M实际上没有4个内核,它有2个内核和超线程。尝试使用2个线程运行代码,看看它有帮助。我发现一般来说,当你进行大量的计算时,超线程是完全无用的,而在我的笔记本电脑上,我把它关闭了,并且我的项目的编译时间要好得多。

事实上,只需进入你的BIOS并关闭HT - 它对开发/计算机无用。