CUDA共享內(nèi)存之bank沖突

前面博客中我們說(shuō)到了共享內(nèi)存的使用方法以及一些高級(jí)特性形纺,并簡(jiǎn)單說(shuō)明了一下bank沖突哗蜈,這里我們將會(huì)通過(guò)一些簡(jiǎn)單的例子來(lái)詳細(xì)介紹一下bank沖突谷遂。
為了獲得較高的內(nèi)存帶寬钠糊,共享存儲(chǔ)器被劃分為多個(gè)大小相等的存儲(chǔ)器模塊泻云,稱(chēng)為bank,可以被同時(shí)訪問(wèn)头谜。因此任何跨越b個(gè)不同的內(nèi)存bank的對(duì)n個(gè)地址進(jìn)行讀取和寫(xiě)入的操作可以被同時(shí)進(jìn)行骏掀,這樣就大大提高了整體帶寬 ——可達(dá)到單獨(dú)一個(gè)bank帶寬的b倍。但是很多情況下柱告,我們無(wú)法充分發(fā)揮bank的功能截驮,以致于shared memory的帶寬非常的小,這可能是因?yàn)槲覀冇龅搅薭ank沖突际度。

bank沖突

當(dāng)一個(gè)warp中的不同線程訪問(wèn)一個(gè)bank中的不同的字地址時(shí)葵袭,就會(huì)發(fā)生bank沖突。
如果沒(méi)有bank沖突的話(huà)乖菱,共享內(nèi)存的訪存速度將會(huì)非常的快坡锡,大約比全局內(nèi)存的訪問(wèn)延遲低100多倍,但是速度沒(méi)有寄存器快窒所。然而鹉勒,如果在使用共享內(nèi)存時(shí)發(fā)生了bank沖突的話(huà),性能將會(huì)降低很多很多吵取。在最壞的情況下禽额,即一個(gè)warp中的所有線程訪問(wèn)了相同bank的32個(gè)不同字地址的話(huà),那么這32個(gè)訪問(wèn)操作將會(huì)全部被序列化皮官,大大降低了內(nèi)存帶寬脯倒。

NOTE:不同warp中的線程之間不存在什么bank沖突。

共享內(nèi)存的地址映射方式

要解決bank沖突捺氢,首先我們要了解一下共享內(nèi)存的地址映射方式藻丢。
在共享內(nèi)存中,連續(xù)的32-bits字被分配到連續(xù)的32個(gè)bank中摄乒,這就像電影院的座位一樣:一列的座位就相當(dāng)于一個(gè)bank悠反,所以每行有32個(gè)座位残黑,在每個(gè)座位上可以“坐”一個(gè)32-bits的數(shù)據(jù)(或者多個(gè)小于32-bits的數(shù)據(jù),如4個(gè)char型的數(shù)據(jù)问慎,2個(gè)short型的數(shù)據(jù))萍摊;而正常情況下,我們是按照先坐完一行再坐下一行的順序來(lái)坐座位的如叼,在shared memory中地址映射的方式也是這樣的冰木。下圖中內(nèi)存地址是按照箭頭的方向依次映射的:

bank_layout

上圖中數(shù)字為bank編號(hào)。這樣的話(huà)笼恰,如果你將申請(qǐng)一個(gè)共享內(nèi)存數(shù)組(假設(shè)是int類(lèi)型)的話(huà)踊沸,那么你的每個(gè)元素所對(duì)應(yīng)的bank編號(hào)就是地址偏移量(也就是數(shù)組下標(biāo))對(duì)32取余所得的結(jié)果,比如大小為1024的一維數(shù)組myShMem:

  • myShMem[4]: 對(duì)應(yīng)的bank id為#4 (相應(yīng)的行偏移量為0)
  • myShMem[31]: 對(duì)應(yīng)的bank id為#31 (相應(yīng)的行偏移量為0)
  • myShMem[50]: 對(duì)應(yīng)的bank id為#18 (相應(yīng)的行偏移量為1)
  • myShMem[128]: 對(duì)應(yīng)的bank id為#0 (相應(yīng)的行偏移量為4)
  • myShMem[178]: 對(duì)應(yīng)的bank id為#18 (相應(yīng)的行偏移量為5)

典型的bank訪問(wèn)方式

下面我介紹幾種典型的bank訪問(wèn)的形式社证。

