計(jì)算的概念
計(jì)算在數(shù)學(xué)上的概念: 計(jì)算是一種行為,通過已知量的可能的組合,獲得新的量。計(jì)算的本質(zhì)是集合之間的映射。
個(gè)人粗淺直白的理解是: 輸入一個(gè)或多個(gè)數(shù)據(jù),經(jīng)過處理,輸出一個(gè)或多個(gè)數(shù)據(jù)。如 1 + 2 就是一個(gè)計(jì)算,輸入 2 個(gè)數(shù)據(jù),輸出 1 個(gè)數(shù)據(jù) 3。
那到這里就會(huì)有很多疑問,在計(jì)算機(jī)上:
- 高性能計(jì)算的概念是什么?
- 全部的普通計(jì)算都能轉(zhuǎn)成高性能計(jì)算實(shí)現(xiàn)么?如果不是的話,那哪些類型的計(jì)算可以呢?
- 我們需要做哪些事情,來實(shí)現(xiàn)高性能計(jì)算?
- Android框架或者是否存在第三方庫為我們做了相關(guān)的工作
高性能計(jì)算的概念
高性能計(jì)算:通常使用很多處理器(作為單個(gè)機(jī)器的一部分)或者某一集群中組織的幾臺(tái)計(jì)算機(jī)(作為單個(gè)計(jì)算資源操作)的計(jì)算系統(tǒng)和環(huán)境。
在移動(dòng)端,我們可以認(rèn)為是通過同時(shí)啟用移動(dòng)設(shè)備的 CPU 和 GPU 構(gòu)成的異構(gòu)計(jì)算資源,進(jìn)行協(xié)同計(jì)算。
計(jì)算模型類型
從數(shù)據(jù)流和指令的角度把計(jì)算模型分為4類(費(fèi)林分類法)
- 單指令單數(shù)據(jù)流 (SISD): 非并行計(jì)算的模型,典型例子就是單核
CPU,所有數(shù)據(jù)都被一個(gè)處理器順次處理,某一時(shí)刻只能使用一個(gè)指令。 - 單指令多數(shù)據(jù)流 (SIMD): 指多個(gè)不同的數(shù)據(jù)同時(shí)被相同的執(zhí)行、指令集或者算法處理,是
GPU的計(jì)算模型。 - 多指令單數(shù)據(jù)流 (MISD): 在同一個(gè)數(shù)據(jù)流上執(zhí)行不同的指令。
- 多指令多數(shù)據(jù)流 (MIMD): 是多核CPU的計(jì)算模型。
本文內(nèi)容討論的高性能計(jì)算則主要是在 SIMD 的基礎(chǔ)上討論,但這里并不需要嚴(yán)格按照 SIMD,只需要計(jì)算流程中的一部分內(nèi)容符合 SIMD 我們就認(rèn)為該實(shí)現(xiàn)過程就是一個(gè)高性能計(jì)算。
知道了計(jì)算模型的類型,我們就能知道并不是所有的計(jì)算類型都能實(shí)現(xiàn)為高性能計(jì)算。只有滿足以下要求的算法(或者算法中的部分滿足,其他部分通過CPU協(xié)調(diào))才能夠比較好的實(shí)現(xiàn)為高性能計(jì)算。
- 每個(gè)數(shù)據(jù)(數(shù)據(jù)包)都需要經(jīng)過相同的流程來處理
- 數(shù)據(jù)之間并沒有相干性,即某些數(shù)據(jù)的計(jì)算不依賴另外一些數(shù)據(jù)的計(jì)算結(jié)果
- 數(shù)據(jù)量龐大
如何實(shí)現(xiàn)高性能計(jì)算
這里首先了解的是圖形顯示流程,常用的通用計(jì)算也正是基于這個(gè)顯示流程做修改而實(shí)現(xiàn)的。這里以O(shè)penGL ES為例,其他的如Direct3D、CG的流程大體也相同。
OpenGL ES 2.0可編程渲染管線:
頂點(diǎn)緩沖對(duì)象
↑
API → 基本處理 → 頂點(diǎn)著色器 → 圖元裝配 → 光柵化 → 片元著色器 → 裁剪測(cè)試
→ 深度測(cè)試&模板測(cè)試 → 顏色緩沖混合 → 抖動(dòng) → 幀緩沖
其中的頂點(diǎn)著色器和片元著色器的處理過程,程序猿可以自行編寫,且是分別在 GPU 中的頂點(diǎn)處理器和片元處理器(或者統(tǒng)一處理器)計(jì)算。
知道了這個(gè)流程,我們可以很容易聯(lián)想到:
- 我們的高性能計(jì)算的主要算法過程是在
頂點(diǎn)著色器或片元著色器中處理的,一般都是片元著色器。 - 這個(gè)流程是用于顯示,輸入是頂點(diǎn)和紋理等數(shù)據(jù),輸出是幀緩沖,很明顯并不是我們所需要的,因此我們還需要修改流程。
修改后的計(jì)算流程圖:
頂點(diǎn)緩沖對(duì)象
↑
紋理數(shù)據(jù) (頂點(diǎn)數(shù)據(jù)和紋理內(nèi)部數(shù)據(jù)) → 基本處理 → 頂點(diǎn)著色器 → {{圖元裝配}} → 光柵化 → 片元著色器
→ {{裁剪測(cè)試}} → {{深度測(cè)試&模板測(cè)試}} → {{顏色緩沖混合}} → {{抖動(dòng)}} → 紋理
其中 {{ }} 顯示的部分并不是我們關(guān)心的內(nèi)容,我們的程序會(huì)經(jīng)過這幾步驟,但邏輯上一般并不用生效。
public int[] increase(int[] input) {
for (int i = 0; i < input.length; i++) {
input[i]++;
}
return input;
}
這里的處理過程還是很模糊,對(duì)比一下上面的常規(guī)計(jì)算 (普通計(jì)算左,高性能計(jì)算右):
- 輸入 int 數(shù)組 → 輸入紋理數(shù)據(jù)
- for 循環(huán)語句 → 片段著色器
- 返回int數(shù)組 → 渲染到紋理(具體對(duì)應(yīng)幀緩存對(duì)象
FBO),并讀取 - 調(diào)用 → 繪制矩形
- 數(shù)組處理范圍 → 坐標(biāo)
為了保證我們輸出到紋理的數(shù)據(jù)是完整正確的,另外需要注意的是:
- 繪制的矩形應(yīng)該與投影平面平行,即正對(duì)攝像機(jī)
- 使用正交投影
- 矩形和紋理等大
- 視口和紋理圖等大
性能提升效果
前面介紹了這么多,但終究只是理論介紹,我們并沒有看到使用高性能計(jì)算究竟提升了多少。
寫一個(gè)非常簡(jiǎn)單的圖像處理的算法 (因?yàn)槭褂脠D像暫時(shí)效果比較明顯,表達(dá)也比較容易,所以這里使用的是圖像顯示的 Demo,并不是說高性能計(jì)算只能用于顯示相關(guān))
算法基本流程是:
- 讀入一張圖片
- 光順處理下,即將每個(gè)像素點(diǎn)和周圍的8個(gè)像素點(diǎn)的顏色做一個(gè)平均,并將均值賦值給中間像素點(diǎn)
- 將各個(gè)像素點(diǎn)置灰,即將每個(gè)像素點(diǎn)的rgb值求和并取平均值
- 調(diào)整亮度,即將每個(gè)像素點(diǎn)的顏色的各通道值乘以0.9
- 將像素?cái)?shù)組取出設(shè)置給bitmap,并設(shè)置給ImageView
常規(guī)計(jì)算 ( java 代碼) 實(shí)現(xiàn)
public class ImageGrayUtil {
// 給出最終求和時(shí)的加權(quán)因子(為調(diào)整亮度)
public static final float scaleFactor = 0.9f;
public static Bitmap apply(Context context, Bitmap sentBitmap) {
Bitmap bitmap = sentBitmap.copy(Bitmap.Config.ARGB_8888, true);
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int[] pix = new int[w * h];
bitmap.getPixels(pix, 0, w, 0, 0, w, h);
// 卷積內(nèi)核中各個(gè)位置的值
float kernalValue = 1.0f/9.0f;
float k00 = kernalValue;
float k10 = kernalValue;
float k20 = kernalValue;
float k01 = kernalValue;
float k11 = kernalValue;
float k21 = kernalValue;
float k02 = kernalValue;
float k12 = kernalValue;
float k22 = kernalValue;
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
// 獲取卷積內(nèi)核中各個(gè)元素對(duì)應(yīng)像素的顏色值
int[] p00 = mutiply(getARGB(pix, w, h, i - 1, j - 1), k00);
int[] p10 = mutiply(getARGB(pix, w, h, i, j - 1), k10);
int[] p20 = mutiply(getARGB(pix, w, h, i + 1, j - 1), k20);
int[] p01 = mutiply(getARGB(pix, w, h, i - 1, j), k01);
int[] p11 = mutiply(getARGB(pix, w, h, i, j), k11);
int[] p21 = mutiply(getARGB(pix, w, h, i + 1, j), k21);
int[] p02 = mutiply(getARGB(pix, w, h, i - 1, j + 1), k02);
int[] p12 = mutiply(getARGB(pix, w, h, i, j + 1), k12);
int[] p22 = mutiply(getARGB(pix, w, h, i + 1, j + 1), k22);
int[] pixARGB = add(p00, p10, p20, p01, p11, p21, p02, p12, p22);
setColor(pix, w, h, i, j, uniform(toGray(pixARGB)));
}
}
bitmap.setPixels(pix, 0, w, 0, 0, w, h);
return bitmap;
}
// 獲取顏色各通道值
private static int[] argbFromColor(@ColorInt int color) {
int[] argb = new int[4];
argb[0] = Color.alpha(color);
argb[1] = Color.red(color);
argb[2] = Color.green(color);
argb[3] = Color.blue(color);
return argb;
}
private static int getColor(int[] pix, int w, int h, int x, int y) {
if (x < 0 || x > w - 1) return 0;
if (y < 0 || y > h - 1) return 0;
return pix[y * w + x];
}
private static int[] setColor(int[] pix, int w, int h, int x, int y, int[] argb) {
if (x < 0 || x > w - 1) return pix;
if (y < 0 || y > h - 1) return pix;
pix[y * w + x] = Color.argb(argb[0], argb[1], argb[2], argb[3]);
return pix;
}
// 獲取顏色置灰,排除alpha通道
private static int[] toGray(int[] argb) {
int v = 0;
for (int i=1; i<argb.length; i++) {
v += argb[i];
}
v /= 3;
return new int[]{v, v, v, v};
}
// 獲取顏色各通道值
private static int[] getARGB(int[] pix, int w, int h, int x, int y) {
int color = getColor(pix, w, h, x, y);
return argbFromColor(color);
}
// 將數(shù)組的各元素和factor相乘
private static int[] mutiply(int[] argb, float factor) {
for (int i = 0; i < argb.length; i++) {
argb[i] = (int) (argb[i] * factor);
}
return argb;
}
// 將數(shù)組相加
private static int[] add(int[]... argbs) {
int[] result = new int[4];
for (int i = 0; i < argbs.length; i++) {
for (int j = 0; j < 4; j++) {
result[j] += argbs[i][j];
}
}
return result;
}
// 將顏色各通道值限制在0-255之間
private static int[] uniform(int[] argb) {
argb[0] = 255;
for (int i = 0; i < argb.length; i++) {
if (argb[i] < 0) argb[i] = 0;
if (argb[i] > 255) argb[i] = 255;
argb[i] *= scaleFactor;
}
return argb;
}
}
在系統(tǒng) 5.1.1 的 Nexus 5 手機(jī),對(duì) 142KB 的正方形 png 圖片做處理,實(shí)現(xiàn)結(jié)果如下:

