@zyl06
2017-02-11T16:33:39.000000Z
字数 7807
阅读 1256
Android
计算在数学上的概念: 计算是一种行为,通过已知量的可能的组合,获得新的量。计算的本质是集合之间的映射。
个人粗浅直白的理解是: 输入一个或多个数据,经过处理,输出一个或多个数据。如 1 + 2
就是一个计算,输入 2 个数据,输出 1 个数据 3
。
那到这里就会有很多疑问,在计算机上:
高性能计算:通常使用很多处理器(作为单个机器的一部分)或者某一集群中组织的几台计算机(作为单个计算资源操作)的计算系统和环境。
在移动端,我们可以认为是通过同时启用移动设备的 CPU
和 GPU
构成的异构计算资源,进行协同计算。
从数据流和指令的角度把计算模型分为4类(费林分类法)
CPU
,所有数据都被一个处理器顺次处理,某一时刻只能使用一个指令。GPU
的计算模型。本文内容讨论的高性能计算则主要是在 SIMD 的基础上讨论,但这里并不需要严格按照 SIMD,只需要计算流程中的一部分内容符合 SIMD 我们就认为该实现过程就是一个高性能计算。
知道了计算模型的类型,我们就能知道并不是所有的计算类型都能实现为高性能计算。只有满足以下要求的算法(或者算法中的部分满足,其他部分通过CPU协调)才能够比较好的实现为高性能计算。
这里首先了解的是图形显示流程,常用的通用计算也正是基于这个显示流程做修改而实现的。这里以OpenGL ES为例,其他的如Direct3D、CG的流程大体也相同。
顶点缓冲对象
↑
API → 基本处理 → 顶点着色器 → 图元装配 → 光栅化 → 片元着色器 → 裁剪测试
→ 深度测试&模板测试 → 颜色缓冲混合 → 抖动 → 帧缓冲
其中的顶点着色器
和片元着色器
的处理过程,程序猿可以自行编写,且是分别在 GPU 中的顶点处理器和片元处理器(或者统一处理器)计算。
知道了这个流程,我们可以很容易联想到:
顶点着色器
或 片元着色器
中处理的,一般都是 片元着色器
。
顶点缓冲对象
↑
纹理数据 (顶点数据和纹理内部数据) → 基本处理 → 顶点着色器 → {{图元装配}} → 光栅化 → 片元着色器
→ {{裁剪测试}} → {{深度测试&模板测试}} → {{颜色缓冲混合}} → {{抖动}} → 纹理
其中 {{ }}
显示的部分并不是我们关心的内容,我们的程序会经过这几步骤,但逻辑上一般并不用生效。
public int[] increase(int[] input) {
for (int i = 0; i < input.length; i++) {
input[i]++;
}
return input;
}
这里的处理过程还是很模糊,对比一下上面的常规计算 (普通计算左,高性能计算右):
FBO
),并读取为了保证我们输出到纹理的数据是完整正确的,另外需要注意的是:
前面介绍了这么多,但终究只是理论介绍,我们并没有看到使用高性能计算究竟提升了多少。
写一个非常简单的图像处理的算法 (因为使用图像暂时效果比较明显,表达也比较容易,所以这里使用的是图像显示的 Demo,并不是说高性能计算只能用于显示相关)
算法基本流程是:
public class ImageGrayUtil {
// 给出最终求和时的加权因子(为调整亮度)
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);
// 卷积内核中各个位置的值
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++) {
// 获取卷积内核中各个元素对应像素的颜色值
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);
}
// 将数组的各元素和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;
}
// 将数组相加
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;
}
}
在系统 5.1.1 的 Nexus 5 手机,对 142KB 的正方形 png 图片做处理,实现结果如下:
说明:
上面的图片是输入图片,下面的图片是输出图片,显示的处理时间是 4234.676ms
片元处理器代码 gray_blur_f.glsl
(用于处理数据) 如下:
precision mediump float;//给出默认的浮点精度
varying vec2 vTexCoord;//从顶点着色器传递过来的纹理坐标
uniform sampler2D sTexture;//纹理内容数据
uniform vec2 uPxD; // pixel delta values
void main() {
// 给出卷积内核中各个元素对应像素相对于待处理像素的纹理坐标偏移量
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);
// 给出最终求和时的加权因子(为调整亮度)
const float scaleFactor = 0.9;
//卷积内核中各个位置的值
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;
// 获取卷积内核中各个元素对应像素的颜色值
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;
//进行亮度加权后将最终颜色传递给管线
gl_FragColor = vec4(hd) * scaleFactor;
}
java 代码 (用于读取数据并生成 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();
// 强制刷新数据至纹理缓冲区
GLES20.glFinish();
// 从纹理缓冲区读取数据
GLES20.glReadPixels(0, 0, w, h, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, ib);
// 数据上下翻转
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
}
在系统 5.1.1 的 Nexus 5 手机,对 142KB 的方形 png 图片做处理,实现结果如下:
说明:
上面的图片是输入图片,下面的图片是输出图片,显示的处理时间是533.0869ms (这里的时间并不仅仅包含片元处理器的代码执行时间,也已经包括从纹理缓存对象中读取数据并生成Bitmap的时间,读取数据的时间也会占用比较多的时间)
综上,可以看到使用高性能计算的计算效率比普通的计算要提高了将近7倍,无愧于高性能
这几个字。虽然 533.0869ms 也还是远大于 Android 16ms 的屏幕刷新时间,但我们可以通过其他手段处理,如在其他线程处理,或者根据特殊业务需求先做预处理,如高斯模糊效果实现方案及性能对比这篇文章中通过预先压缩图片的方式,让模糊算法的时间降低至 16ms 以内。
既然高性能计算的效率优势如此明显,很奇怪现在的移动端开发很少使用这套东西。
这里也必须客观的提一下高性能计算的缺点,根据自己粗浅的认知总结如下:
说实话,在移动开发过程中,除了图像处理,其他的业务逻辑中真的很少能想到有什么需求需要这么做。很多时候,图像处理也是交给服务器(如 nos )处理了。当然通过
url
告诉服务器处理,那图片的加载速度就依赖于网络请求,如果产品策划无法忍受这一点的话,就可以考虑是否要使用本地高性能计算了
shader language
上面的示例中,并没有给出显示设置相关的代码,而这部分代码也较为冗长难写。对于一般的移动端开发同学其实并不关心或者不感兴趣这些内容。所幸的是,在
Android
端已经出现了RanderScript
,将上述的复杂流程极度简化了。
GPU
是 SIMD
计算模型的核心,而 GPU
相比 CPU
的特点是 GPU
并没有复杂的缓存系统、分支预测系统和各种控制逻辑,而是使用芯片上大多数的晶体管作为纯计算单元。可见,GPU 并不能处理复杂的计算逻辑。对于程序员来说,比较明显的一点,就是大部分版本的
shader language
并不支持循环语句,当然以后会不会有就不得而知了
对于一些过时的老机器很可能没有 GPU,其次各个 GPU 制造厂商的的产品对
OpenGL ES
的支持程度也会有少许差别。当然,这些支持的差异,一般这边也不会涉及到。还有,因为高性能计算
是利用了图形渲染管线的,很容易知道其应用程序的Android SDK
要不低于 2.2。
其实移动端的高性能计算的使用还是相对少见,至少我支持的几个 Android 项目都没有涉及到,本文仅仅是作为科普讲解下一点点基础,让大家了解下什么是高性能计算,相信即便是使用过 RenderScript
的Android开发同学,也可能不清楚什么是高性能计算。
因为本文讲述的仅仅是基础,如果有大牛同学有自己的看法的话,欢迎指正出来,也帮助我成长。 O(∩_∩)O~