下面這這種訪問(wèn)方式是典型的線性訪問(wèn)方式(訪問(wèn)步長(zhǎng)(stride)為1)逼龟,由于每個(gè)warp中的線程ID與每個(gè)bank的ID一一對(duì)應(yīng),因此不會(huì)產(chǎn)生bank沖突追葡。

無(wú)沖突的線性訪問(wèn)方式

下面這種訪問(wèn)雖然是交叉的訪問(wèn)腺律,每個(gè)線程并沒(méi)有與bank一一對(duì)應(yīng),但每個(gè)線程都會(huì)對(duì)應(yīng)一個(gè)唯一的bank宜肉,所以也不會(huì)產(chǎn)生bank沖突匀钧。

無(wú)沖突的交叉訪問(wèn)方式

下面這種雖然也是線性的訪問(wèn)bank,但這種訪問(wèn)方式與第一種的區(qū)別在于訪問(wèn)的步長(zhǎng)(stride)變?yōu)?谬返,這就造成了線程0與線程28都訪問(wèn)到了bank 0之斯,線程1與線程29都訪問(wèn)到了bank 2...,于是就造成了2路的bank沖突遣铝。我在后面會(huì)對(duì)以不同的步長(zhǎng)(stride)訪問(wèn)bank的情況做進(jìn)一步討論佑刷。

有沖突的線性訪問(wèn)方式

下面這種訪問(wèn)造成了8路的bank沖突,

8路訪問(wèn)沖突

這里我們需要注意酿炸,下面這兩種情況是兩種特殊情況:

特殊情況1

上圖中瘫絮,所有的線程都訪問(wèn)了同一個(gè)bank,貌似產(chǎn)生了32路的bank沖突填硕,但是由于廣播(broadcast)機(jī)制(當(dāng)一個(gè)warp中的所有線程訪問(wèn)一個(gè)bank中的同一個(gè)字(word)地址時(shí)檀何,就會(huì)向所有的線程廣播這個(gè)字(word)),這種情況并不會(huì)發(fā)生bank沖突廷支。

同樣,這種訪問(wèn)方式也不會(huì)產(chǎn)生bank沖突:

特殊情況2

這就是所謂的多播機(jī)制(multicast)——當(dāng)一個(gè)warp中的幾個(gè)線程訪問(wèn)同一個(gè)bank中的相同字地址時(shí)栓辜,會(huì)將該字廣播給這些線程恋拍。

NOTE:這里的多播機(jī)制(multicast)只適用于計(jì)算能力2.0及以上的設(shè)備,上篇博客中已經(jīng)提到藕甩。

數(shù)據(jù)類(lèi)型與bank沖突

我們都知道施敢,當(dāng)每個(gè)線程訪問(wèn)一個(gè)32-bits大小的數(shù)據(jù)類(lèi)型的數(shù)據(jù)(如int周荐,float)時(shí),不會(huì)發(fā)生bank沖突僵娃。

extern __shared__ int shrd[];
foo = shrd[baseIndex + threadIdx.x]

但是如果每個(gè)線程訪問(wèn)一個(gè)字節(jié)(8-bits)的數(shù)據(jù)時(shí)概作,會(huì)不會(huì)發(fā)生bank沖突呢?很明顯這種情況會(huì)發(fā)生bank沖突的默怨,因?yàn)樗膫€(gè)線程訪問(wèn)了同一個(gè)bank讯榕,造成了四路bank沖突。同理匙睹,如果是short類(lèi)型(16-bits)也會(huì)發(fā)生bank沖突愚屁,會(huì)產(chǎn)生兩路的bank沖突,下面是這種情況的兩個(gè)例子:

extern __shared__ char shrd[];
foo = shrd[baseIndex + threadIdx.x];
訪問(wèn)1字節(jié)的例子1
extern __shared__ short shrd[];
foo = shrd[baseIndex + threadIdx.x];
訪問(wèn)1字節(jié)的例子2

訪問(wèn)步長(zhǎng)與bank沖突

我們通常這樣來(lái)訪問(wèn)數(shù)組:每個(gè)線程根據(jù)線程編號(hào)tid與s的乘積來(lái)訪問(wèn)數(shù)組的32-bits字(word):

extern __shared__ float shared[];
float data = shared[baseIndex + s * tid];

