滄東 - 如何在圖可視化場景下進行性能優(yōu)化

點擊上方藍字關(guān)注我們
前端早早聊大會,前端成長的新起點,與掘金聯(lián)合舉辦。加微信 codingdreamer 進大會專屬周邊群,贏在新的起跑線。
第二十九屆|前端可視化專場,了解數(shù)據(jù)可視化/時空可視化/大屏/搭建/畫布/等等的可能性,7-17 全天直播,10 位講師(貝殼/奇安信/預(yù)策科技/螞蟻/小米/阿里/阿里云/數(shù)字冰雹等等),點我上車?? (報名地址):
所有往期都有全程錄播,可以購買年票一次性解鎖全部
正文如下
本文是第十八屆 - 前端早早聊性能優(yōu)化專場,也是早早聊第 128 場,來自 阿里 螞蟻-滄東 的分享。
開場

今天帶來的分享和可視化場景有關(guān),是關(guān)于圖的可視化,如果有對圖可視化這些場景可能不太了解的,我待會會舉一些例子,會給大家簡單描述一下我們的場景大概是長什么樣子,以及我們之前在業(yè)務(wù)里在這些場景下都遇到了哪些性能問題,我們有一些對應(yīng)的優(yōu)化手段,這些大概今天會分享的內(nèi)容。
這里面可能會涉及到前端同學(xué)可能會較少接觸到的工具或者場景,我們的優(yōu)化手段也不會局限在 Web-Worker 等等這些常見的手段,可能更多的會借助 GPU 的能力去做一些針對性的性能優(yōu)化方案。
自我介紹

我的花名叫滄東,來自螞蟻金服的體驗技術(shù)部,下面是我的知乎的個人主頁,可能會在上面不定期的分享一些和可視化相關(guān)的一些文章,或者一些其他技術(shù)方面。如果大家有興趣的話可也可以關(guān)注一下。
首先我們來舉一個圖可視化場景例子。
圖可視化場景

在這樣一個例子里面,如果忽略掉這些和業(yè)務(wù)非常相關(guān)的操作,可以看到視頻里有很多的節(jié)點,它們之間會有關(guān)系,我們會把節(jié)點和節(jié)點之間的關(guān)系用這個邊來表示,它們會有很多種不同的布局方案。
布局就是指把每個節(jié)點和邊放在哪個位置,在不同的布局之間,可以進行任意的切換。常見的布局有力導(dǎo)布局 ,可以在不同布局之間互相切換。這個就是一個常見的圖可視化場景。我們需要有一些性能非常高的手段,幫助用戶在這樣的場景里做一些圖相關(guān)的分析工作。
性能優(yōu)化方案

在圖可視化場景下,遇到的性能問題,可以分成三種類型。拋開圖可視化場景,在大多數(shù)可視化場景下,遇到的性能問題分別是計算的問題,渲染的問題以及交互的問題。
高性能計算
圖領(lǐng)域中的常見算法

在計算里會遇到什么樣的性能問題呢?在這個圖可視化領(lǐng)域中,經(jīng)常會用到一些常見的算法,大致可以分成兩類:
第一類叫 布局算法,就剛剛在演示視頻里看到的,數(shù)據(jù)里面會包含很多的節(jié)點以及它們的關(guān)系。要做的就是根據(jù)節(jié)點和邊的關(guān)系,算出每個節(jié)點的位置,并把它畫出來。常見的是力導(dǎo)布局。 第二類叫分析算法,常見的一個計算任務(wù)可能是在一個圖里,需要查找一些最短路徑,或者還有一些配置 Rank 等等,有大量的分析算法。
布局算法介紹

在布局算法中,我們會遇到什么問題?力導(dǎo)布局大概的表現(xiàn)就是這個節(jié)點和節(jié)點之間會有不同種類,不同非常多種力的作用下,會計算得到每個節(jié)點的位置??从疫呥@張圖,以中間這個節(jié)點為例,它會受到三種類型的力的作用:
第一種,它會受到 重力 的影響,在這個場景里它可能是指向我們的坐標原點,在 Gravity 還會受到重力的影響。 第二種,它會受到 吸引力 的影響,以中間這個節(jié)點為例,它會受到左邊這個點對它的吸引力,把它往左邊進行拉伸。 第三種,它會受到 排斥力的影響,它會受到下方這個節(jié)點給它往上頂?shù)倪@樣一個排斥力的作用。
布局算法