說明:
上面的圖片是輸入圖片,下面的圖片是輸出圖片,顯示的處理時(shí)間是 4234.676ms
高性能計(jì)算實(shí)現(xiàn)
片元處理器代碼 gray_blur_f.glsl (用于處理數(shù)據(jù)) 如下:
precision mediump float;//給出默認(rèn)的浮點(diǎn)精度
varying vec2 vTexCoord;//從頂點(diǎn)著色器傳遞過來的紋理坐標(biāo)
uniform sampler2D sTexture;//紋理內(nèi)容數(shù)據(jù)
uniform vec2 uPxD; // pixel delta values
void main() {
// 給出卷積內(nèi)核中各個(gè)元素對(duì)應(yīng)像素相對(duì)于待處理像素的紋理坐標(biāo)偏移量
vec2 offset0=vec2(-1.0,-1.0); vec2 offset1=vec2(0.0,-1.0); vec2 offset2=vec2(1.0,-1.0);
vec2 offset3=vec2(-1.0,0.0); vec2 offset4=vec2(0.0,0.0); vec2 offset5=vec2(1.0,0.0);
vec2 offset6=vec2(-1.0,1.0); vec2 offset7=vec2(0.0,1.0); vec2 offset8=vec2(1.0,1.0);
// 給出最終求和時(shí)的加權(quán)因子(為調(diào)整亮度)
const float scaleFactor = 0.9;
//卷積內(nèi)核中各個(gè)位置的值
float kernelValue = 1.0/9.0;
float kernelValue0 = kernelValue; float kernelValue1 = kernelValue; float kernelValue2 = kernelValue;
float kernelValue3 = kernelValue; float kernelValue4 = kernelValue; float kernelValue5 = kernelValue;
float kernelValue6 = kernelValue; float kernelValue7 = kernelValue; float kernelValue8 = kernelValue;
// 獲取卷積內(nèi)核中各個(gè)元素對(duì)應(yīng)像素的顏色值
vec4 p00 = texture2D(sTexture, vTexCoord + offset0.xy * uPxD.xy) * kernelValue0;
vec4 p10 = texture2D(sTexture, vTexCoord + offset1.xy * uPxD.xy) * kernelValue1;
vec4 p20 = texture2D(sTexture, vTexCoord + offset2.xy * uPxD.xy) * kernelValue2;
vec4 p01 = texture2D(sTexture, vTexCoord + offset3.xy * uPxD.xy) * kernelValue3;
vec4 p11 = texture2D(sTexture, vTexCoord + offset4.xy * uPxD.xy) * kernelValue4;
vec4 p21 = texture2D(sTexture, vTexCoord + offset5.xy * uPxD.xy) * kernelValue5;
vec4 p02 = texture2D(sTexture, vTexCoord + offset6.xy * uPxD.xy) * kernelValue6;
vec4 p12 = texture2D(sTexture, vTexCoord + offset7.xy * uPxD.xy) * kernelValue7;
vec4 p22 = texture2D(sTexture, vTexCoord + offset8.xy * uPxD.xy) * kernelValue8;
//顏色求和
vec4 clr = p00 + p01 + p02 +
p10 + p11 + p12 +
p20 + p21 + p22;
// 灰度化
float hd = (clr.r + clr.g + clr.b) / 3.0;
//進(jìn)行亮度加權(quán)后將最終顏色傳遞給管線
gl_FragColor = vec4(hd) * scaleFactor;
}
java 代碼 (用于讀取數(shù)據(jù)并生成 bitmap) 如下:
public Bitmap saveFrameBufferToBitmap(int w, int h) {
resultBm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
IntBuffer ib = ByteBuffer.allocateDirect((int) (w * h * 4))
.order(ByteOrder.nativeOrder()).asIntBuffer();
IntBuffer ibt = ByteBuffer.allocateDirect((int) (w * h * 4))
.order(ByteOrder.nativeOrder()).asIntBuffer();
ib.rewind();
ibt.rewind();
// 強(qiáng)制刷新數(shù)據(jù)至紋理緩沖區(qū)
GLES20.glFinish();
// 從紋理緩沖區(qū)讀取數(shù)據(jù)
GLES20.glReadPixels(0, 0, w, h, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, ib);
// 數(shù)據(jù)上下翻轉(zhuǎn)
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
ibt.put((h - i - 1) * w + j, ib.get(i * w + j));
}
}
resultBm.copyPixelsFromBuffer(ibt);
return resultBm
}
在系統(tǒng) 5.1.1 的 Nexus 5 手機(jī),對(duì) 142KB 的方形 png 圖片做處理,實(shí)現(xiàn)結(jié)果如下:

