65.9K
CodeProject 正在变化。 阅读更多。
Home

通过 Intel TBB 进行并行编程基础

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.39/5 (27投票s)

2010年6月13日

CPOL

12分钟阅读

viewsIcon

80432

一篇旨在介绍和扩展 Intel Threading Building Blocks 多线程库的文章。

引言

Intel TBB 由 Intel 在其通过核心而非千兆赫兹来提升系统性能的运动中发布。它最初是为了公司内部使用而设计的,但他们发布了试用版下载和开源版本。它将能够扩展成为那些从事并行编程和多核编程的人的强大工具。请注意,一些强大的示例代码可以从 http://www.threadingbuildingblocks.org/ 下载。

前言

有时,并发和并行这两个术语会被互换使用。但一个多线程应用程序可以有线程在单个物理芯片上同时执行。然而,只有当这些并发线程在多核芯片的独立核心上执行时,才能实现并行。当我们编写一个使用并发的算法时,我们最重要的设计选择应该是弄清楚如何将原始问题划分为独立的子部分。由于大多数程序花费大量执行时间在迭代范围内的循环上,我们应该标准化 Intel TBB 的使用。例如,使用 C++ STL 迭代器的循环遍历数据集合的内容。在并行化这些循环之前,必须确定这个迭代空间。Intel TBB 是一个基于 C++ 模板的库,用于循环级并行,它侧重于定义任务而不是显式线程。TBB 的组件包括通用并行算法、并发容器、低级同步原语和任务调度器。在撰写本文时,TBB 3.0 是最新版本。使用 TBB 的程序员可以通过将迭代的块视为任务来并行化迭代的执行,并允许 TBB 任务调度器确定任务的大小、使用的线程数、将任务分配给这些线程以及这些线程的执行调度方式。任务调度器将优先处理最近在核心中的任务,以期最好地利用可能包含任务数据的缓存。在思考任务而非线程时,我们应该关注性能、不同子问题之间的负载均衡、数据共享、控制以及子问题之间的数据依赖性。

任务调度器利用任务窃取机制来进行执行的负载均衡。此外,将程序分解为独立的函数块并将每个块分配给一个单独的线程通常不能很好地扩展,因为通常函数块的数量是固定的。相比之下,Threading Building Blocks 强调数据并行编程,使多个线程能够最有效地协同工作。数据并行编程通过将数据集划分为更小的部分,能够很好地扩展到更多的处理器。将函数分解为更小的代码块会限制可伸缩性,并可能导致性能下降,因为程序中的方法数量很少是动态的。采用数据并行方法可以通过将迭代空间分解为独立的工作单元来解决这个问题。任何使用库的算法模板或任务调度器的线程都必须有一个已初始化的 tbb::task_scheduler_init 对象。一个线程可以同时初始化多个这样的对象。当所有 task_scheduler_init 对象终止时,任务调度器关闭。默认情况下,task_scheduler_init 的构造函数进行初始化,析构函数进行终止。因此,在 main( ) 中声明一个 task_scheduler_init,如下面的示例所示,可以同时启动和关闭调度器。

#include "tbb/task_scheduler_init.h"
using namespace tbb;
int main( ) {
task_scheduler_init init;
...
return 0;
}

parallel_for 模板并行化包含在 for 循环中的任务。该模板需要两个参数:一个用于迭代的范围类型和一个执行范围或子范围的迭代的体类型。范围类必须定义复制构造函数和析构函数,is_empty() 方法(如果范围为空则返回 TRUE)和 is_divisible() 方法(如果范围可以被分割则返回 TRUE),以及一个用于将范围分成两半的分割构造函数。可以使用分区器类对象来启发式地找到应分配的最少迭代次数。TBB 库包含两种预定义的范围类型:blocked_rangeblocked_range2D。这些范围分别用于一维和二维范围。体类必须定义复制构造函数和析构函数以及 operator()operator() 将包含原始串行循环的副本,该副本已修改为在来自范围类型的子范围值上运行。parallel_reduce 模板将迭代一个范围并将每个任务计算的部分结果组合成一个最终(归约)值。parallel_reduce 的范围类型的要求与 parallel_for 相同。体类型需要一个分割构造函数和一个 join 方法。体中的分割构造函数复制运行循环体所需的只读数据,并分配归约操作的标识元素,该元素初始化归约变量。join 方法根据使用的归约操作组合任务的部分结果。那么,让我们来看一个例子。