結(jié)合這三種不同類型的力的共同作用下,計算出每個階段的位置,計算每一個節(jié)點都需要計算這么多種力,跟它周圍這么多節(jié)點進行計算,它的算法復(fù)雜度是非常高的,運算規(guī)模也是很大,自然性能也會不好。
以具體的一個例子來看, Fruchterman 是力道布局中的一種。它的算法復(fù)雜度是相對比較高的, V 是一個節(jié)點的數(shù)目,E 就是邊的數(shù)目,它的算法復(fù)雜度大概是節(jié)點的數(shù)目乘以邊的數(shù)目,再乘以一個迭代次數(shù)。
迭代次數(shù)是為了使布局達到穩(wěn)定,需要重復(fù)的運算一定的數(shù)量,通常這個值可能會在比如 1000 、2000 都有可能,視不同的業(yè)務(wù)來定。
這個算法復(fù)雜度其實已經(jīng)相當高了。左邊這張圖就實際的效果,它的節(jié)點可能會有 200 多,邊可能會有 1000 多,迭代次數(shù)可能會有 3000 左右。在這樣的算法復(fù)雜度下,如果我們在 CPU 端進行運算,會造成一個什么樣的結(jié)果呢?
它的整個耗時會達到 30 秒,大家可以直接訪問上面的 demo 鏈接,它的體驗是很不佳的。
這還是在已經(jīng)進行了一定的優(yōu)化的基礎(chǔ)上,仍然需要 30 秒。我們不希望用戶在這 30 秒之內(nèi)什么都做不了,只能在這傻等。因此首先會嘗試把這堆復(fù)雜的運算丟到 WebWorker 里。
把這些計算放在 Worker 里,會有一個好處,雖然還是需要等 30 秒,但在這個過程中主線程的一些交互,比如頁面的滾動可以不受影響,用戶至少還能瀏覽頁面的其他內(nèi)容。但是這 30 秒也確實很長,這就涉及到我們今天的主題了。
使用 GPU 進行通用計算

我們可以跳出 CPU 的限制,嘗試用 GPU 去解決一些高復(fù)雜度的計算問題,用 GPU 進行計算這個概念,其實早在 20 年前就有科學(xué)家提出了,最初 GPU 發(fā)明出來肯定是用于渲染圖形或網(wǎng)頁,但是當時就有叫 Mark Harris 的科學(xué)家,就提出說 GPU 的計算能力這么強大,是不是可以不僅僅用它來做渲染,也可以用它來做一些通用計算。
他當時就提出了用 GPU 進行通用計算,簡稱 GPGPU 的概念。他提出這樣一個概念之后,后面的很多年,有大量這方面的實踐,例如一些視頻的編解碼、仿真等等。
GPU 和 CPU 的性能對比

GPU 和 CPU 之間的性能到底差多少呢?
這張圖是非常直觀的。它表達了 GPU 和 CPU 在浮點數(shù)的運算效率上差距會越來越大。
適合并行計算的 GPU

發(fā)展到今天,GPU 其實是更強了。比如 NVidia 或者 AMD 的 GPU,尤其是 NVidia ,這個行業(yè) GPU 行業(yè)的龍頭老大,為了在 GPU 中去做更多的計算,甚至把 TENSOR CORE 專門用來做光線追蹤的硬件。這樣的 GPU 處理,比如說張量的計算,可以簡單理解為一些矩陣的運算,以及做光線追蹤的時候,由于硬件加速的加持,它會比 CPU 更適合做這樣的任務(wù),效率是更高的。
可并行的矩陣乘法

到底什么是可并行的計算 ?到底什么是可并行 ?
舉一個簡單例子,假如說現(xiàn)在我們想實現(xiàn)兩個矩陣相乘或者兩個矩陣相加。用 JS 去寫,一種最樸素的寫法,大概就是這樣:我們可能會拿到這兩個矩陣,分別去遍歷它們里面的元素,依次相乘。大家都會寫這樣的兩層嵌套的循環(huán)來完成這樣一個矩陣乘法。
但是大家仔細想一想,當這個算法在 CPU 中按順序執(zhí)行的時候,它其實會有一個性能問題,就在于什么呢?
當我在計算第一個矩陣和第二個矩陣,它們中的第一個元素相乘的時候,其實同時就可以計算它們第二個元素相乘。
大家可以想一下這個問題,就是我后續(xù)的矩陣中每個元素的運算,其實它是可以并行計算,我并不需要像我們寫的這樣一個串行算法一樣,必須要等到第一個元素算完了之后,再去算第二個元素,再算第三個元素。
當我們的矩陣的維度非常高的時候,我們計算時間他不就顯然會拉長了嗎?
但是很幸運的是矩陣乘法它本身是可并行的,是什么意思?
如果從 GPU 的視角來看,或者從線程可并行的思路,去嘗試解決這樣的問題,就會寫出類似這樣的代碼。
給每一個線程分配一個任務(wù),讓每一個線程只負責去計算這兩個矩陣中的某一個元素,比如第一個線程你就去計算第一個,第二個線程的你就去計算第二個,你們這么多個線程的,你們可以并行同時的去計算,最后再把結(jié)果匯總起來。這樣就是一個線程可并行的概念。這種思路如果總結(jié)下來,就是可以讓多個線程去執(zhí)行相同的程序。
如果這么多線程全去處理一樣的數(shù)據(jù),它是沒有意義的,必須要每個線程能夠處理各自的數(shù)據(jù)。這樣的程序在 CUDA 中會叫核函數(shù)。
GPGPU