如果按照上面的方式痕檬,那么當(dāng)s*n是bank的數(shù)量(即32)的整數(shù)倍時(shí)或者說(shuō)n是32/d的整數(shù)倍(d是32和s的最大公約數(shù))時(shí)霎槐,線程tid和線程tid+n會(huì)訪問(wèn)相同的bank。我們不難知道如果tid與tid+n位于同一個(gè)warp時(shí)梦谜,就會(huì)發(fā)生bank沖突丘跌,相反則不會(huì)。

仔細(xì)思考你會(huì)發(fā)現(xiàn)唁桩,只有warp的大小(即32)小于等于32/d時(shí)闭树,才不會(huì)有bank沖突,而只有當(dāng)d等于1時(shí)才能滿(mǎn)足這個(gè)條件朵夏。要想讓32和s的最大公約數(shù)d為1蔼啦,s必須為奇數(shù)。于是仰猖,這里有一個(gè)顯而易見(jiàn)的結(jié)論:當(dāng)訪問(wèn)步長(zhǎng)s為奇數(shù)時(shí)捏肢,就不會(huì)發(fā)生bank沖突。

bank沖突的例子

既然我們已經(jīng)理解了bank沖突饥侵,那我們就小試牛刀鸵赫,來(lái)練習(xí)下吧!下面我們以并行計(jì)算中的經(jīng)典的歸約算法為例來(lái)做一個(gè)簡(jiǎn)單的練習(xí)躏升。

假設(shè)有一個(gè)大小為2048的向量辩棒,我們想用歸約算法對(duì)該向量求和。于是我們申請(qǐng)了一個(gè)大小為1024的線程塊膨疏,并聲明了一個(gè)大小為2048的共享內(nèi)存數(shù)組一睁,并將數(shù)據(jù)從全局內(nèi)存拷貝到了該共享內(nèi)存數(shù)組。

我們可以有以下兩種方式實(shí)現(xiàn)歸約算法:

不連續(xù)的方式:

不連續(xù)的方式

連續(xù)的方式:

連續(xù)的方式

下面我們用具體的代碼來(lái)實(shí)現(xiàn)上述兩種方法佃却。

// 非連續(xù)的歸約求和
__global__ void BC_addKernel(const int *a, int *r)
{
    __shared__ int cache[ThreadsPerBlock];
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    int cacheIndex = threadIdx.x;

    // copy data to shared memory from global memory
    cache[cacheIndex] = a[tid];
    __syncthreads();

    // add these data using reduce
    for (int i = 1; i < blockDim.x; i *= 2)
    {
        int index = 2 * i * cacheIndex;
        if (index < blockDim.x)
        {
            cache[index] += cache[index + i];
        }
        __syncthreads();
    }

    // copy the result of reduce to global memory
    if (cacheIndex == 0)
        r[blockIdx.x] = cache[cacheIndex];
}

上述代碼實(shí)現(xiàn)的是非連續(xù)的歸約求和者吁,從int index = 2 * i * cacheIndexcache[index] += cache[index + i];兩條語(yǔ)句,我們可以很容易判斷這種實(shí)現(xiàn)方式會(huì)產(chǎn)生bank沖突饲帅。當(dāng)i=1時(shí)复凳,步長(zhǎng)s=2xi=2瘤泪,會(huì)產(chǎn)生兩路的bank沖突;當(dāng)i=2時(shí)育八,步長(zhǎng)s=2xi=4对途,會(huì)產(chǎn)生四路的bank沖突...當(dāng)i=n時(shí),步長(zhǎng)s=2xn=2n髓棋∈堤矗可以看出每一次步長(zhǎng)都是偶數(shù),因此這種方式會(huì)產(chǎn)生嚴(yán)重的bank沖突仲锄。