并行求和的简单示例

并行求和操作可以与任何满足结合律和交换律的组合操作一起工作。例如,乘法、最大值、最小值以及某些逻辑操作都是合适的。在串行程序中,操作的交换律并不重要,因为串行执行组合元素的顺序每次都相同。尽管如此,为了有机会并行执行计算,我们依赖于操作的这个属性,因为并行执行不会对独立计算的顺序施加固定的调度。

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctype.h>
#include "parallel_reduce.h"
#include "blocked_range.h"
#include "task_scheduler_init.h"
#include "tick_count.h"

using namespace tbb;

// Uncomment the line below to enable the auto_partitioner
#define AUTO_GRAIN

struct Sum {
    float value;
    Sum() : value(0) {}
    Sum( Sum& s, split ) {value = 0;}
    void operator()( const blocked_range<float*>& range ) {
        float temp = value;
        for( float* a=range.begin(); a!=range.end(); ++a ) {
            temp += *a;
        }
        value = temp;
    }
    void join( Sum& rhs ) {value += rhs.value;}
};

float ParallelSum( float array[], size_t n ) {
    Sum total;
#ifndef AUTO_GRAIN
    parallel_reduce( blocked_range<float*>( array, array+n, 1000 ), 
                     total );
#else /* AUTO_GRAIN */
    parallel_reduce( blocked_range<float*>( array, array+n ), 
                     total, auto_partitioner() );
#endif /* AUTO_GRAIN */
    return total.value;
}

//! Problem size
const int N = 1000000;

//! Number of threads to use.
static int MaxThreads = 4;

//! If true, print out bits of the array
static bool VerboseFlag = false;

//! Parse the command line.
static void ParseCommandLine( int argc, char* argv[] ) {
    int i = 1;
    if( i<argc )="=0" verboseflag="true;"><argc>
	<argc maxthreads="strtol(argv[i++],0,0);" i="0;">< N; ++i ) {
      input[i] = (float)(rand() % 10);
    }

    if( VerboseFlag ) {
      printf(" Input: ");
      for ( size_t i = 0; i < 7; ++i ) {
        printf("%7.2f ", input[i]);
      }
      printf("...\n");
    }

    // Try different numbers of threads
    for( int p=1; p<=MaxThreads; ++p ) {

        task_scheduler_init init(p);

        tick_count t0 = tick_count::now();
        float output = ParallelSum(input, N);
        tick_count t1 = tick_count::now();
        
        if( VerboseFlag ) {
          printf("Output: %7.2f\n", output);
        }
        printf("%2d threads: %.3f msec\n", p, (t1-t0).seconds()*1000);
    }
    return 0;
}

此 Intel 代码是通过 Intel C++ Professional Compiler 编译的,该编译器安装后会集成到 Microsoft Visual Studio 2008 或 2010 中。安装完成后,就可以安装性能库 Intel TBB 3.0。随附的文档将指导您配置环境,以便编译器可以引用包含的头文件和 Lib 文件。请注意结果

 1 threads: 6.513 msec
 2 threads: 3.333 msec
 3 threads: 3.496 msec
 4 threads: 3.345 msec 

事实证明,通过 Intel TBB 并行执行 4 个线程所需的时间与执行一个线程所需的时间相同。另一个性能优势是 Intel TBB 库如何设法保持线程安全。在确定体在并行运行时是安全的之后,并行化循环最简单的方法是将循环的大小(上范围 – 下范围 – 假设迭代按升序排列)除以处理器数量。然而,这种方法最好留给任务调度器。简单的除法过程很容易导致处理器使用效率低下。