分配很多任務(wù)給每一個線程,讓這么多線程一起并行的去計算,它的速運算速度效率可能會高。
在當今并行計算這個領(lǐng)域的絕對王者,可能是 Media。它其實也在 GPGPU 概念提出之后不久,它就推出了自家的這樣一個叫 CUDA 的通用的并行計算的平臺,支持很多種語言,比如 Java、Python、C、C++,沒有 JS ,因為它是需要在 GPU 中運行的,它提出 CUDA 的概念之后,后面就有很多在 CUDA 基礎(chǔ)上的應(yīng)用就應(yīng)運而生。
在圖領(lǐng)域中比較關(guān)心的一個叫 nvGRAPH 的應(yīng)用,它也是基于 CUDA 實現(xiàn)的,它里面就會內(nèi)置很多各種各樣的圖分析算法,最短路徑的計算、PageRank 等等,它基于 GPU 實現(xiàn)的算法的執(zhí)行效率是非常高??梢栽谒墓倬W(wǎng)上看到,它宣稱自己是可以支持 20 億條邊,叫大規(guī)模的數(shù)據(jù)運算 。
在 Web 端使用 GPGPU

CUDA 有這么多優(yōu)秀的實現(xiàn),甚至它有很多高性能算法都已經(jīng)實現(xiàn)好了。作為前端,在 Web 端如果也想使用 GPU 做并行計算的能力,我們有什么樣的手段呢?
目前在 Web 端大概只能通過兩類 API:
第一類,最常用的可能就是 WebGL ,它的好處就是相對來說它的瀏覽器的兼容性是比較好。不管是做渲染,像 Three.js 或者 Babylon.js 這樣的渲染引擎,可以在手機上運行或者 tensorflow 等等。它們其實都是可以在手機端或 PC 端,但是它缺點就是能力是比較有限。 第二類,相對比較新的一類 API ,叫做 WebGPU 。相比 WebGL,它可能是更適合做 GPU 計算的一種新的規(guī)范。它目前是屬于實驗中,它的好處就是他相對外表他比較新,所以它的能力會比較豐富。
已有的實踐

在 Web 端高性能計算比較有名的實現(xiàn)是 tensorflow.js。它相當于把關(guān)于這些模型或者張量的運算,可以讓前端開發(fā)者在瀏覽器中也能實現(xiàn)了,就可以做一些基于面部的識別,會有你畫我猜的應(yīng)用,可以在手機上或瀏覽器上完成了。
tensorflow.js 提供給開發(fā)者的,它的 API 是相對比較簡單的。作為使用者來說,你不需要去關(guān)心我背后的算法到底是跑在 GPU 上還是跑在 CPU 上?tensorflow.js 會自動幫你選擇它的后端。
WebGL 的實現(xiàn)原理

