新聞中心
在本文中,我們將討論創(chuàng)建和訪問(wèn)數(shù)據(jù)的方式可能對(duì)應(yīng)用程序性能的影響。

介紹
JavaScript是一種非常高級(jí)的語(yǔ)言,在使用JavaScript開(kāi)發(fā)的時(shí)候不必對(duì)存儲(chǔ)器中的數(shù)據(jù)存儲(chǔ)方式作過(guò)多的考慮。在本文中,我們將探討數(shù)據(jù)如何存儲(chǔ)在內(nèi)存中,以及JavaScript中分發(fā)和訪問(wèn)數(shù)據(jù)的方式將如何影響CPU和內(nèi)存的性能。
浪漫三角
當(dāng)計(jì)算機(jī)需要進(jìn)行一些計(jì)算任務(wù)時(shí),計(jì)算機(jī)處理單元(CPU)需要數(shù)據(jù)進(jìn)行處理,因此,根據(jù)手中的任務(wù),它將發(fā)送請(qǐng)求到內(nèi)存以通過(guò)總線獲取待處理的數(shù)據(jù),就像下面這樣:
這就是我們的浪漫三角
CPU->總線->內(nèi)存
浪漫三角需要第四個(gè)元素來(lái)保持穩(wěn)定
由于CPU比內(nèi)存快得多,因此從CPU->總線->內(nèi)存->總線->CPU這樣的處理方式就浪費(fèi)了很多計(jì)算時(shí)間,因?yàn)椴檎覂?nèi)存時(shí),CPU處于空閑狀態(tài)而無(wú)法執(zhí)行其他操作。
緩存的出現(xiàn)有效的緩解了這個(gè)問(wèn)題。在本文中我們不會(huì)詳細(xì)討論緩存的技術(shù)細(xì)節(jié)和類(lèi)型,我們只需要知道緩存可以作為CPU的一個(gè)內(nèi)部存儲(chǔ)空間。
當(dāng)CPU接收到要運(yùn)行的命令時(shí),它將首先在高速緩存中搜索目標(biāo)數(shù)據(jù),如果沒(méi)有搜索到目標(biāo)數(shù)據(jù),它再通過(guò)總線發(fā)起請(qǐng)求。
然后,總線將請(qǐng)求的數(shù)據(jù)加上一部分內(nèi)存,將其存儲(chǔ)在高速緩存中以供CPU快速調(diào)用。
這樣一來(lái),CPU就能夠有效的處理數(shù)據(jù),而不會(huì)浪費(fèi)時(shí)間等待內(nèi)存返回。
高速緩存的引用也可能導(dǎo)致新的問(wèn)題
基于上面的架構(gòu),我們?cè)谔幚泶罅繑?shù)據(jù)時(shí)可能會(huì)出現(xiàn)一種名為”高速緩存未命中”的錯(cuò)誤。
高速緩存未命中意味著在計(jì)算期間,CPU發(fā)現(xiàn)高速緩存中沒(méi)有必要的數(shù)據(jù),因此需要通過(guò)常規(guī)通道(即已知的慢速存儲(chǔ)器)來(lái)請(qǐng)求此數(shù)據(jù)。
上圖是一個(gè)很好的實(shí)例,在處理組中數(shù)據(jù)是,由于計(jì)算的數(shù)據(jù)超出了緩存限制的數(shù)據(jù),就導(dǎo)致了緩存未命中。
可是這跟我作為JavaScript程序員有什么關(guān)系呢?
好問(wèn)題,大多數(shù)情況下,我們JavaScript開(kāi)發(fā)人員不必關(guān)心這個(gè)問(wèn)題。但是隨著越來(lái)越多的數(shù)據(jù)涌入Node.js服務(wù)器甚至富客戶端,所以當(dāng)使用JavaScript遍歷大型數(shù)據(jù)集時(shí)就容易遇見(jiàn)緩存未命中的問(wèn)題。
一個(gè)經(jīng)典的例子
接下來(lái)讓我們以一個(gè)例子作為說(shuō)明。
這是一個(gè)叫做Boom的類(lèi):
- class Boom {
- constructor(id) {
- this.id = id;
- }
- setPosition(x, y) {
- this.x = x;
- this.y = y;
- }
- }
此類(lèi)(Boom)僅具有3個(gè)屬性:id,x和y。
現(xiàn)在,讓我們創(chuàng)建一個(gè)填充x和y的方法。
讓我們構(gòu)建設(shè)置:
- const ROWS = 1000;
- const COLS = 1000;
- const repeats = 100;
- const arr = new Array(ROWS * COLS).fill(0).map((a, i) => new Boom(i));
現(xiàn)在,我們將在一種方法中使用此設(shè)置:
- function localAccess() {
- for (let i = 0; i < ROWS; i++) {
- for (let j = 0; j < COLS; j++) {
- arr[i * ROWS + j].x = 0;
- }
- }
- }
本地訪問(wèn)的作用是線性遍歷數(shù)組并將x設(shè)置為0。
如果我們重復(fù)執(zhí)行此功能100次(請(qǐng)查看設(shè)置中的重復(fù)常數(shù)),則可以測(cè)量運(yùn)行時(shí)間:
- function repeat(cb, type) {
- console.log(`%c Started data ${type}`, 'color: red');
- const start = performance.now();
- for (let i = 0; i < repeats; i++) {
- cb();
- }
- const end = performance.now();
- console.log('Finished data locality test run in ', ((end?-?start) / 1000).toFixed(4), ' seconds');
- return end?-?start;
- }
- repeat(localAccess, 'Local');
日志輸出是這樣的:
丟失緩存要付出的代價(jià)
現(xiàn)在,根據(jù)上面的了解,如果我們處理迭代過(guò)程中距離較遠(yuǎn)的數(shù)據(jù),則會(huì)導(dǎo)致緩存丟失。遠(yuǎn)處的數(shù)據(jù)是不在相鄰索引中的數(shù)據(jù),如下所示:
- function farAccess() {
- for (let i = 0; i < COLS; i++) {
- for (let j = 0; j < ROWS; j++) {
- arr[j * ROWS + i].x = 0;
- }
- }
- }
在這里發(fā)生的是,在每次迭代中,我們都處理上次迭代距ROWS的索引。因此,如果ROWS為1000(在我們的例子中),我們將得到以下迭代:[0,1000,2000,…,1,1001,2001,…]。
讓我們將其添加到速度測(cè)試中:
- repeat(localAccess, 'Local');
- setTimeout(() => {
- repeat(farAccess, 'Non Local');
- }, 2000);
這是最終結(jié)果:
非本地迭代速度幾乎慢了4倍。隨著數(shù)據(jù)量的增加,這種差異將越來(lái)越大。發(fā)生這種情況的原因是由于高速緩存未命中,CPU處于空閑狀態(tài)。
那么您要付出的代價(jià)是什么?這完全取決于您的數(shù)據(jù)大小。
好吧,我發(fā)誓我永遠(yuǎn)不會(huì)那樣做!
您通??赡懿贿@么認(rèn)為,但在某些情況下,您可能會(huì)希望使用非線性(例如1,2,3,4,5)或非偶然性。比如( for (let i = 0; i < n; i+=1000))
例如,您從服務(wù)或數(shù)據(jù)庫(kù)中獲取數(shù)據(jù),并需要通過(guò)某種復(fù)雜的邏輯對(duì)數(shù)據(jù)進(jìn)行排序或過(guò)濾。這可能導(dǎo)致訪問(wèn)數(shù)據(jù)的方式與farAccess函數(shù)中顯示的方式類(lèi)似。
如下所示:
查看上圖,我們看到了存儲(chǔ)在內(nèi)存中的數(shù)據(jù)(頂部灰色條)。在下面,我們看到了當(dāng)數(shù)據(jù)從服務(wù)器到達(dá)時(shí)創(chuàng)建的數(shù)組。最后,我們看到排序后的數(shù)組,其中包含對(duì)存儲(chǔ)在內(nèi)存中各個(gè)位置的對(duì)象的引用。
這樣,對(duì)排序后的數(shù)組進(jìn)行迭代可能會(huì)導(dǎo)致在上面的示例中看到的多個(gè)緩存未命中。
請(qǐng)注意,此示例適用于小型陣列。高速緩存未命中與更大的數(shù)據(jù)有關(guān)。
在當(dāng)今世界中,您需要在前端使用精美的動(dòng)畫(huà),并且可以在后端(無(wú)服務(wù)器或其他服務(wù)器)中為CPU的每毫秒時(shí)間計(jì)費(fèi)(這很關(guān)鍵)。
不好了!都沒(méi)了!!!
并不是,有多種解決方案可以解決此問(wèn)題,現(xiàn)在您已經(jīng)知道造成性能下降的原因,您可以自己考慮解決方案。比如只需要將處理在一起的數(shù)據(jù)更緊密地存儲(chǔ)在內(nèi)存中。
這種技術(shù)稱(chēng)為數(shù)據(jù)局部性設(shè)計(jì)模式。
讓我們繼續(xù)我們的例子。假設(shè)在我們的應(yīng)用程序中,最常見(jiàn)的過(guò)程是使用farAccess函數(shù)中顯示的邏輯來(lái)遍歷數(shù)據(jù)。我們希望對(duì)數(shù)據(jù)進(jìn)行優(yōu)化,以使其在最常見(jiàn)的for循環(huán)中快速運(yùn)行。我們將像這樣排列相同的數(shù)據(jù):
- const diffArr = new Array(ROWS * COLS).fill(0);
- for (let col = 0; col < COLS; col++) {
- for (let row = 0; row < ROWS; row++) {
- diffArr[row * ROWS + col] = arr[col * COLS + row];
- }
- }
所以現(xiàn)在在diffArr中,原始數(shù)組中索引為[0,1,2,…]的對(duì)象現(xiàn)在被設(shè)置為[0,1000,2000,…,1,1001,2001,…,2, 1002,2002,…]。數(shù)字表示對(duì)象的索引。這模擬了對(duì)數(shù)組進(jìn)行排序的方法,這是實(shí)現(xiàn)數(shù)據(jù)局部性設(shè)計(jì)模式的一種方法。
為了方便測(cè)試,我們將稍微更改farAccess函數(shù)以獲得一個(gè)自定義數(shù)組:
- function farAccess(array) {
- let data = arr;
- if (array) {
- data = array;
- }
- for (let i = 0; i < COLS; i++) {
- for (let j = 0; j < ROWS; j++) {
- data[j * ROWS + i].x = 0;
- }
- }
- }
現(xiàn)在將場(chǎng)景添加到我們的測(cè)試中:
- repeat(localAccess, 'Local');
- setTimeout(() => {
- repeat(farAccess, 'Non Local')
- setTimeout(() => {
- repeat(() => farAccess(diffArr), 'Non Local Sorted')
- }, 2000);
- }, 2000);
我們運(yùn)行它,我們得到:
我們已經(jīng)優(yōu)化了數(shù)據(jù),以適應(yīng)需要查看的更常見(jiàn)的方式。
分享題目:關(guān)于JavaScript的的高速緩存未命中分析
分享網(wǎng)址:http://www.dlmjj.cn/article/dhgssis.html


咨詢(xún)
建站咨詢(xún)
