改了一行代碼,數(shù)組遍歷耗時(shí)從10.3秒降到了0.5秒!
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
兩個(gè)簡(jiǎn)單的測(cè)試程序定義一個(gè)同樣大小的二維數(shù)組,然后循環(huán)遍歷,對(duì)數(shù)組元素賦值。
編譯運(yùn)行,并用time命令統(tǒng)計(jì)一下運(yùn)行時(shí)間: array1用時(shí)0.528秒,array2用時(shí)10.310秒。array2耗時(shí)居然是array1的將近20倍! 有沒(méi)有被這個(gè)結(jié)果震驚到?為什么會(huì)有如此之大的性能差異呢? 重要說(shuō)明要想真正理解這個(gè)問(wèn)題,必須要先補(bǔ)充一些關(guān)于現(xiàn)代計(jì)算機(jī)存儲(chǔ)系統(tǒng)相關(guān)的背景知識(shí),這也是理解這個(gè)問(wèn)題的關(guān)鍵所在。為方便大家理解,我會(huì)盡量以白話的形式進(jìn)行講解,盡可能避免枯燥無(wú)味的純理論描述。 存儲(chǔ)金字塔相信不少人都聽(tīng)過(guò)“存儲(chǔ)金字塔”這個(gè)詞,或者至少見(jiàn)過(guò)類(lèi)似下面這張圖: 這張圖很直觀地描述了現(xiàn)代計(jì)算機(jī)系統(tǒng)的分級(jí)存儲(chǔ)模型。 可以認(rèn)為CPU就位于金字塔的頂點(diǎn)上,越靠近塔頂,離CPU越近,訪問(wèn)速度越快,但生產(chǎn)成本越高,相應(yīng)的容量也就越小。在所有存儲(chǔ)器中,寄存器直接內(nèi)嵌在CPU中,訪問(wèn)速度最快,容量也最小,一般CPU上也就最多幾十個(gè)通用寄存器供應(yīng)用程序使用。 反之,越往下靠近塔底,訪問(wèn)速度越慢,生產(chǎn)成本越低,相應(yīng)的容量也就越大。比如圖中最底部的網(wǎng)絡(luò)存儲(chǔ)設(shè)備,相對(duì)其他存儲(chǔ)設(shè)備而言是訪問(wèn)速度最慢的,但其容量卻幾乎可以認(rèn)為是無(wú)限制的。 那么,這種金字塔式結(jié)構(gòu)中,不同層級(jí)的存儲(chǔ)設(shè)備之間究竟是如何協(xié)調(diào)工作的呢? 用一句話概括:高一級(jí)的存儲(chǔ)設(shè)備,往往是作為低一級(jí)存儲(chǔ)設(shè)備的緩存來(lái)使用的。 簡(jiǎn)單來(lái)說(shuō),系統(tǒng)運(yùn)行時(shí),為了提升數(shù)據(jù)訪問(wèn)效率,把程序中最近最經(jīng)常訪問(wèn)的數(shù)據(jù),盡可能放到訪問(wèn)速度更快的高一級(jí)存儲(chǔ)器中。這樣一來(lái),每次訪問(wèn)數(shù)據(jù)時(shí),從金字塔的頂端開(kāi)始,都先嘗試在高一級(jí)存儲(chǔ)器中查找,如果被訪問(wèn)的數(shù)據(jù)存在且有效,則直接訪問(wèn),否則,就逐級(jí)到更低級(jí)的存儲(chǔ)器中去查找。 這種金字塔式的分級(jí)存儲(chǔ)模型之所以能夠以近乎完美的方式運(yùn)行,實(shí)際上都是建立在現(xiàn)代計(jì)算機(jī)科學(xué)中的一個(gè)非常重要的理論基礎(chǔ)之上:程序的局部性原理。 局部性原理一個(gè)程序的局部性,包含兩個(gè)維度:時(shí)間局部性和空間局部性。
高速緩存 - Cache根據(jù)常識(shí),我們知道,程序要想被CPU正常執(zhí)行,必須要先被加載到內(nèi)存中。(當(dāng)然也有例外,有童鞋知道哪些程序不是在內(nèi)存中運(yùn)行的嗎?歡迎添加作者微信CreCoding討論?。?/p> 但是,內(nèi)存的訪問(wèn)速度與CPU運(yùn)行速度相比,要慢好幾個(gè)數(shù)量級(jí),可以想象一下,如果CPU每次都直接從內(nèi)存中存取數(shù)據(jù),會(huì)造成大量的計(jì)算資源浪費(fèi),程序性能?chē)?yán)重受損,假如真是這樣的話,你還能像現(xiàn)在這樣愉快的吃雞嗎? 為了解決CPU和內(nèi)存之間速度嚴(yán)重不匹配的問(wèn)題,在CPU和內(nèi)存之間設(shè)計(jì)了高速緩存,即Cache。 目前,主流CPU一般都有三級(jí)(或更多級(jí))的Cache,依次是L1 Cache、L2 Cache、L3 Cache。其中L1 Cache速度最快,幾乎可以和內(nèi)嵌在CPU中的寄存器媲美,容量也最小,而L3 Cache速度最慢(但仍然比內(nèi)存快很多),但容量最大。 CPU讀取數(shù)據(jù)時(shí),會(huì)在L1、L2、L3Cache中逐級(jí)查找,如果找到,就從Cache直接讀取,找不到再?gòu)膬?nèi)存讀取,并且把數(shù)據(jù)存放到Cache中,以便提高下次訪問(wèn)的效率。 在這個(gè)過(guò)程中,如果在Cache中找到所需數(shù)據(jù),稱(chēng)為Cache命中(Cache Hit), 找不到稱(chēng)為Cache未命中(Cache Miss)。 不難看出,L1 Cache命中的時(shí)候,讀取數(shù)據(jù)最快,性能最好,而當(dāng)L1、L2、L3 Cache全部未命中時(shí),就必須要直接從內(nèi)存中讀取數(shù)據(jù),性能最差! Cache LineCache Line 是 Cache和內(nèi)存之間進(jìn)行數(shù)據(jù)傳輸?shù)淖钚挝弧?/strong> 根據(jù)上文講解的程序的局部性原理,如果一個(gè)數(shù)據(jù)被CPU訪問(wèn)了,那么這個(gè)數(shù)據(jù)相鄰的其他數(shù)據(jù)也很快會(huì)被訪問(wèn)到。因此,為了提高內(nèi)存數(shù)據(jù)的讀取效率,并且最大化利用CPU資源,數(shù)據(jù)在Cache和內(nèi)存之間傳輸時(shí),不是一個(gè)字節(jié)一個(gè)字節(jié)進(jìn)行傳輸?shù)?,而是以緩存?Cache Line)為單位進(jìn)行傳輸?shù)摹?/p> 不同CPU的Cache line大小可能不同,典型的CPU Cache line大小是64個(gè)字節(jié)。 我們通過(guò)下面一個(gè)簡(jiǎn)單的例子,加深一下理解。 Cache Line 實(shí)例講解在一個(gè)Cache Line大小為64字節(jié)的機(jī)器上,定義個(gè)數(shù)組: int a[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; 我們假設(shè)數(shù)組a的起始地址是Cache Line對(duì)齊的,可以簡(jiǎn)單理解為a的地址能被64整除。假設(shè),數(shù)組a還從來(lái)沒(méi)有被訪問(wèn)過(guò),如果此時(shí)需要訪問(wèn)a中的一個(gè)元素a[5],如: int x = a[5]; 由于在此之前數(shù)組a沒(méi)有被訪問(wèn)過(guò),所以理論上講,數(shù)組a應(yīng)該只存在于內(nèi)存中,并未被加載到Cache中。因此,此時(shí)CPU在Cache中找不到a[5],觸發(fā)Cache Miss,然后需要從內(nèi)存中讀取數(shù)據(jù),并更加載到Cache中。前面提到,Cache和內(nèi)存之間是以Cache Line為單位進(jìn)行數(shù)據(jù)傳輸?shù)?,因此,這里會(huì)把一個(gè)Cache line大小(64字節(jié))的數(shù)據(jù)從內(nèi)存讀取出來(lái)加載到Cache中。由于a的起始地址恰巧是Cache line對(duì)齊的,所以CPU會(huì)把整個(gè)數(shù)組(64個(gè)字節(jié),剛好一個(gè)Cache Line)都加載到Cache中。 緊接著,如果再訪問(wèn)數(shù)組a的元素,如: int y = a[10]; 此時(shí),整個(gè)數(shù)組都在Cache中,所以CPU在訪問(wèn)時(shí),觸發(fā)Cache Hit,直接從Cache讀取數(shù)據(jù)即可,不需要再?gòu)膬?nèi)存中讀取。 了解了Cache的背景知識(shí),現(xiàn)在來(lái)分析下array1.c和array2.c為什么會(huì)存在這么巨大的性能差異。 按行、列訪問(wèn)的真正差異 - Cache首先,必須要知道一點(diǎn):C語(yǔ)言的數(shù)組,所有元素是存放在地址連續(xù)的內(nèi)存中的,此外,C語(yǔ)言的多維數(shù)組,是按行進(jìn)行存儲(chǔ)的。 array1.c按行對(duì)數(shù)組進(jìn)行訪問(wèn),也就是從數(shù)組起始地址開(kāi)始,一直連續(xù)訪問(wèn)到數(shù)組的最后一個(gè)元素的地址處。第一次訪問(wèn)一個(gè)Cache Line的首個(gè)元素時(shí),觸發(fā)Cache Miss,與該元素臨近的幾個(gè)元素會(huì)組成一個(gè)Cache Line,被一起加載到Cache中。如此,在訪問(wèn)下一個(gè)元素的時(shí)候,就會(huì)Cache Hit,直接從Cache讀取數(shù)據(jù)即可。 而array2.c按列對(duì)數(shù)組進(jìn)行訪問(wèn),因此并不是按照連續(xù)地址進(jìn)行訪問(wèn)的,而是每次間隔10240 * 4個(gè)字節(jié)進(jìn)行訪問(wèn)。第一次訪問(wèn)一個(gè)Cache Line的首個(gè)元素時(shí),假設(shè)地址為x,盡管該元素臨近的一個(gè)Cache Line大小的元素也會(huì)被一起加載進(jìn)Cache中,但是程序接下來(lái)訪問(wèn)的并不是臨近的元素,而是地址為x + 10240 * 4處的元素,因此會(huì)再次觸發(fā)Cache Miss。而當(dāng)程序回過(guò)頭來(lái)訪問(wèn)x + 4地址處的元素時(shí),這個(gè)Cache Line可能已經(jīng)被其他數(shù)據(jù)沖刷掉了。因?yàn)?,盡管Cache會(huì)盡量緩存最近訪問(wèn)過(guò)的數(shù)據(jù),但畢竟大小有限,當(dāng)Cache被占滿時(shí),一些舊的數(shù)據(jù)就會(huì)被沖刷替換掉。 可以看出,無(wú)論是時(shí)間局部性還是空間局部性,array1.c都要比array2.c好很多!相比array1.c,array2.c會(huì)觸發(fā)大量的Cache Miss,這也是為什么array2的性能會(huì)如此之差! 下面我們用perf來(lái)對(duì)比下這兩個(gè)程序的性能指標(biāo)。 用perf觀測(cè)性能指標(biāo)perf是Linux提供的一個(gè)功能強(qiáng)大的實(shí)用性能調(diào)優(yōu)工具,它可以用來(lái)觀測(cè)幾乎所有CPU相關(guān)的性能指標(biāo),但perf工具本身,不是本文重點(diǎn),以后會(huì)有專(zhuān)門(mén)文章詳細(xì)介紹perf的使用。 先獲取array1.c的性能指標(biāo): 再看下array2.c的性能指標(biāo): 這里,我們只關(guān)注一個(gè)最重要的性能指標(biāo):L1 Data Cache的Miss率。 對(duì)比一下,array1.c的L1-dcache-load-misses率只有3.10%, 而按列訪問(wèn)的array2.c,則達(dá)到了驚人的93.03%!這就是兩者性能差異如此巨大的真相! 小結(jié)Cache是CPU內(nèi)部最為重要的部件之一,也是影響程序性能的最重要因素之一,但由于各種原因,經(jīng)常被程序員忽視。在進(jìn)行性能調(diào)優(yōu)時(shí),除了系統(tǒng)架構(gòu)、算法等方面的考量之外,不妨也從CPU的角度去思考一下,比如Cache,有時(shí)會(huì)對(duì)程序性能產(chǎn)生決定性影響。 該文章在 2023/10/30 9:23:16 編輯過(guò) |
關(guān)鍵字查詢(xún)
相關(guān)文章
正在查詢(xún)... |