WebGL 去做 GPGPU 它的實現(xiàn)原理大概是怎樣的?
這張圖就是在 CPU 和 GPU 它們之間做計算,有一個比較粗略的對位關(guān)系。
在 CPU 里面通常會把用于計算的數(shù)據(jù)放在一個數(shù)組里,剛剛舉的矩陣乘法的例子,可能會用一個數(shù)組去表示一個矩陣,到了 GPU 這邊,它是沒有所謂的這些數(shù)組或者對象,它是沒有這樣的存儲的這樣一個概念,對 GPU 來說,它所能理解的就是紋理或者圖片,可以把圖片塞給 GPU 它也可以渲染出一張圖片或者紋理輸出,數(shù)組和圖片是有這樣一個對位關(guān)系的。
在 CPU 里面經(jīng)常會寫很多的循環(huán),剛剛在矩陣乘法那個例子里也看到,我們會寫嵌套的循環(huán)去做計算。到了 GPU 這邊,就基本上不會去寫循環(huán)這樣一件事情。在 GPU 看來,它會去給每一個屏幕上每一個像素點去分配一個統(tǒng)一的計算任務(wù),讓這么多項數(shù)一起渲染出來,一起去執(zhí)行這樣一段計算邏輯,也是實現(xiàn)了類似于循環(huán)的類似的效果,它們是從思路上是一致的。
對于 CPU 來說,我們?nèi)フ{(diào)用一段寫好的程序,在 JS 里面就能立刻拿到結(jié)果。對于 GPU 來說,它渲染了一張圖形之后,會把結(jié)果渲染到紋理,我們可以從紋理中拿到每個像素點的數(shù)據(jù)。
關(guān)于計算調(diào)用,可能是在 CPU 這端,如果用 JS 去寫,那就是 JS 的執(zhí)行引擎會幫助我們做這樣一件事情。
對于 GPU 來說,我們寫的計算程序是通過光柵化去分配給每個像素點完成的。
布局算法

回到剛剛那個例子,在執(zhí)行這個 F 打頭的一個布局算法,我們可能在 CPU 這端需要將近 30 秒的時間。當我們把這個算法挪到 GPU 里去實現(xiàn),大家可以看到它的耗時就來到了 0.8 秒,這基本上是將近幾十倍的這樣一個提升。
之所以可以把這樣的算法放在 GPU 完成,是因為類似于這樣的布局算法,它其實都是可并行的。
在計算第一個點位置的時候,其實可以同時上第二個線程去計算第二個點。如果這里面有 200 個節(jié)點,就可以分配 200 個線程給 GPU ,然后讓它們同時的進行線程上的并行運算,最后把結(jié)果返回給我們。我們可以再用 Canvas 或者用 SVG ,一切你想用的渲染手段,我們只需要在指定的點去用這些渲染手段把點和邊畫出來就行了。
它的瓶頸,至少在布局算法這個場景里,性能的瓶頸通常并不來自于渲染,而是來自于計算。
實現(xiàn)細節(jié)

怎么在 GPU 里去做這樣一個布局運算,以及一些簡單的實現(xiàn)細節(jié)。這里不會涉及到具體的實現(xiàn)代碼,會簡單的講一下思路,怎么去實現(xiàn)這樣的算法。
圖存儲
鄰接矩陣

在 GPU 里去對這張圖做一些運算,就是怎么在 GPU 里去存儲這樣一張圖,如果在 CPU 這邊去寫代碼的話,對圖的結(jié)構(gòu)可以定義的很靈活,可以定義很多個節(jié)點,它可能是一個對象,那個節(jié)點是一個對象,每條邊我也可以把它定義成一個對象。那節(jié)點和節(jié)點之間的關(guān)系,可以通過邊距連接。
在 GPU 這邊,它是不認識這些對象、數(shù)組,我們需要找到某種線性的結(jié)構(gòu)去把這張圖存在 CPU 里,然后才能進行后續(xù)的運算。首先,能想到的第一種比較常見的存儲一張圖的方式叫做鄰接矩陣,這張圖其實是來自于維基百科,每個節(jié)點會有一個編號,它們之間的關(guān)系是用這個邊去把它連接在一起的,對應(yīng)到右邊這張連接矩陣,就可以這樣表示,它和左邊這張圖是有一一對應(yīng)關(guān)系。
鄰接矩陣的好處就是比較直觀,它用一個二維的矩陣,就可以表示出節(jié)點和邊之間的關(guān)系。但缺點也很明顯,這里面有很多為 0 的元素,0 就代表什么?
就代表這個節(jié)點和對應(yīng)的另外的一個節(jié)點,它之間是沒有關(guān)系的。如果圖是比較稀疏的,矩陣里其實就會有大量空白的元素沒有被充分的利用到,它其實是非常消耗存儲的,非常消耗 GPU 內(nèi)存的。
鄰接表

在實際的使用當中,通常不會選擇鄰接矩陣矩陣來存儲一張圖,會采取一種叫做鄰接表的結(jié)構(gòu),來存儲這張圖,它的好處相比鄰接矩陣會更加的緊湊,相對來說定義也會比較靈活。
鄰接表簡單理解為一個數(shù)組。在數(shù)組的前半部分,用來存儲節(jié)點,后半部分用來存儲邊,其中的每一個元素,每一個元素在 GPU 看來就是一個像素點,每一個像素點又有 RGBA 4 個通道,可以充分的利用這 4 個通道去存儲內(nèi)容。
對于節(jié)點來說,它的 R 通道,就是紅色通道,可以用來存儲節(jié)點的 X 坐標,綠色通道,可以用來存儲它的 Y 坐標,藍色通道可以存儲偏移量,偏移量是什么呢?
節(jié)點和節(jié)點之間不是有邊做連接,偏移量就表示 GPU 到時候來尋址的時候,在第幾個元素就找到這個節(jié)點對應(yīng)的界面,通過偏移量來表示這樣的一個比較緊湊的連接表的存儲,可以進一步壓縮 GPU 內(nèi)存。
性能對比

GPU 和 CPU 在布局算法下到底會有多大的性能差異?
這張表來自于 G6 里面,其實兩里面對比了兩種不同的布局算法,在右邊第二列可以看出在大多數(shù)情況下或者在節(jié)點和邊的數(shù)目來到一個比較大的規(guī)模的情況下,GPU 比 CPU 通常來說都是會有一些比較明顯的優(yōu)勢的。
但是當你的節(jié)點數(shù)和邊的數(shù)目不那么大,第一個 GFore 的一個布局算法里面,節(jié)點數(shù)可能三十幾個邊,邊數(shù)可能六十條邊,GPU 算的還沒 CPU 快,反而更慢。
這張圖表達的就是在選擇不同的算法實現(xiàn)的時候,選 GPU 和 CPU 的時候,是要根據(jù)具體的業(yè)務(wù)場景,不是說一定就是 GPU 算的就一定比 CPU 快,它會有一些限制和要求。
當然在大規(guī)模圖的計算場景下,GPU 的效果那是不言而喻的,甚至高的會達到百倍這樣的一個性能差異。
WebGL 的局限性

WebGL 去做 GPU 計算它會有哪些局限性?
如果對 WebGL 比較有些了解的同學(xué)可能會聽說過 WebGL 它本質(zhì)是用來做渲染的,比如我們大家熟悉的 Three.js 等等其他的一些渲染引擎,都是用來做渲染的,這是它的本意。
但是我們卻用它來做計算,這就會造成一些局限性:
第一個局限性,相比一些專用的用來做計算的管線,它的渲染管線實際上是會有很多冗余的。在做具體的計算任務(wù)的時候,很多階段其實都是沒有必要的。 第二個局限性,WebGL 在做計算的時候有很多的特性,沒辦法支持,這也就導(dǎo)致我們的很多可并行的算法沒法實現(xiàn)。 第三個局限性,WebGL 的底層是 OpenGL,也有將近 20 多年的歷史了,它的底層依賴的原生 API 是相對老舊的,不管是一些特性或者本身的執(zhí)行效率,相比一些新推出的底層 API 也會有一定的劣勢,這也是 WebGL 的一些局限性。
舉兩個例子,共享內(nèi)存和同步 ,在進行線程間并行的時候,在大多數(shù)情況下,線程和線程之間是可以去獨立的做計算的。
但也有些場景,我們希望在線程間做通信或者做同步。這種時候如果用 WebGL 去實現(xiàn),其實就滿足不了。
Web 端的下一代圖形 API

Web 端下一代的圖形 API 到底是什么?
從兩年前開始, W3C 和 Chrome 等等瀏覽器廠商,他們就在主推 Web 端的下一代圖形 API,叫 WebGPU,在他的描述里,可以直接看到 WebGPU 被提出一個很重要的特點,需要用 WebGPU 來做 GPU 的計算,它相比 WebGL 是更加合適的。
目前它的發(fā)展狀態(tài)是什么樣的?
現(xiàn)在基本上已經(jīng)可以在 Chrome 或者它的預(yù)覽版本、 Safari 的預(yù)覽版本、Firefox 的預(yù)覽版本等等這些實驗版本中其實已經(jīng)可以應(yīng)用到 WebGPU 了。
WebGPU vs WebGL

簡單的對比下 WebGPU 和 WebGL,我們其實只想用用 GPU 來做計算,可能并不關(guān)心渲染,這就要求你底層 API 需要提供給我專門的計算管線。
右邊這張圖是來自包墾,另外一個底層的渲染 API,可以簡單對比一下,左邊的是它的渲染管線,里面有很多個步驟非常長,而右邊是它的對應(yīng)的計算管線就非常短。
從管線的長度上直觀的也能看出,如果這個 API 能直接提供給開發(fā)者一個專用的計算管線,它能跳過中間很多我們不需要的步驟,自然它的計算速度也會很快,這是一個比較直觀的理解。

第二點就是 WebGPU 相比 WebGL ,它的底層的渲染或者是它底層的原生 API 是更加先進的。
這個表格里其實就對比的 WebGL 和 WebGPU 它的一些差異,它們當然有共同點了,都是瀏覽器提供的 API,前端開發(fā)者都可以直接拿來用,但是它們底層的原生 API 就完全不同,以 WebGL 舉例,它底層的原生 API 可能是 20 年前的 OpenGL 或者是 DirectX11 等等比較老的。
對于 WebGPU 來說,它底層的原生 API 渲染可就非常先進了。在 MAC上面我們會使用 Metal,蘋果上的 metal,在 Windows 下,就可能可以使用到微軟最新的 D13、D12 等等,底層的 API 的性能差距,其實就會體現(xiàn)在上層瀏瀏覽器 API 的渲染性能以及計算性能上面的差距。
圖分析算法
單源最短路徑

什么是圖分析算法?
舉一個例子,以右邊這張圖為例,可能有 A、B、C、D、E 等 5 個點,它們之間會有一個距離,有這樣一個需求,從 A 點出發(fā),到 C 點的最短距離是什么?
可以從 A 到 B 再到 C,也可以從 A 到 D 再到 C 一個距離是 3,一個距離是 4,我們很容易就計算出從 A 點到 C 點,最短距離是 3 ,從 A 點到 C 點途經(jīng) B 點,最短距離就是 3 了。對于這樣的一個最短路徑算法來說,它的復(fù)雜度其實也是蠻高的。
如果用一個比較簡單的單元最短路徑的算法來實現(xiàn)的話,它的復(fù)雜度也會是節(jié)點數(shù)乘以邊數(shù)。這樣的算法可以在 CPU 中進行,也有很多這樣的庫,包括 G6 本身它也已經(jīng)實現(xiàn)了這樣的最短路徑的算法,但是相比 GPU 來說,它肯定會相對較慢。我們這個例子是比較簡單的,只有 5 個點,可以想象一下,當這個圖的規(guī)模非常大,有幾百上千甚至上萬個點的時候,我們?nèi)フ乙粋€點到另外一個點的最短路徑可就沒這么快了。