现在考虑一个复合整数,复合意味着该整数是奇数或偶数。我们知道质数可以被自身和 1 整除。但是任何复合整数都可以分解为素数的因子。因此,计算并定位整数范围内质数的数量将涉及持续的计算过程。请看下面的代码

#include <cctype>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <math.h>
#include <cassert>
#include "parallel_reduce.h"
#include "task_scheduler_init.h"
#include "tick_count.h"

using namespace std;
using namespace tbb;

typedef unsigned long Number;

static bool PrintPrimes = false;

static Number GrainSize = 1000;

class Multiples {
    inline Number strike( Number start, 
                          Number limit, 
                          Number stride ) {
        
bool* is_composite = my_is_composite;

assert( stride>=2 );

for(start =0;start < limit; start+="stride" i+="2" +="m&1;" i="3;"
 n_factor="0;"
 my_factor="new" my_striker="new" 
my_is_composite="new"
 m="Number(sqrt(double(n)));" 
is_composite[start]="true;" k="0;"
is_composite="my_is_composite;" my_factor[n_factor++]="i;" 
my_striker[n_factor]="strike(" 

正如我们所期望的,并行度越高,计算输出结果的速度就越快。

#primes from [2..100000000] = 5761455 (1.10 sec with serial code)
#primes from [2..100000000] = 5761455 (1.10 sec with 1-way parallelism)
#primes from [2..100000000] = 5761455 (0.59 sec with 2-way parallelism)
#primes from [2..100000000] = 5761455 (0.57 sec with 3-way parallelism)
#primes from [2..100000000] = 5761455 (0.60 sec with 4-way parallelism)

我们现在在哪里?

到目前为止,我们已经注意到,有时计算的一部分涉及计算大量值的总和。如果可用多个处理器,则无需按顺序将值相加,而是可以对集合进行分区,并在不同处理器上同时计算子集的总和。然后将部分和组合起来得到最终答案。因此,使用多个处理器并行计算可以让我们更快地获得解决方案。此外,如果每个处理器都有自己的内存,则对处理器之间的数据进行分区可以处理比单个处理器能够处理的更大的问题。所以,如果我们想形成一个从 1 开始的整数集,并且从 1/1 +n.. 开始,我们可以将它们相加,但序列不会在数字 2 处结束。该序列将是无穷多的分数,随着数量的增加而趋近于 2,但从不等于 2。但我们添加的值越多,它们就越小。

这是一个基于 parallel_forblocked_range 的简单地震波模拟(波传播)示例。程序的主体通过一个核心循环遍历模拟地震波,该循环设置扰动源的脉冲,执行应力更新和速度的两个棘手计算,最后清理模拟的边缘。地震波是穿过地球核心的声音波,可能由于地震或爆炸的结果。因为声波形成一系列波(类似于火车在到达时声音变大;离开时,声音减小直到听不见。这就是声波链的终结)。在早期,科学家们会炸一些炸药。使用一种设备(原理与地震仪类似)来跟踪声波的方向。一个方向意味着在更稠密的介质上发生衍射,他们通过跟随这些波直到衍射导致方向改变来学习。过了一段时间,他们会在图上发现一种改变方向的方式,挖掘该区域,也许会找到盐。有时他们甚至会找到石油,但他们的目的是寻找黄金。类似地,当一个粒子被位移时,某种力使其位移。它沿着矢量路径移动,这意味着位移是通过方向和大小来衡量的。追踪路径,然后在网格上绘制该路径的每个点将揭示方向改变发生的位置。

我介绍这个程序是因为它体现了 DNA 测序等领域使用的基本概念,在计算机动画中,渲染是应用动画文件中的信息(如光照、纹理和着色)到 3D 模型以生成构成电影帧的 2D 图像的步骤。并行计算对于生成电影所需帧数(每秒 24 帧)至关重要。现在请注意下图中的涟漪效应。

wave.jpg

地震波传播

//#define _CRT_SECURE_NO_DEPRECATE
#define VIDEO_WINMAIN_ARGS
#include "../../common/gui/video.h"
#include <cstring>
#include <cstdlib>
#include <math.h
#include <cctype>
#include <cstdio>
#include <cassert>
#include "tbb/task_scheduler_init.h"
#include "tbb/blocked_range.h"
#include "tbb/parallel_for.h"
#include "tbb/tick_count.h"

using namespace std;

#ifdef _MSC_VER
// warning C4068: unknown pragma
#pragma warning(disable: 4068)
#endif

#define DEFAULT_NUMBER_OF_FRAMES 100
int number_of_frames = -1;
const size_t MAX_WIDTH = 1024;
const size_t MAX_HEIGHT = 512;

int UniverseHeight=MAX_HEIGHT;
int UniverseWidth=MAX_WIDTH;

typedef float value;

//! Velocity at each grid point
static value V[MAX_HEIGHT][MAX_WIDTH];

//! Horizontal stress
static value S[MAX_HEIGHT][MAX_WIDTH];

//! Vertical stress
static value T[MAX_HEIGHT][MAX_WIDTH];

//! Coefficient related to modulus
static value M[MAX_HEIGHT][MAX_WIDTH];

//! Coefficient related to lightness
static value L[MAX_HEIGHT][MAX_WIDTH];

//! Damping coefficients
static value D[MAX_HEIGHT][MAX_WIDTH];

static tbb::affinity_partitioner Affinity;

enum MaterialType {
    WATER=0,
    SANDSTONE=1,
    SHALE=2
};

//! Values are MaterialType, cast to an unsigned char to save space.
static unsigned char Material[MAX_HEIGHT][MAX_WIDTH];

static const colorcomp_t MaterialColor[4][3] = { // BGR
    {96,0,0},     // WATER
    {0,48,48},    // SANDSTONE
    {32,32,23}    // SHALE
};

static const int DamperSize = 32;

static const int ColorMapSize = 1024;
static color_t ColorMap[4][ColorMapSize];

static int PulseTime = 100;
static int PulseCounter;
static int PulseX = UniverseWidth/3;
static int PulseY = UniverseHeight/4;

static bool InitIsParallel = true;
const char *titles[2] = {"Seismic Simulation: Serial", "Seismic Simulation: Parallel"};
int threads_low = 0, threads_high = tbb::task_scheduler_init::automatic;

static void UpdatePulse() {
    if( PulseCounter>0 ) {
        value t = (PulseCounter-PulseTime/2)*0.05f;
        V[PulseY][PulseX] += 64*sqrt(M[PulseY][PulseX])*exp(-t*t);
        --PulseCounter;
    }
}

static void SerialUpdateStress() {
    drawing_area drawing(0, 0, UniverseWidth, UniverseHeight);
    for( int i=1; i<universeheight-1; j="1;">
	<universewidth-1; +="M[i][j]*(V[i][j+1]-V[i][j]);" 
	index="(int)(V[i][j]*(ColorMapSize/2))"><0 ) index = 0;
            if( index>=ColorMapSize ) index = ColorMapSize-1;
            color_t* c = ColorMap[Material[i][j]];
            drawing.put_pixel(c[index]);
        }
    }
}

struct UpdateStressBody {
    void operator()( const tbb::blocked_range<int>& range ) const {
        drawing_area drawing(0, range.begin(), 
UniverseWidth, range.end()-range.begin());
        int i_end = range.end();
        for( int y = 0, i=range.begin(); i!=i_end; ++i,y++ ) {
            drawing.set_pos(1, y);
#pragma ivdep
            for( int j=1; j<universewidth-1; 
		+="M[i][j]*(V[i][j+1]-V[i][j]);" 
		index="(int)(V[i][j]*(ColorMapSize/2))"><0 ) index = 0;
                if( index>=ColorMapSize ) index = ColorMapSize-1;
                color_t* c = ColorMap[Material[i][j]];
                drawing.put_pixel(c[index]);
            }
        }
    }
};