NOTE:在《GPU高性能運(yùn)算之CUDA》這本書(shū)中對(duì)實(shí)現(xiàn)不連續(xù)的歸約算法有兩種代碼實(shí)現(xiàn)方式劲妙,但筆者發(fā)現(xiàn)書(shū)中的提到(p179)的兩種所謂相同計(jì)算邏輯的函數(shù)reduce0reduce1,其實(shí)具有本質(zhì)上的不同儒喊。前者不會(huì)發(fā)生bank沖突镣奋,而后者(即本文中所使用的)才會(huì)產(chǎn)生bank沖突。由于前者線程ID要求的條件比較“苛刻”怀愧,只有滿(mǎn)足tid % (2 * s) == 0的線程才會(huì)執(zhí)行求和操作(sdata[tid]+=sdata[tid+i])侨颈;而后者只要滿(mǎn)足index(2 * s * tid,即線程ID的2xs倍)小于線程塊的大小(blockDim.x)即可芯义」福總之,前者在進(jìn)行求和操作(sdata[tid]+=sdata[tid+i])時(shí)扛拨,線程的使用同樣是不連續(xù)的耘分,即當(dāng)s=1時(shí),線程編號(hào)為0,2,4,...,1022绑警;而后者的線程使用是連續(xù)的求泰,即當(dāng)s=1時(shí),前512個(gè)線程(0,1,2,...,511)在進(jìn)行求和操作(sdata[tid]+=sdata[tid+i])计盒,而后512個(gè)線程是閑置的渴频。前者不會(huì)出現(xiàn)多個(gè)線程訪問(wèn)同一bank的不同字地址,而后者正如書(shū)中所說(shuō)會(huì)產(chǎn)生嚴(yán)重的bank沖突北启。(書(shū)中用到的s與本文中多次用到的步長(zhǎng)s不是同一個(gè)變量卜朗,注意不要混淆這兩個(gè)變量)當(dāng)然這些只是筆者的想法,如有不同咕村,歡迎來(lái)與我討論场钉,郵箱:chaoyanglius@outlook.com

// 連續(xù)的歸約求和
__global__ void NBC_addKernel2(const int *a, int *r)
{
    __shared__ int cache[ThreadsPerBlock];
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    int cacheIndex = threadIdx.x;

    // copy data to shared memory from global memory
    cache[cacheIndex] = a[tid];
    __syncthreads();

    // add these data using reduce
    for (int i = blockDim.x / 2; i > 0; i /= 2)
    {
        if (cacheIndex < i)
        {
            cache[cacheIndex] += cache[cacheIndex + i];
        }
        __syncthreads();
    }

    // copy the result of reduce to global memory
    if (cacheIndex == 0)
        r[blockIdx.x] = cache[cacheIndex];
}

由于每個(gè)線程的ID與操作的數(shù)據(jù)編號(hào)一一對(duì)應(yīng)懈涛,因此上述的代碼很明顯不會(huì)產(chǎn)生bank沖突惹悄。

參考資料

  1. C語(yǔ)言程序設(shè)計(jì)現(xiàn)代方法,[美]K.N.King著肩钠,人民郵電出版社
  2. 英偉達(dá)CUDA C programming guide v7.0
  3. 威斯康星大學(xué)仿真實(shí)驗(yàn)室CUDA課程講義10-07-2013:http://sbel.wisc.edu/Courses/ME964/2013/
  4. GPU高性能運(yùn)算之CUDA泣港,張舒,褚艷利价匠,中國(guó)水利水電出版社
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末当纱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子踩窖,更是在濱河造成了極大的恐慌坡氯,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洋腮,死亡現(xiàn)場(chǎng)離奇詭異箫柳,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)啥供,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)悯恍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人伙狐,你說(shuō)我怎么就攤上這事涮毫。” “怎么了贷屎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵罢防,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我唉侄,道長(zhǎng)咒吐,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任属划,我火速辦了婚禮恬叹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘榴嗅。我一直安慰自己妄呕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布嗽测。 她就那樣靜靜地躺著绪励,像睡著了一般。 火紅的嫁衣襯著肌膚如雪唠粥。 梳的紋絲不亂的頭發(fā)上疏魏,一...
    開(kāi)封第一講書(shū)人閱讀 51,443評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音晤愧,去河邊找鬼大莫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛官份,可吹牛的內(nèi)容都是我干的只厘。 我是一名探鬼主播烙丛,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼羔味!你這毒婦竟也來(lái)了河咽?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤赋元,失蹤者是張志新(化名)和其女友劉穎忘蟹,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體搁凸,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡媚值,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了护糖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褥芒。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖椅文,靈堂內(nèi)的尸體忽然破棺而出喂很,到底是詐尸還是另有隱情,我是刑警寧澤皆刺,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布少辣,位于F島的核電站,受9級(jí)特大地震影響羡蛾,放射性物質(zhì)發(fā)生泄漏漓帅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一痴怨、第九天 我趴在偏房一處隱蔽的房頂上張望忙干。 院中可真熱鬧,春花似錦浪藻、人聲如沸捐迫。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)施戴。三九已至,卻和暖如春萌丈,著一層夾襖步出監(jiān)牢的瞬間赞哗,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工辆雾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肪笋,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像藤乙,于是被迫代替她去往敵國(guó)和親猜揪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354