如果把這個算法放在 GPU 里,會多快的這樣的一個效果。這個是在 GPU 里的實現(xiàn)。這個圖里會有 1000 多個節(jié)點,2000 多條邊,使用了剛剛講過的 WebGPU 去實現(xiàn)在 Chrome 的實驗板中去運行,可以發(fā)現(xiàn)它的速度是非??斓?,這個例子里面大概只需要 62 毫秒,立刻就可以拿到從 3 號點到 2 號點的一個最短路徑的效果。
高性能渲染

現(xiàn)在圖里面肯定不光有計算,算完之后還得把節(jié)點、邊都得畫出來,這里面就涉及到一個渲染問題。當你要渲染的點或者邊非常多的時候,也會遇到類似的性能問題。解決方法大概會有哪些?
使用底層渲染 API

舉個簡單的例子, WebGL、Canvas 2D、SVG 的一些渲染的性能對比,這個例子是比較直觀的。
這個例子就是可以在 WebGL 和 Canvas 之間進行一個切換,這邊會選舉大概 3000 個節(jié)點,讓他做某種運動的動畫,去對比它的幀數(shù),就 FPS 在 WebGL 下,基本上你看 65、70 相對都是可以滿載運行。但我們一旦如果切換到 Canvas ,它的幀率會迅速來到,可能只有 5 或者 6,給人的直觀感覺就是非??D。無論是再去做畫布的移動、拾取都會非??ā?/p>
如果在渲染上遇到瓶頸,最直觀的方法去使用更加底層的渲染 API 。如果使用 Canvas、SVG 去實現(xiàn)的,換成 WebGL、WebGPU 性能會更好。
按需渲染

