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

Splitting up a program into 4 threads is slower than a single thread

本文关键字:线程 单个 4个 程序 拆分      更新时间:2023-10-16

我在过去的一周里一直在写一个光线跟踪器,并且已经到了一个地步,它已经做得足够多了,多线程是有意义的。我曾尝试使用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——这对开发/计算机器没有用处。