iOS 开发者的 Metal 养成计划「5」 —— 利用 Metal 并发处理数据以及性能测试

发布于:2018-02-28 23:29,阅读数:858,点赞数:0


# 引言 在上一篇文章中已经将图形绘制的路走通。从 Metal API 到顶点着色器到片元着色器,完成这几部就可以在屏幕上绘制图像了。那么在本文中,将把 GPU 并发运算的路走通。 # Metal Shader System ## 并发运算 相对于图像渲染,利用 Metal 来进行并发运算就简单的多。 ![](//cdn.blog.yuusann.com/img/corpus/18009_1.jpg) 在批量处理数据时,可以将需要处理的源数据拼在一起封装成`MTLBuffer`对象上传至 GPU,在 Metal 中使用`kernel function`对批量数据进行并发处理,之后写入另一个`MTLBuffer`中。 大致步骤描述如下: - 将批量处理的源数据组装进一个`MTLBuffer`中,并准备另一个`MTLBuffer`用于存放输出数据。 - 在`MTLCommandBuffer`编码阶段设置输入输出的缓冲区。 - 通过`MTLLibrary`加载 Metal 函数,即上文提到的`Kernel Function`。 - 使用上一条中中加载完的函数来构造`MTLComputePipelineState`对象,用于命令编码。 - 使用上一条中构造的`MTLComputePipelineState`编码计算命令。 下面来看看具体实现 ### Kernel Function 先来看看上文提到的`Kernel Function`,这到底是个什么东西呢? ```cpp kernel void compute_test(device int *inputs [[buffer(0)]], device int *outputs [[buffer(1)]], uint gid [[ thread_position_in_grid ]]) { int input = intputs[gid]; outputs[gid] = input + 1; } ``` 上面是一个简单的`Kernel Function`,将输入 buffer 的值都 +1 然后输出到输出 buffer。 这个函数的前缀为`kernel`,返回值为空,第一个参数为输入的 buffer,第二个参数为输出的 buffer,第三个参数为索引。`[[]]`符号中的值为句柄,供 Metal API 使用,这在前面的文章中已经提到过了。 以上函数在 Metal 中会被并发执行。具体并发的程度是可以指定的,在编码阶段调用`dispatchThreadgroups`方法即可,具体的解释已在这篇文章下方介绍:[「iOS 开发者的 Metal 养成计划「3」 —— 并行渲染和 GPU 高并发运算」](https://blog.yuusann.com/corpus/article/18006) ## Metal API 那么现在有了`Kernal Function`,其他工作该如何进行呢? 首先需要一个输入`MTLBuffer`和一个输出`MTLBuffer`,可以通过给`Array`添加扩展来实现`Array → MTLBuffer`的工作: ```swift extension Array { public func toMetalBuffer() -> MTLBuffer? { guard let buffer = device.makeBuffer(length: self.count * MemoryLayout<Element>.size, options: MTLResourceOptions.cpuCacheModeWriteCombined) else { return nil } buffer.contents().copyBytes(from: self, count: self.count * MemoryLayout<Element>.size) return buffer } } ``` 那么就可以很容易地得到两个`MTLBuffer`: ```swift // 此 demo 中我们将并发处理 100 万个数据 let size = 1024 * 1024 // 构造两个 Array let input = Array.init(repeating: Int32(99), count: size) let output = Array.init(repeating: Int32(0), count: size) // 从扩展获得两个 MTLBuffer 对象 guard let inputBuffer = input.toMetalBuffer() else { // error } guard let outputBuffer = output.toMetalBuffer() else { //error } ``` 接下来在命令编码阶段将两个`MTLBuffer`对象交给 Metal,再指定并发量即可: ```swift // 通过句柄设置两个 MTLBuffer encoder.setBuffer(inputBuffer, offset: 0, index: 0) encoder.setBuffer(outputBuffer, offset: 0, index: 1) // 设置并发量,pipelineState.maxTotalThreadsPerThreadgroup 取得支持的最高并发量 let threadsPerThreadgroup = MTLSizeMake(pipelineState.maxTotalThreadsPerThreadgroup, 1, 1) let threadsPerGrid = MTLSize(width: size / threadsPerThreadgroup.width, height: 1, depth: 1) encoder.dispatchThreadgroups(threadsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) ``` 最后提交命令等待执行即可。通过以上的代码,已经完成了一个简单的计算命令。 ## 性能 那么利用 Metal 批量处理数据的性能如何呢? ### 测试方法 批量处理`100万`、`200万`、`500万`、`1000万`、`2000万`和`5000万`个数据,对比使用 Metal 处理和使用 CPU 处理的时间。 这里的 100万 = 1024 * 1024。测试中最高使用了 5000万 组数据,是因为 5000万个浮点数需占用内存 400M,输入输出两个缓冲区共需要 800M 内存,考虑到手机的内存限制,没有继续增加数据量。 测试手机为:iPhone SE iOS 11.2.5。 iPhone SE 拥有 iPhone 6s 完全相同的处理器和内存容量。由于 CPU 只拥有两个核心,在测试 CPU 性能中,双线程已经挖掘了手机的性能,四线程下速度已无提升,因此没有继续往上加线程。 由于 Metal 中使用的`MTLCommandBuffer`不可重复利用,因此每一次运算,都需要重新编码命令,CPU 测试中,为了避开 Swift 的`copy-on-write`特性而没有使用`Array`,直接使用了`UnsafeMutablePointer`去`alloc`内存空间,并且为了避开使用线程锁,直接手动拆解了任务分配到各个线程。使得整个测试对 Metal 不利。但从结果来看,Metal 的优势仍然十分明显。 以下所有数据为重复测试了`100`次。由于受 Demo 启动的影响,剔除前`10`次结果,取后面`90`次耗时取平均值所得。 ### 整数性能 整数性能测试中,将对测试数据做`+1`处理。即从输入缓冲区中取出值,+1后存入输出缓冲区。测试结果如下: |数据量|Metal|CPU 单线程|CPU 双线程|CPU 四线程| |:--:|:--:|:--:|:--:|:--:| |100万|1.41 ms|12.42 ms|6.45 ms|7.02 ms| |200万|2.32 ms|23.94 ms|12.69 ms|13.26 ms| |500万|5.63 ms|59.56 ms|32.41 ms|32.45 ms| |1000万|9.17 ms|115.40 ms|68.23 ms|66.27 ms| |2000万|18.01 ms|231.62 ms|145.46 ms|142.03 ms| |5000万|29.03 ms|586.28 ms|363.60 ms|383.05 ms| ![](//cdn.blog.yuusann.com/img/corpus/18009_2.jpg) 可以看到在整数测试中,Metal 优势非常明显。但对于 GPU 来说,整数运算并不是它的强项,浮点才是,因此有了以下浮点性能测试。 ### 浮点性能 浮点性能测试中,将对测试数据做`与π相乘`处理。即从输入缓冲区中取出值,与π相乘后存入输出缓冲区。测试结果如下: |数据量|Metal|CPU 单线程|CPU 双线程|CPU 四线程| |:--:|:--:|:--:|:--:|:--:| |100万|1.36 ms|18.30 ms|9.70 ms|9.88 ms| |200万|2.77 ms|38.02 ms|18.97 ms|19.55 ms| |500万|5.05 ms|90.89 ms|49.83 ms|49.30 ms| |1000万|9.73 ms|180.62 ms|95.80 ms|104.43 ms| |2000万|14.11 ms|366.09 ms|191.78 ms|194.54 ms| |5000万|28.33 ms|909.11 ms|513.64 ms|492.42 ms| ![](//cdn.blog.yuusann.com/img/corpus/18009_3.jpg) 在浮点性能测试中,CPU 运算的耗时上升明显,上升了约50%,而 GPU 则没有显著变化。 从测试结果可以得出在 A9 处理器中使用 Metal 处理数据,其性能超过了每秒200亿次浮点运算。(官方提供的 A9 的 GPU 理论可以达到 115.2GFlops,每秒约1152亿次浮点运算。) # 小结 在性能测试中,Metal 的性能给我留下了深刻的印象。但是很明显,Metal 处理数据是有很大的局限性。我们没法使用 Metal 去写文件,去读数据库,只能用 Metal 处理可拆分的,可并发的 CPU 密集型的数据。每一个 Metal 命令执行的时间是有限制的,因此太过复杂的处理也没办法交给 Metal 去做。(这一点目前是系统硬性限制的,希望日后 Apple 能放开这一限制) 但是在合适的场景下使用 Metal 去处理数据也是一种思路。


评论:0条


返回列表

返回主页