當有 10 萬條數(shù)據(jù),我們一起把它渲染出來,那肯定是會非??D的。但實際上用戶在瀏覽器里又不會同時去看這 10 萬條。我們會有一個虛擬列表,每次只會渲染出有線條,從這 10 萬條里面選擇可 100 條或 1000 條。
在渲染方面,其實也是有類似的思路。在場景里可能有成千上萬個對象,右邊這張圖所示有成千上上萬個立方體或者球體,但是我屏幕或者視口用戶能看到的部分總歸是有限的,就沒有必要在每一幀都把這些成千上萬的對象全都畫出來。
可以在 CPU 端做一個計算,就把用戶看不到的對象,都先把它剔除掉。待到在實際渲染的時候,只渲染用戶能看到的去提交給 GPU 去渲染的,GPU 的壓力就小了。
這個是一個比較樸素和直觀的想法,基本上在所有的渲染引擎,不管是在 Web 端或者在桌面端 Three.js 或者 Unity 都會使用。
在渲染引擎里,這種手段叫做裁剪 。通過在 CPU 端去計算出一個最小的渲染集合,丟給 GPU去做渲染,這樣一個過程叫裁剪 。其實不光是 3D 渲染引擎了,在 Canvas 2d 也可以做類似的事情。但是相比 3D 場景,我2D 做包圍盒的計算肯定會更加簡單,也會更加快。
GPU 粒子動畫

在很多的可視化場景中是會需要用到動畫效果。SANDDANCE 來自于微軟的一個非常著名的產(chǎn)品,可以看到 SANDDANCE 基本上是可以在不同的布局之間進行切換,從一個柱狀圖立馬切換到一個以地圖為布局的場景,可以在不同產(chǎn)品之間進行切換。
右邊也是可以在不同的維度非常流暢的切換,如果想實現(xiàn)這樣的效果,使用 D3或者基于 SVG 或者 Canvas 的渲染引擎,去做粒子動畫,當粒子數(shù)量非常多的時候,用 Canvas 或者 SVG, 顯然會非常卡頓,達不到流暢的效果。在微軟的 SANDDANCE 里面,他們是怎么做的呢?
他們基本上是會把整個動畫以及整個粒子的位置放在 GPU 中運行,才能達到在每一幀都能看到流暢的布局切換的效果。
高性能交互

如何在渲染里面實現(xiàn)高性能,大概三個思路
第一個是使用底層的 API,不管是 WebGL 也好,WebGPU 也好。 第二個就是去嘗試做按需渲染的工作,盡可能的減少 GPU 需要繪制的對象。 第三個就是當需要去實現(xiàn)一些非常復(fù)雜、炫酷的動畫效果的時候,確認用 WebGL 或者 WebGPU 的底層去做。
解放主線程

在圖可視化場景下的交互,用戶經(jīng)常需要去做一些交互,那常見的交互有哪些呢?
可能是需要對畫布進行縮放,放大、縮小、平移、拖拽、選中某一個節(jié)點、選中某一條邊。當節(jié)點和邊的規(guī)模非常大的時候,是否還能保證交互的流暢性,這個是需要去思考的一個問題。
離屏渲染

JS 腳本可能通常都非常大,主線程是非常忙碌的,一方面需要去渲染 UI ,一方面又要去做計算,另外一方面還有具體的業(yè)務(wù)邏輯,主線程要做這么多事情,非常忙碌,忙于做計算的過程中,如果去滾動頁面,可能就會感覺到頁面非??D。
其實這種卡頓就是由于主線程太忙碌了,忙不過來了造成。有一個比較普遍的解決方案叫做 OffscreenCanvas,即離屏渲染 。
把圖的計算和渲染丟給一個離屏的 Canvas ,它是可以跑在 Web-Worker 當中的,交給他去做。等離屏的 Canvas 在 Worker 中完成計算和渲染任務(wù)的時候,再把結(jié)果同步給主線程,就能立刻拿到結(jié)果。主線程就不需要去等待計算和渲染的完成,就可以去處理用戶的一些交互,在用戶視角看來,整個的頁面的滾動都是非常流暢的。