說明:
上面的圖片是輸入圖片,下面的圖片是輸出圖片,顯示的處理時(shí)間是533.0869ms (這里的時(shí)間并不僅僅包含片元處理器的代碼執(zhí)行時(shí)間,也已經(jīng)包括從紋理緩存對(duì)象中讀取數(shù)據(jù)并生成Bitmap的時(shí)間,讀取數(shù)據(jù)的時(shí)間也會(huì)占用比較多的時(shí)間)
總結(jié)
綜上,可以看到使用高性能計(jì)算的計(jì)算效率比普通的計(jì)算要提高了將近7倍,無愧于高性能這幾個(gè)字。雖然 533.0869ms 也還是遠(yuǎn)大于 Android 16ms 的屏幕刷新時(shí)間,但我們可以通過其他手段處理,如在其他線程處理,或者根據(jù)特殊業(yè)務(wù)需求先做預(yù)處理,如高斯模糊效果實(shí)現(xiàn)方案及性能對(duì)比這篇文章中通過預(yù)先壓縮圖片的方式,讓模糊算法的時(shí)間降低至 16ms 以內(nèi)。
既然高性能計(jì)算的效率優(yōu)勢(shì)如此明顯,很奇怪現(xiàn)在的移動(dòng)端開發(fā)很少使用這套東西。
這里也必須客觀的提一下高性能計(jì)算的缺點(diǎn),根據(jù)自己粗淺的認(rèn)知總結(jié)如下:
- 符合高性能計(jì)算要求的算法要求較為苛刻,需要大數(shù)據(jù)輸入,且數(shù)據(jù)處理過程之間無關(guān)聯(lián)
說實(shí)話,在移動(dòng)開發(fā)過程中,除了圖像處理,其他的業(yè)務(wù)邏輯中真的很少能想到有什么需求需要這么做。很多時(shí)候,圖像處理也是交給服務(wù)器(如 nos )處理了。當(dāng)然通過
url告訴服務(wù)器處理,那圖片的加載速度就依賴于網(wǎng)絡(luò)請(qǐng)求,如果產(chǎn)品策劃無法忍受這一點(diǎn)的話,就可以考慮是否要使用本地高性能計(jì)算了
- 整個(gè)流程,需要了解圖形渲染管線的流程,需要知道如何設(shè)置攝像機(jī)、投影模式,如何觸發(fā)離屏渲染等等圖形相關(guān)的知識(shí),且需要知道一種
shader language
上面的示例中,并沒有給出顯示設(shè)置相關(guān)的代碼,而這部分代碼也較為冗長(zhǎng)難寫。對(duì)于一般的移動(dòng)端開發(fā)同學(xué)其實(shí)并不關(guān)心或者不感興趣這些內(nèi)容。所幸的是,在
Android端已經(jīng)出現(xiàn)了RanderScript,將上述的復(fù)雜流程極度簡(jiǎn)化了。
-
GPU是SIMD計(jì)算模型的核心,而GPU相比CPU的特點(diǎn)是GPU并沒有復(fù)雜的緩存系統(tǒng)、分支預(yù)測(cè)系統(tǒng)和各種控制邏輯,而是使用芯片上大多數(shù)的晶體管作為純計(jì)算單元??梢?,GPU 并不能處理復(fù)雜的計(jì)算邏輯。
對(duì)于程序員來說,比較明顯的一點(diǎn),就是大部分版本的
shader language并不支持循環(huán)語句,當(dāng)然以后會(huì)不會(huì)有就不得而知了
- 使用高性能計(jì)算后,各種機(jī)型適配也可能會(huì)是一個(gè)比較頭疼的事情
對(duì)于一些過時(shí)的老機(jī)器很可能沒有 GPU,其次各個(gè) GPU 制造廠商的的產(chǎn)品對(duì)
OpenGL ES的支持程度也會(huì)有少許差別。當(dāng)然,這些支持的差異,一般這邊也不會(huì)涉及到。還有,因?yàn)?高性能計(jì)算是利用了圖形渲染管線的,很容易知道其應(yīng)用程序的Android SDK要不低于 2.2。
其實(shí)移動(dòng)端的高性能計(jì)算的使用還是相對(duì)少見,至少我支持的幾個(gè) Android 項(xiàng)目都沒有涉及到,本文僅僅是作為科普講解下一點(diǎn)點(diǎn)基礎(chǔ),讓大家了解下什么是高性能計(jì)算,相信即便是使用過 RenderScript 的Android開發(fā)同學(xué),也可能不清楚什么是高性能計(jì)算。
因?yàn)楸疚闹v述的僅僅是基礎(chǔ),如果有大牛同學(xué)有自己的看法的話,歡迎指正出來