Intel TBB 开发指南 3 parallel_reduce
原文
循環(huán)可以進(jìn)行歸約,如以下求和:
float SerialSumFoo( float a[], size_t n ) {float sum = 0;for( size_t i=0; i!=n; ++i )sum += Foo(a[i]);return sum; }如果迭代是獨(dú)立的,你可以使用模板類 parallel_reduce 并行化此循環(huán),如下所示:
float ParallelSumFoo( const float a[], size_t n ) {SumFoo sf(a);parallel_reduce( blocked_range<size_t>(0,n), sf );return sf.my_sum; }SumFoo 類指定了歸約的細(xì)節(jié),例如如何累加和組合它們。 這是類 SumFoo 的定義:
class SumFoo {float* my_a; public:float my_sum;void operator()( const blocked_range<size_t>& r ) {float *a = my_a;float sum = my_sum;size_t end = r.end();for( size_t i=r.begin(); i!=end; ++i )sum += Foo(a[i]);my_sum = sum;}SumFoo( SumFoo& x, split ) : my_a(x.my_a), my_sum(0) {}void join( const SumFoo& y ) {my_sum+=y.my_sum;}SumFoo(float a[] ) :my_a(a), my_sum(0){} };請(qǐng)注意與來(lái)自 parallel_for 的類 ApplyFoo 的區(qū)別。 首先,operator() 不是 const。 這是因?yàn)樗仨毟?SumFoo::my_sum。 其次,SumFoo 有一個(gè)拆分構(gòu)造函數(shù)和一個(gè)方法 join,必須存在才能使 parallel_reduce 工作。 拆分構(gòu)造函數(shù)將原始對(duì)象的引用和類型為 split 的偽參數(shù)作為參數(shù),該參數(shù)由庫(kù)定義。 虛擬參數(shù)將拆分構(gòu)造函數(shù)與復(fù)制構(gòu)造函數(shù)區(qū)分開(kāi)來(lái)。
在示例中,operator() 的定義將局部臨時(shí)變量(a、sum、end)用于循環(huán)內(nèi)訪問(wèn)的標(biāo)量值。 這種技術(shù)可以通過(guò)讓編譯器清楚地知道這些值可以保存在寄存器而不是內(nèi)存中來(lái)提高性能。 如果這些值太大而無(wú)法放入寄存器中,或者它們的地址以編譯器無(wú)法跟蹤的方式獲取,則該技術(shù)可能無(wú)濟(jì)于事。 對(duì)于典型的優(yōu)化編譯器,僅對(duì)寫入的變量(例如示例中的 sum)使用局部臨時(shí)變量就足夠了,因?yàn)檫@樣編譯器就可以推斷出循環(huán)不會(huì)寫入任何其他位置,并將其他讀取提升到外部 循環(huán)。
當(dāng)工作線程可用時(shí),由任務(wù)調(diào)度程序決定,parallel_reduce 調(diào)用拆分構(gòu)造函數(shù)為工作線程創(chuàng)建子任務(wù)。 當(dāng)子任務(wù)完成時(shí),parallel_reduce 使用方法 join 來(lái)累積子任務(wù)的結(jié)果。 下圖頂部的圖形顯示了當(dāng)工作程序可用時(shí)發(fā)生的拆分連接序列:
上圖中的箭頭表示時(shí)間順序。 拆分構(gòu)造函數(shù)可能會(huì)在對(duì)象 x 用于縮減的前半部分時(shí)并發(fā)運(yùn)行。 因此,創(chuàng)建 y 的拆分構(gòu)造函數(shù)的所有操作都必須相對(duì)于 x 成為線程安全的。 因此,如果拆分構(gòu)造函數(shù)需要遞增與其他對(duì)象共享的引用計(jì)數(shù),則應(yīng)使用原子遞增。
如果 worker 不可用,則使用減少前半部分的相同主體對(duì)象減少迭代的后半部分。 那就是下半場(chǎng)的減持開(kāi)始,上半場(chǎng)的減持結(jié)束。
由于如果 worker 不可用,則不使用 split/join,parallel_reduce 不一定會(huì)進(jìn)行遞歸拆分。
由于同一個(gè)主體可能用于累積多個(gè)子范圍,因此 operator() 不丟棄較早的累積是至關(guān)重要的。 下面的代碼顯示了 SumFoo::operator() 的錯(cuò)誤定義。
由于錯(cuò)誤,主體返回最后一個(gè)子范圍的部分和,而不是 parallel_reduce 應(yīng)用它的所有子范圍。
parallel_reduce 的分區(qū)器和粒度規(guī)則與 parallel_for 相同。
parallel_reduce 推廣到任何關(guān)聯(lián)操作。 通常,拆分構(gòu)造函數(shù)會(huì)做兩件事:
- 復(fù)制運(yùn)行循環(huán)體所需的只讀信息。
- 將歸約變量初始化為操作的標(biāo)識(shí)元素。
join 方法應(yīng)該進(jìn)行相應(yīng)的合并。 你可以同時(shí)進(jìn)行多次縮減:可以使用單個(gè) parallel_reduce 收集最小值和最大值。
歸約運(yùn)算可以是非交換的。 如果浮點(diǎn)加法被字符串連接替換,該示例仍然有效。
Advanced Example
更高級(jí)的關(guān)聯(lián)操作的一個(gè)例子是找到 Foo(i) 最小化的索引。 串行版本可能如下所示:
long SerialMinIndexFoo( const float a[], size_t n ) {float value_of_min = FLT_MAX; // FLT_MAX from <climits>long index_of_min = -1;for( size_t i=0; i<n; ++i ) {float value = Foo(a[i]);if( value<value_of_min ) {value_of_min = value;index_of_min = i;}}return index_of_min; }該循環(huán)記錄迄今為止找到的最小值以及該值的索引。 這是循環(huán)迭代之間攜帶的唯一信息。 要將循環(huán)轉(zhuǎn)換為使用 parallel_reduce,函數(shù)對(duì)象必須跟蹤攜帶的信息,以及當(dāng)?shù)植荚诙鄠€(gè)線程上時(shí)如何合并這些信息。 此外,函數(shù)對(duì)象必須記錄指向 a 的指針以提供上下文。
以下代碼顯示了完整的函數(shù)對(duì)象。
現(xiàn)在 SerialMinIndex 可以使用 parallel_reduce 重寫,如下所示:
long ParallelMinIndexFoo( float a[], size_t n ) {MinIndexFoo mif(a);parallel_reduce(blocked_range<size_t>(0,n), mif );return mif.index_of_min; }Advanced Topic: Other Kinds of Iteration Spaces
到目前為止的示例都使用了類 blocks_range<T> 來(lái)指定范圍。 這個(gè)類在許多情況下都很有用,但它并不適合所有情況。 你可以使用 oneTBB 來(lái)定義自己的迭代空間對(duì)象。 該對(duì)象必須通過(guò)提供一個(gè)基本的拆分構(gòu)造函數(shù)、一個(gè)可選的比例拆分構(gòu)造函數(shù)(伴隨著啟用其使用的特征值)和兩個(gè)謂詞方法來(lái)指定如何將其拆分為子空間。 如果你的類稱為 R,則方法和構(gòu)造函數(shù)應(yīng)如下所示:
class R {// True if range is emptybool empty() const;// True if range can be split into non-empty subrangesbool is_divisible() const;// Splits r into subranges r and *thisR( R& r, split );// Splits r into subranges r and *this in proportion pR( R& r, proportional_split p );// Allows usage of proportional splitting constructorstatic const bool is_splittable_in_proportion = true;... };如果范圍為空,方法 empty 應(yīng)該返回 true。如果范圍可以拆分為兩個(gè)非空子空間,則方法 is_divisible 應(yīng)該返回 true,這樣的拆分值得開(kāi)銷。基本的拆分構(gòu)造函數(shù)應(yīng)該有兩個(gè)參數(shù):
- 第一個(gè) R 類型
- 第二個(gè) oneapi::tbb::split 類型
第二個(gè)參數(shù)沒(méi)有被實(shí)際使用;它僅用于將構(gòu)造函數(shù)與普通的復(fù)制構(gòu)造函數(shù)區(qū)分開(kāi)來(lái)。基本的拆分構(gòu)造函數(shù)應(yīng)該嘗試將 r 大致拆分為兩半,并將 r 更新為前半部分,并將構(gòu)造的對(duì)象設(shè)置為后半部分。
與基本拆分構(gòu)造函數(shù)不同,比例拆分構(gòu)造函數(shù)是可選的,并采用 oneapi::tbb::proportional_split 類型的第二個(gè)參數(shù)。該類型具有返回比例值的 left 和 right 方法。應(yīng)該使用這些值來(lái)相應(yīng)地拆分 r,使更新后的 r 對(duì)應(yīng)于比例的左側(cè)部分,而構(gòu)造的對(duì)象對(duì)應(yīng)于右側(cè)部分。只有在類中定義了靜態(tài)常量 is_splittable_in_proportion 并賦值為 true 時(shí),才會(huì)使用比例拆分構(gòu)造函數(shù)。
兩個(gè)拆分構(gòu)造函數(shù)都應(yīng)該保證更新后的 r 部分和構(gòu)造的對(duì)象不為空。只有當(dāng) r.is_divisible 為真時(shí),并行算法模板才會(huì)調(diào)用 r 上的拆分構(gòu)造函數(shù)。
迭代空間不必是線性的。查看 oneapi/tbb/blocked_range2d.h 以獲得二維范圍的示例。它的拆分構(gòu)造函數(shù)嘗試沿其最長(zhǎng)軸拆分范圍。當(dāng)與parallel_for 一起使用時(shí),它會(huì)導(dǎo)致循環(huán)以提高緩存使用率的方式“遞歸阻塞”。這種良好的緩存行為意味著,在blocked_range2d<T> 上使用parallel_for 可以使循環(huán)比順序等效的循環(huán)運(yùn)行得更快,即使在單個(gè)處理器上也是如此。
總結(jié)
以上是生活随笔為你收集整理的Intel TBB 开发指南 3 parallel_reduce的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 关于开源框架GPUImage 的简单说明
- 下一篇: 关于销售订单挑库发放卡接口以及发运处理卡