舉個實際使用的例子,例子可以在 MDN 或者網(wǎng)上都能找到,可以搜索關(guān)鍵詞,比如怎么使用離屏渲染,或者 OffscreenCanvas 就能搜索到類似的方案。
它的思路其實非常簡單,對于前端開發(fā)者來說,有主線程和 Worker 線程兩個視角。在主線程這邊,和創(chuàng)建一個普通的 Canvas 不一樣,先要去創(chuàng)建一個離屏 Canvas,通過 transfer control to off screen API 能夠把 Canvas 交給一個離屏的線程;第二步,和其他創(chuàng)建 Worker 的時候的方法一樣,需要去創(chuàng)建一個 Worker;它的差別就在這第三步,在創(chuàng)建完 Worker 之后,可以把離屏 Canvas 作為參數(shù),或者作為一個 transferable 可轉(zhuǎn)移的這樣一個對象去傳遞給 Worker 。實際上在離屏渲染里面,可以看到它的參數(shù),可以直接把整個離屏 Canvas 從主線程去傳遞給 Worker 線程。
回到 Worker 線程的視角,現(xiàn)在已經(jīng)能夠拿到從主線上傳過來的 Canvas,就可以在主線程去調(diào)用 Canvas 的 API 一樣,可以去創(chuàng)建 WebGL 的上下文,調(diào)用常規(guī)的繪制。這些和在主線上都是完全一樣的,只不過在最后一步,在完成了渲染和計算之后,需要把結(jié)果再同步給主線程,需要調(diào)用一個可逆的方法,這樣才能在主線上拿到了。
離屏渲染其實也不是多新的技術(shù),在很多的 Web 端的 3D 引擎都可以看到。它們其中有一個特性就是支持離屏渲染,也有很多這樣的例子。用 Three.js 去做渲染的對象非常多,可以把它丟給 Worker 去渲染,Worker 在每一陣完成渲染任務(wù)之后,通過 Commit 把確認結(jié)果同步給主線上,這樣的主線程還能進行一些非常消耗性能的 UI 操作或交互。
在圖可視化里面的例子中,基本它的效果是這樣。比如說主線程可以去渲染 AntD 的一個 Loading 組件是完全不受影響的。非常消耗性能的布局運算,實際上是在 Worker 端計算完成的。我們的主線程可以在此過程中,你可以去滾動或者去渲染其他的 UI 組件,都是沒有問題。如果我們沒有采用這樣的手段,大家可以想象我的主線程就會非???,因為我的計算結(jié)果還沒完成,可能 Loading 就會一直卡在他的初始狀態(tài)。
除了我們剛剛提到的離屏渲染,我們在這個圖場景或者大部分的可視化場景里面,用戶總歸是需要去做一些交互的。
高效拾取

最常見的一個交互就是拾取,什么是拾???
比如說大家可以看下面來自化點主要的例子,它這個場景里面大家看到有很多成千上萬個立方體,用戶的鼠標在移動的過程中,我們需要知道用戶當前鼠標停留在哪個對象上,我們需要去確切的把它找出來,這個過程就叫做拾取。
如果找這個對象,根據(jù)坐標找對象這樣的一個計算過程,我們?nèi)绻欠旁?CPU 端進行,我們需要去進行大量的叫包圍和計算,需要在場景里面每一個對象全部都遍歷一遍,才能判斷出來你當前要拾取的是對象,可想而知,他的性能是非常差的,與之相對的我們的拾取其實也是可以放在 GPU 這邊完成的。
在很多這樣的 3D 引擎里面,會把這兩種方法,一個叫做可能叫 Repeating,一個可能叫 Pixel picking,基于顏色編碼或者叫基于像素的時序,會要在 GPU 端進行許的好處。
顯然大家在剛剛這個例子也看到了,我鼠標可以任意的在場景中進行滑動,可以看到它的拾取基本上是比較實時的。他這個對象是能很好的跟隨鼠標的移動,并且把它高亮出來。

我現(xiàn)在可以簡單總結(jié)一下,我之前就以我的開發(fā)經(jīng)歷來講的話,我覺得我們前端開發(fā)者在去用 GPU 不管是做計算還是做渲染的時候,經(jīng)常會遇到的一些困難有哪些?
我總結(jié)下來可能包含這么兩類,第一個就是當我們前端開發(fā)者想用 GPU 去做計算的時候,畢竟我們只是前端,我們可能缺少一些 GPU 的編程模型的理解或者是實現(xiàn)。
相比那些比如說用 CURD 去做編程的人來說,我們會缺少比如說線程的線程組他們是什么意思,GPU 的內(nèi)存模型是什么樣,更不要說一些更加高級的特性,比如共享內(nèi)存和同步這些是什么?我們前端開發(fā)者因為很少接觸,所以自然對這些概念也不是很了解。
所以即使我們知道計算任務(wù)是很適合在 GPU 這端完成的,我們也很難去寫這樣一個程序,或者很難實現(xiàn)這樣一個算法。這是我覺得我們可能會遇到的第一個困難。
第二個困難,我們現(xiàn)在對編程模型也有理解了,算法我們也能實現(xiàn)了。
下一步就是在實現(xiàn)過程中,我們有這些圖形渲染或者計算的 API 需要去學(xué)習,比如說我們可能前端需要去學(xué)習 WebGL ,或者是相對比較新的 WebGPU,他們的 API 我們需要去學(xué),因為實際的算法我們是需要寫在 Shader 里的,這邊多提一嘴就 Shader 是什么?
實際上大家可以理解為就是寫給 GPU 看的程序,只有 GPU 才看得懂的,所以它的語法和我們寫的比如 JS 是會有很大的區(qū)別的。
他可能從語言風格上來講,Shader 的語法更接近于 C 的語法?;旧鲜?C 的語法。我理解主要是有一些學(xué)習成本,我們后面也在想怎么去降低前端開發(fā)者,去寫這樣的或者是去和 GPU 打交道,我們想辦法去降低這樣的成本,我們也會有一些方案了。
大家如果感興趣的話可以去訪問,下面這個鏈接里面會有說我們目前大概做法,我們盡量想讓前端開發(fā)者少去學(xué)習,尤其是 Shader 在這語法這方面,我們會讓我們的前端直接去寫 TS 的程序,通過一些編譯器去把我們的 TS 程序直接編譯成 Shader 的語法,然后放在 GPU 端進行,這樣就減少大家的一些學(xué)習成本。
對于一些編程模型在我們的 WebGPU 也會有些介紹,如果大家感興趣的話也會去學(xué)習一下。
總結(jié)