static void ParallelUpdateStress() {
    tbb::parallel_for( tbb::blocked_range<int>( 1, UniverseHeight-1 ), 
// Index space for loop
                       UpdateStressBody(),                            
 // Body of loop
                       Affinity );                                     
// Affinity hint
}

static void SerialUpdateVelocity() {
    for( int i=1; i<universeheight-1; j="1;">
		<universewidth-1; v[i][j]="D[i][j]*(V[i][j]"><int>& range ) const {
        int i_end = range.end();
        for( int i=range.begin(); i!=i_end; ++i ) 
#pragma ivdep
            for( int j=1; j<universewidth-1; 
		v[i][j]="D[i][j]*(V[i][j]"><int>( 1, 
		UniverseHeight-1 ), // Index space for loop
                       UpdateVelocityBody(),                           

                       Affinity );                                    
 
}

void SerialUpdateUniverse() {
    UpdatePulse();
    SerialUpdateStress();
    SerialUpdateVelocity();
}

void ParallelUpdateUniverse() {
    UpdatePulse();
    ParallelUpdateStress();
    ParallelUpdateVelocity();
}

class seismic_video : public video
{
    void on_mouse(int x, int y, int key) {
        if(key == 1 && PulseCounter == 0) {
            PulseCounter = PulseTime;
            PulseX = x; PulseY = y;
        }
    }
    void on_key(int key) {
        key &= 0xff;
        if(char(key) == ' ') InitIsParallel = !InitIsParallel;
        else if(char(key) == 'p') InitIsParallel = true;
        else if(char(key) == 's') InitIsParallel = false;
        else if(char(key) == 'e') updating = true;
        else if(char(key) == 'd') updating = false;
        else if(key == 27) running = false;
        title = InitIsParallel?titles[1]:titles[0];
    }
    void on_process() {
        tbb::task_scheduler_init Init(threads_high);
        do {
            if( InitIsParallel )
                ParallelUpdateUniverse();
            else
                SerialUpdateUniverse();
            if( number_of_frames > 0 ) --number_of_frames;
        } while(next_frame() && number_of_frames);
    }
} video;

void InitializeUniverse() {
    PulseCounter = PulseTime;
    // Initialize V, S, and T to slightly non-zero values,
 in order to avoid denormal waves.
    for( int i=0; i<universeheight; j="0;"><universewidth; 
		t[i][j]="S[i][j]" i="1;"><universeheight-1; j="1;">
		<universewidth-1; x="float(j-UniverseWidth/2)/
		(UniverseWidth/2);" t="(value)i/UniverseHeight;" 
		d[i][j]="1.0;"><0.3f ) {
                m = WATER;
                M[i][j] = 0.125;
                L[i][j] = 0.125;
            } else if( fabs(t-0.7+0.2*exp(-8*x*x)+0.025*x)<=0.1 ) {
                m = SHALE;
                M[i][j] = 0.5;
                L[i][j] = 0.6;
            } else {
                m = SANDSTONE;
                M[i][j] = 0.3;
                L[i][j] = 0.4;
            } 
            Material[i][j] = m;
        }
    }
    value scale = 2.0f/ColorMapSize;
    for( int k=0; k<4; ++k ) {
        for( int i=0; i<colormapsize; t="(i-ColorMapSize/2)*scale;" r="t">0 ? t : 0;
            value b = t<0 ? -t : 0;
            value g = 0.5f*fabs(t);
            memcpy(c, MaterialColor[k], sizeof(c));
            c[2] = colorcomp_t(r*(255-c[2])+c[2]);
            c[1] = colorcomp_t(g*(255-c[1])+c[1]);
            c[0] = colorcomp_t(b*(255-c[0])+c[0]);
            ColorMap[k][i] = video.get_color(c[2], c[1], c[0]);
        }
    }
    // Set damping coefficients around border to reduce reflections from boundaries.
    value d = 1.0;
    for( int k=DamperSize-1; k>0; --k ) {
        d *= 1-1.0f/(DamperSize*DamperSize);
        for( int j=1; j<universewidth-1; *="d;" i="1;">
		<universeheight-1; *="d;"> 1 && isdigit(argv[1][0])) {
        char* end; threads_high = threads_low = (int)strtol(argv[1],&end,0);
        switch( *end ) {
            case ':': threads_high = (int)strtol(end+1,0,0); break;
            case '\0': break;
            default: printf("unexpected character = %c\n",*end);
        }
    }
    if (argc > 2 && isdigit(argv[2][0])){
        number_of_frames = (int)strtol(argv[2],0,0);
    }
    // video layer init
    video.title = InitIsParallel?titles[1]:titles[0];
#ifdef _WINDOWS
    #define MAX_LOADSTRING 100
    TCHAR szWindowClass[MAX_LOADSTRING];    // the main window class name
    LoadStringA(video::win_hInstance, 
    IDC_SEISMICSIMULATION, szWindowClass, MAX_LOADSTRING);
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
    WNDCLASSEX wcex; memset(&wcex, 0, sizeof(wcex));
    wcex.lpfnWndProc    = (WNDPROC)WndProc;
    wcex.hIcon          = 
     LoadIcon(video::win_hInstance, MAKEINTRESOURCE(IDI_SEISMICSIMULATION));
    wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = LPCTSTR(IDC_SEISMICSIMULATION);
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = LoadIcon(video::win_hInstance, MAKEINTRESOURCE(IDI_SMALL));
    video.win_set_class(wcex); // ascii convention here
    video.win_load_accelerators(IDC_SEISMICSIMULATION);
#endif
    if(video.init_window(UniverseWidth, UniverseHeight)) {
        video.calc_fps = true;
        video.threaded = threads_low > 0;
        // video is ok, init universe
        InitializeUniverse();
        // main loop
        video.main_loop();
    }
    else if(video.init_console()) {
        // do console mode
        if(number_of_frames <= 0) number_of_frames = DEFAULT_NUMBER_OF_FRAMES;
        if(threads_high == tbb::task_scheduler_init::automatic) threads_high = 4;
        if(threads_high < threads_low) threads_high = threads_low;
        for( int p = threads_low; p <= threads_high; ++p ) {
            InitializeUniverse();
            tbb::task_scheduler_init init(tbb::task_scheduler_init::deferred);
            if( p > 0 )
                init.initialize( p );
            tbb::tick_count t0 = tbb::tick_count::now();
            if( p > 0 )
                for( int i=0; i<number_of_frames; i="0;">
		<number_of_frames; t1="tbb::tick_count::now();"> 0 ) 
                printf(" with %d way parallelism\n",p);
            else
                printf(" with serial version\n"); 
        }
    }
    video.terminate();
    return 0;
}

