PBRT并行化研究

一、并行模型的设计

	PBRT是由Matt Pharr 和Greg Humphreys完成的一个经典的光线追踪程序,它十分注重于光线追踪算法的效率。而光线追踪算法的一个最大的难点就是其性
能问题,算法中需要对每个像素点进行光线的反向追踪,并且反向追踪时每碰到一个物体,就要考虑其表面的反射、折射等现象。其中存在大量的并行机会,因此
并行化一直是光线追踪算法的一个研究热点问题。
	光线追踪算法的并行化,有两种基本思路:一是依次对每个像素点的光线追踪进行并行,同时执行多个相邻像素点的光线追踪,这种方法并行粒度较细,并行
时负载平衡性较好,但只能串行的获取每个像素点;二是将要生成的图片分块,同时执行多个块中的不同像素点的光线追踪,这种方法并行粒度较大,可以并行获
取每个像素点,但是负载可能不够平衡。针对以上情况,PBRT设计并实现了一种集中上面两种方法的优点的并行模型。假定计算是在提供一致性共享内存的处理器
上运行的,选用Linux下的pthread线程作为共享内存并行程序的实现方法。一致性共享内存的主要思想是所有线程都可以读写一组公共的内存位置,并且一个线
程对内存的更改最终将由其他线程看到。
	如下图1所示,在所设计的模型中,光线追踪要生成的图片被划分成几个子块,每个子块对应一个子任务,所有的子任务组成n个子任务队列,每个子任务队列
分别含有m个子任务,n个线程分别从n个子任务队列中依次获取自己的子任务执行。初始时,每个子任务队列均含m个子块,作为m个子任务,当线程执行完自己子
任务队列中的所有子任务时,向子任务管理器申请从别的子任务队列中调拨新的子任务,如失败,则结束执行。

图1 线程并行执行模型

在这里插入图片描述

	并行模型充分利用前面提到的两类并行模型的优点:n和m足够大时,可以保证子块足够小,相应的子任务粒度足够细,且并行时所有线程的负载具有较好的平
衡性;同时各个线程各自独立地获取子任务,相互之间可以并行执行。
	线程和子任务管理器的相应算法的伪代码如下所示:
	线程i的执行算法:
void thread_unit_run(int i){
while(true){
pthread_mutex_lock(locki);
if(get_subtask(i)){//获取子任务进行光线追踪
pthread_mutex_unlock(locki);
Run_subtask(i);
}else{
pthread_mutex_unlock(locki);
If(sub_task_adjust(i)==false)break;//通过子任务管理器申请调拨新的子任务
}
}
}
子任务管理器执行算法:
Boolean sub_task_adjust(int i){
If(存在j){//子任务队列j中有剩余的子任务
pthread_mutex_lock(locki);
pthread_mutex_lock(lockj);
从j中取部分子任务到i中
pthread_mutex_unlock(lock);
pthread_mutex_unlock(locki);
Return true;
}else
Return false;
}

二、并行模型的实现

	更加具体一点,PBRT中的所有多核并行性都是通过使用ParallelFor()函数并行for循环表示的。在应用启动的时候,由ParallelInit()函数创建n个线程,
所有线程初始化完成后就让它进入等待状态,直到有任务唤醒它为止。然后由ParallelFor()函数把任务放到工作列表中,并和线程池中的线程一起完成任务。因
为调用ParallelFor的线程也是资源,不能让它空闲,和线程池中的线程一起工作,这样也能加快速度。最后由workerThreadFunc()函数负责让工作线程执行任
务。取任务这个操作是被互斥体包围的,取完之后,真正执行任务的时候,互斥体就会被释放。在任务执行的过程中,其他线程可以从工作列表中获取任务执行
(即多线程)。完成任务后,继续获得互斥体继续循环看看是否还有任务。

ParallelInit函数代码:
void ParallelInit() {
   CHECK_EQ(threads.size(), 0);
   int nThreads = MaxThreadIndex();
   ThreadIndex = 0;

   /*
创建一个屏障,以便在我们从该函数返回之前,确保所有工作线程都通过了对	ProfilerWorkerThreadInit()的调用。反过来,我们可以确保直到所有工作线	程	都完成了这一操作之后,才会启动分析系统。
*/
    std::shared_ptr<Barrier> barrier = std::make_shared<Barrier>(nThreads);

    //启动的工作线程比我们要做的工作线程总数少一个,因为主线程也有帮助。 
    for (int i = 0; i < nThreads - 1; ++i)
        threads.push_back(std::thread(workerThreadFunc, i + 1, barrier));

    barrier->Wait();
}
图2 ParallelFor()函数程序框图