來到最后我們就簡單總結(jié)一下今天內(nèi)容。我覺得當我們在可視化或者說圖可視化里面遇到一些性能瓶頸的時候,我覺得我們第一步還是要先定位他的問題,我們首先要分析一下,性能瓶頸到底來自于哪些方面。
比如它不一定就是說,我們第一感覺你這性能平均肯定是渲染有問題。我就無腦的去用一些更加底層的渲染 API 解決,其實我們在之前的布局算法例子里也看到了,其實就 200 多個節(jié)點,你用 Canvas 是畫也很快,用 SVG 也很快,他的瓶頸壓根就不是在渲染上面。
在我們遇到這樣的問題的時候,我們第一步先要分析出它是來自于計算,來自于訓(xùn)練還是交互,我們其實都有對應(yīng)的解決方案,當我們定位了問題之后,我們需要去依據(jù)我們具體的這些業(yè)務(wù)場景去選擇方案,并不一定就是無腦說 WebGL 或者說 WebGPU 就一定以 Canvas 2D 或者 SVG 更加適合我們場景,這是不一定的。
我們剛才在性能分析里我們大家也看到了,可能在比如你的節(jié)點和邊的數(shù)目下,就在圖場景上,節(jié)點和邊的數(shù)目還比較小的情況下,在 CPU 中去算,它甚至比 GPU 更快。大家可能很好奇,其實為什么?因為大家可以簡單理解,我在 GPU雖然它計算是比較快的,但是它相對的你需要去創(chuàng)建紋理,去調(diào)度這個過程,以及最后我們會需要在 CPU 端你還要去讀 GPU 的計算結(jié)果,這些步驟其實也是非常消耗性能的。
所以大家要根據(jù)自己的業(yè)務(wù)場景實際的去斟酌一下,去選擇合適的方案。聽完分享,大家即使對那些復(fù)雜的算法或者你的業(yè)務(wù)里不會遇到,或者說也沒有這樣的圖可視化場景,也希望能給大家?guī)硪稽c小小的啟示。當我們發(fā)現(xiàn)我們的算法,它其實是一個可并行算法的時候,我們其實就已經(jīng)可以嘗試在 GPU 中去做這樣一件事情了。通常它的效果尤其是當你的數(shù)據(jù)量達到一定規(guī)模的時候,通常它的效果都會比在 CPU 中來得好。
歡迎加入

我們來自螞蟻金服,在可視化方面會有很多產(chǎn)品,它們組成了一個叫做 AntV 的產(chǎn)品矩陣。
推薦一門課

我們傳統(tǒng)是給大家可以推薦一本書,這里我就推薦一門課,大家在 B 站上就可以直接免費看。
它是閆令琪博士的一門課:「計算機圖形學(xué)入門」,里面不涉及比如 WebGL 或者 OpenGL 的 API 怎么用,而會講這些 API 背后的一些思路。如果你能夠堅持的把這樣一門基礎(chǔ)入門課看完,當你想用比如 Three.js 或者 WebGL 等等去做一些和 3D 有關(guān)的計算和渲染相關(guān)的工作的時候,你要做的可能就只是看 Three.js 的文檔。因為已經(jīng)有之前的一些基礎(chǔ)知識做打底了,你在學(xué)習這些 API 的時候,速度也會更快,也更容易理解。
Thanks

別忘了 7-17(下周六) 的第二十九屆|前端可視化專場,了解數(shù)據(jù)可視化/時空可視化/大屏/搭建/畫布/等等的可能性,7-17 全天直播,10 位講師(貝殼/奇安信/預(yù)策科技/螞蟻/小米/阿里/阿里云/數(shù)字冰雹等等),點我上車?? (報名地址):
所有往期都有全程錄播,可以購買年票一次性解鎖全部


掃碼二維碼
獲取更多精彩
前端早早聊

點個在看你最好看