#ifdef _WINDOWS
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_INITDIALOG: return TRUE;
    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) {
            EndDialog(hDlg, LOWORD(wParam));
            return TRUE;
        }
        break;
    }
    return FALSE;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int wmId, wmEvent;
    switch (message) {
    case WM_COMMAND:
        wmId    = LOWORD(wParam); 
        wmEvent = HIWORD(wParam); 
        // Parse the menu selections:
        switch (wmId)
        {
        case IDM_ABOUT:
            DialogBox(video::win_hInstance, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, 
            (DLGPROC)About);
            break;
        case IDM_EXIT:
            PostQuitMessage(0);
            break;
        case ID_FILE_PARALLEL:
            if( !InitIsParallel ) {
                InitIsParallel = true;
                video.title = titles[1];
            }
            break;
        case ID_FILE_SERIAL:
            if( InitIsParallel ) {
                InitIsParallel = false;
                video.title = titles[0];
            }
            break;
        case ID_FILE_ENABLEGUI:
            video.updating = true;
            break;
        case ID_FILE_DISABLEGUI:
            video.updating = false;
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

#endif

不可否认,波传播的概念本身就足够困难,还需要串行和并行方法。但是,使用并行编程的专业领域数量惊人。这就是为什么选择一个强大的线程库至关重要的原因。请查看输出。该图像需要几分钟才能完成。

a.jpg

地球或某种弹性介质中发生扰动,并触发一系列波,这些波具有速度和方向。加速度并不是一个准确的测量。现在检查大约 30 秒后的图像。每秒帧数几乎保持在同一范围内。

b.jpg

光线追踪器示例

MSDN 并行计算中心提供原生代码和托管代码的并行编程示例下载。光线追踪器示例之所以出名,是因为光线追踪基本上是一个“非常适合并行的问题”。图像中的每个像素都是通过发射一束假想的光线生成的,该光线在场景中的物体上和穿过物体时反弹,并记录结果的颜色。因此,每个像素都与其他像素独立,允许它们全部并行处理。该示例可从 http://code.msdn.microsoft.com/ParExtSamples 下载。现在,如果我们考虑一些涉及医学成像的基本原理,那么我们可以看一下 X 射线。X 射线具有所有光波中最短的波长。X 射线的频率高于紫外线波。因此,当从 X 射线管发射时,突然爆发会捕捉到实体图像。如今他们使用伽马射线。但无论如何,分辨率总是很差。

为了解决这个问题,模型被设计用来显示辐射如何穿过人体。假定人体内随机选择的点发射辐射(通常是伽马射线),并跟踪每条射线的轨迹。当粒子(射线)穿过人体时,它会被穿过的不同器官衰减,直到粒子离开人体并撞击相机模型,从而定义完整的轨迹。为了创建统计上显著的模拟,需要跟踪数千甚至数百万条轨迹。这个问题可以通过两种方式进行并行化。因为每条轨迹都是独立的,所以可以通过将每条轨迹与任务或数据并行化相关联来并行化应用程序。

因此,在这一点上,我们将以一个简单的二维光线追踪器(一个茶杯)示例来结束。代码的执行方式是使用一个包含图像建模的数据文件。然后我们执行程序,并注意到每秒帧数的计算速度更快,分辨率也得到了提高。代码可以在本文的开头部分下载。读者应该下载 Intel C++ 编译器的试用评估版。安装后,您将看到它如何与 Visual Studio 2008 集成作为内置功能。接下来,将 Intel TBB beta 3.0 版本作为您的线程库安装。之后,将文件解压到项目目录文件夹中的一个新创建的文件夹中。但我们将使用“nmake”实用程序来构建文件及其可执行文件。稍后将对此进行更清楚的说明。

tea.jpg

提供上述图像的命令是

C>\..\tachyon.tbb.exe teacup.dat

要构建此可执行文件以及在 dat 目录中保存文件的其他比较可执行文件,然后构建一个目录来解压缩可下载的 dat.zip 文件。然后设置环境变量,因为我们将从 VS Studio 2008 使用“nmake”。我们首先配置 Intel 编译器的路径,然后运行一个批处理脚本来设置环境变量。之后,我们对 Microsoft VC++ 编译器执行相同的操作,并运行一个类似的批处理脚本来设置环境变量。

set PATH=%PATH%;.;c:\Program Files\Intel\compiler\11.1\065\bin\ia32
iclvars_ia32.bat
set PATH=%PATH%;.;C:\Program Files\Microsoft Visual Studio 9.0\VC\bin
vcvars32.bat
(now run nmake)
nmake.exe 

构建将显示串行执行和并行执行后出现的球体。因此,文件是 balls.datteapot.dat 等。尝试复制并粘贴这些.dat 文件到 nmake 构建可执行文件的先前目录中。

参考文献

  • James Reiner 的《Intel Threading Building Blocks》
  • Stephen Toub 的《并行编程模式》
© . All rights reserved.