在这里插入图片描述

图3 workerThreadFunc()函数程序框图

在这里插入图片描述

三、并行化输出问题

	并行化能明显提高性能,加快运行速度,但随之而来的如何确保串行程序并行化后的正确性成为了一个难点问题。并行程序由于自身特点,会导致即使完全并
行化正确,其输出结果也可能和串行程序不一致。影响程序并行化后的输出的问题,主要包含下面三类:
	(1)数据冲突:指多个线程在没有锁保护的情况下读写同一内存区域,导致某些线程不能得到正确的读写结果。PBRT通过采用互斥和原子内存操作来解决多
个执行线程正在访问共享的已修改数据时的同步问题。互斥是在pbrt中用std::mutex对象实现的。mutex可以用来保护对某些资源的访问,确保一次只有一个线
程可以访问它。而原子内存操作是通过多个线程正确执行这种类型的内存更新的另一种选择。原子是机器指令,可确保它们各自的内存更新将在单个事务中执行。
	(2)随机数生成器:PBRT中采用了一种称为Mersenne Twister的随机数生成器。当进行并行化后,每个像素点进行光线追踪时采用的随机数会和原来串行
时不一致,这样也就导致了并行前后输出的结果不一致。为了消除随机数生成器对输出结果的影响,方便对数据冲突的消除,对串行PBRT中生成的随机数进行了
profiling,并行后对每个像素点采用其串行时profiling的随机数,这样消除随机数生成器导致的并行前后输出结果的不一致,以使最后输出完全一致。
	(3)浮点数运算:在PBRT中,为了充分实现反走样技术,采用MSAA,对屏幕上每个像素点,计算其周围4个像素点的平均值作为该像素点的值。并行执行一
个像素点周围的4个点的光线追踪时,如果这4个点的值的计算速度不一致,则计算平均值时进行加运算的顺序不一致,由于这些值都是浮点数,这样会导致即使4
个点的值分别和串行时一致,其平均值也可能和串行时的不一致。因此,PBRT在并行化过程中,通过对每个像素周围的4个点进行编号,根据编号计算其平均值,
这样便消除了验证时对输出结果的影响。

四、性能优化

	在解决上述问题后,我们只是得到了一个初步认为正确的并行PBRT程序,但此时的并行并行PBRT程序性能并不高。伪共享是共享内存编程中一种常见的性能
瓶颈。虽然变量共享在功能上是正确的,但是由于变量的共享会导致频繁的Cache失效,也就成为了整个程序性能的瓶颈。
	在PBRT中的伪共享主要分为两类:一类是共享的变量被多个线程反复修改,每个线程修改时,为了维护Cache数据的一致性,则需要更新运行其他线程的CPU
的Cache上拥有的该数据;另一类是在内存中相邻位置的多个变量如果在同一Cache块中,当此Cache块上的数据共享时,如果被不同的线程更改时,也会产生伪共
享现象。
	针对第一类问题,PBRT的解决思路是将线程中用到的这些变量进行线程私有化,每个线程有自己的统计变量,只有当这些线程结束后,才将该线程统计的结果
添加到全局统计变量中。
	针对第二类问题,PBRT采用数据填充解决,基本思想是将位于同一Cache块上的变量分布到不同的内存区域,不再出现位于Cache块上的现象,以此来消除伪
共享。举例说明,在并行PBRT中,有一些整数变量,如lastMailboxId和curMailboxId,用于保存光线追踪的中间结果。并行时多个像素点的光线并发追踪,需要
更多空间保存光线追踪的中间结果,一个较为直观的方法就是将其扩充为数组lastMailboxId[ThreadNum]和curMailboxId[ThreadNum]。不同的线程根据自己
的线程id访问相邻的数组元素,由于这些相邻的元素很大可能会位于同一Cache块,此时就会产生伪共享问题。PBRT利用C++中数组按行存储的特性将这些保存中
间结果的数组分别填充为二维数组lastMailboxId[ThreadNum][N]和curMailboxId[ThreadNum][N],但是只使用每行的第一个元素lastMailboxId
[ThreadNum][0],并且调整N的大小使每行元素各占一个Cache块,这样便不会再发生伪共享。

参考

[1]1.4 pbrt的并行化
[2]A.6并行性

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值