移動(dòng)端高性能計(jì)算基礎(chǔ)


計(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è)備的 CPUGPU 構(gòu)成的異構(gòu)計(jì)算資源,進(jìn)行協(xié)同計(jì)算。

計(jì)算模型類型

從數(shù)據(jù)流和指令的角度把計(jì)算模型分為4類(費(fèi)林分類法)

  1. 單指令單數(shù)據(jù)流 (SISD): 非并行計(jì)算的模型,典型例子就是單核 CPU,所有數(shù)據(jù)都被一個(gè)處理器順次處理,某一時(shí)刻只能使用一個(gè)指令。
  2. 單指令多數(shù)據(jù)流 (SIMD): 指多個(gè)不同的數(shù)據(jù)同時(shí)被相同的執(zhí)行、指令集或者算法處理,是 GPU 的計(jì)算模型。
  3. 多指令單數(shù)據(jù)流 (MISD): 在同一個(gè)數(shù)據(jù)流上執(zhí)行不同的指令。
  4. 多指令多數(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ì)算。

  1. 每個(gè)數(shù)據(jù)(數(shù)據(jù)包)都需要經(jīng)過相同的流程來處理
  2. 數(shù)據(jù)之間并沒有相干性,即某些數(shù)據(jù)的計(jì)算不依賴另外一些數(shù)據(jù)的計(jì)算結(jié)果
  3. 數(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)想到:

  1. 我們的高性能計(jì)算的主要算法過程是在 頂點(diǎn)著色器片元著色器 中處理的,一般都是 片元著色器。
  2. 這個(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ì)算右):

  1. 輸入 int 數(shù)組 → 輸入紋理數(shù)據(jù)
  2. for 循環(huán)語句 → 片段著色器
  3. 返回int數(shù)組 → 渲染到紋理(具體對(duì)應(yīng)幀緩存對(duì)象 FBO),并讀取
  4. 調(diào)用 → 繪制矩形
  5. 數(shù)組處理范圍 → 坐標(biāo)

為了保證我們輸出到紋理的數(shù)據(jù)是完整正確的,另外需要注意的是:

  1. 繪制的矩形應(yīng)該與投影平面平行,即正對(duì)攝像機(jī)
  2. 使用正交投影
  3. 矩形和紋理等大
  4. 視口和紋理圖等大

性能提升效果

前面介紹了這么多,但終究只是理論介紹,我們并沒有看到使用高性能計(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é)果如下:

image

說明:

上面的圖片是輸入圖片,下面的圖片是輸出圖片,顯示的處理時(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é)果如下:

image

說明:

上面的圖片是輸入圖片,下面的圖片是輸出圖片,顯示的處理時(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)化了。

  • GPUSIMD 計(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é)有自己的看法的話,歡迎指正出來

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容