作为较早在客户端侧选择Flutter方案的技术团队,性能和用户体验一直是闲鱼技术团队在开发中比较关注的点。而Metal这样的直接操作GPU的底层接口无疑会给闲鱼技术团队突破性能瓶颈提供一些新的思路。
本文将会详细阐述一下这次大会Metal相关的新特性,以及对于闲鱼技术和整个淘系技术来说,这些新特性带来了哪些技术启发与思考。
前言
Metal 是一个和 OpenGL ES 类似的面向底层的图形编程接口,通过使用相关的 api 可以直接操作 GPU ,最早在 2014 年的 WWDC 的时候发布。Metal 是 iOS 平台独有的,意味着它不能像 OpenGL ES 那样支持跨平台,但是它能最大的挖掘苹果移动设备的 GPU 能力,进行复杂的运算,像 Unity 等游戏引擎都通过 Metal 对 3D 能力进行了优化, App Store 还有相应的运用 Metal 技术的游戏专题。
闲鱼团队是比较早在客户端侧选择Flutter方案的技术团队,当前的闲鱼工程里也是一个较为复杂的Native-Flutter混合工程。作为一个2C的应用,性能和用户体验一直是闲鱼技术团队在开发中比较关注的点。而Metal这样的直接操作GPU的底层接口无疑会给闲鱼技术团队突破性能瓶颈提供一些新的思路。
下面会详细阐述一下这次大会Metal相关的新特性,以及对于闲鱼技术和整个淘系技术来说,这些新特性带来了哪些技术启发与思考。
Metal相关新特性
▐ Harness Apple GPUs with Metal
这一章其实主要介绍的是Apple GPU的在图形渲染上的原理和工作流,是一些比较底层的硬件原理。当我们使用Metal进行App或者是游戏的构建的时候,Metal会利用GPU的tile-based deferred rendering (TBDR)架构给应用和游戏带来非常可观的性能提升。这一章主要就是介绍GPU的的架构和能力,以及TBDR架构进行图像渲染的原理和流程。总之就是号召开发者们使用Metal来构建应用和游戏。因为这个session没有涉及到上层的软件开发,就不对视频的具体内容进行赘述了。详情可见:Harness Apple GPUs with Metal
▐ Optimize Metal apps and games with GPU counters
这一章主要介绍了Xcode中的GPU性能分析工具Instrument,这个工具现在已经支持了GPU的性能分析。然后从多个方面分析了GPU的性能瓶颈,以及性能瓶颈出现时的优化点。总体来说就是通过性能分析工具来优化我们的App或者游戏,让整个画面更加流畅。整个章节主要分为五个部分:
✎ 总体介绍
这个环节主要是快速回顾了一下Apple的GPU的架构和渲染流程。然后因为很多渲染任务都需要在不同的硬件单元上进行,例如ALU和TPU。他们对不同的吞吐量有着不同的度量。有很多GPU的性能指标需要被考虑,所以推出了GPU性能计数器。这个计数器可能测量到GPU的利用率,过高和过低都会造成我们的渲染性能瓶颈。关于计数器的具体使用,参考官方的video效果会更好:Optimize Metal apps and games with GPU counters(6:37~9:57),主要使用了Instrument工具,关于工具的全面详细的使用可以参考WWDC19的session videoGetting Started with Instruments
✎ 性能瓶颈分析
这一章主要介绍了造成GPU性能瓶颈的各个方面以及它们的优化点。主要分为六个方面,如下图所示:
1.Arithmetic(运算能力)
GPU中通常通过ALU(Arithmetic Logic Unit)来处理各种运算,例如位操作,关系操作等。他是着色器核心的一部分。在这里一些复杂的操作或者是高精度的浮点运算都会造成一些性能瓶颈,所以给出以下建议来进行优化:
如上图所示,我们可以使用近似或者是查找表的方式来替换复杂的运算。此外,我们可以将全精度的浮点数替换为半精度的浮点数。尽量避免隐式转换,避免32位浮点数的输入。以及确保所有的着色器都使用Metal的“-ffast-math”来进行编译。
2.Texture Read and Write
GPU通过Texture Processing Unit来处理纹理的读写操作。当然在读写的过程中也会遇到一些性能瓶颈问题。这里从读和写两个部分分别来给出优化点:
如上图所示,我们可以尝试使用mipmaps。此外,可以考虑更改过滤选项。例如,使用双线性代替三线性,降低像素大小。确保使用了纹理压缩,对Asset使用块压缩(如ASTC),对运行时生成的纹理使用无损纹理压缩。
如上图所示,我们应该注意到像素的大小,以及每个像素中唯一MSAA样本的数量。此外,可以尝试一些优化一些逻辑写法。
✎ Tile Memory Load and Store
图块内存是一组存储Thread Group和ImageBlock数据的高性能内存。当从ImageBlock或是Threadgroup读取或写入像素数据时,比如在使用Tile着色器时或者是计算分派时,可以访问到Tile内存。那当使用GPU性能计数器发现这个方面的性能瓶颈时,我们可以如下图所示进行优化。
考虑减少threadgroup的并行,或者是SIMD/Quadgroup操作。此外,确保将线程组的内存分配和访问对齐到16字节。最后,可以考虑重新排序内存访问模式。
✎ Buffer Read and Write
在Metal中,缓冲区只被着色器核心访问。在这个地方发现了性能瓶颈。我们可以如下图所示进行优化:
可以更大力度的压缩打包数据,例如使用例如packed_half3这样小的类型。此外,可以尝试向量化加载和存储。例如使用SIMD类型。避免寄存器溢出,以及可以使用纹理来平衡工作负载。
✎ GPU Last Level Cache
如果在这个方面,我们的GPU性能计数器显示一个过高的值。我们可以如下图这样优化:
如果纹理或者是缓存区也同样显示一个过高的值,我们可以把这个优化放到第一优先级。我们可以考虑减小工作集的大小。如果Shader正在使用Device Atomics,我们可以尝试重构我们的代码来使用Threadgroup Atomics。
✎ Fragment Input Interpolation
分段输入插值。分段输入在渲染阶段由着色器核心进行插值。着色器核心有一个专用的分段输入插值器。这个是比较固定和高精度的功能。我们能优化的点不多,如下图所示:
尽可能的移除传递给分段着色器的顶点属性。
▐ 内存带宽
内存带宽也是影响我们GPU性能的一个重要因素。如果在GPU性能计数器的内存带宽模块看到一个很高的值。我们就应该如下图所示来进行优化:
如果纹理和缓存区也同样显示比较高的值,那优化优先级应该排到第一位。优化方案也是较少Working Set的大小。此外,我们应该只加载当前渲染过程需要的数据,只存储未来渲染过程需要的数据。然后就是确保使用纹理压缩。
▐ Occupancy
如果我们看到整体利用率比较低,这意味着Shader可能已经耗尽了一些内部资源,比如tile或者threadgroup内存。也可能是线程完成执行的速度比GPU创建新线程的速度快。
▐ 避免重复绘制
我们通过GPU计数器可以统计到重复绘制的区域,我们应该高效使用HSR来避免这样的重绘。我们可以如图所示的顺序来进行绘制。
Build GPU binaries with Metal
这一章主要给开发者们介绍了一种使用Metal的编程工作流,可以通过优化Metal的渲染编译模型来增强渲染管线,这个优化可以在应用程序启动,特别是首次启动时大大减少PSO(管线状态对象)的加载时间。可以让我们的图形渲染更加的高效。整个章节主要分为四个部分:
▐ Metal的Shader编译模型概述
众所周知,Metal Shading Language是Apple为开发者提供的Shader编程语言,Metal会将编程语言编译成为一个叫做AIR的中间产物,然后AIR会在设备上进一步编译,生成每个GPU所需的特定的机器码。整个过程如下图所示:
上述过程在每个管线的生命周期中都会发生,当前Apple为了加速管线的重新编译和重新创建流程,会缓存一些Metal的方法变体,但是这个过程还是会造成屏幕的加载耗时过长。而且在当前的这个编译模型中,应用程序不能在不同的PSO(管线状态对象)中重用之前生成的机器码子程序。
所以我们需要一种方法来减少这个整个管线编译(即源代码->AIR->GPU二进制代码)的时间成本,还需要一种机制来支持不同PSO之间共享子程序和方法,这样就不需要将相同的代码多次编译或者是多次加载到内存中。这样开发者们就可以使用这套工具来优化App首次的启动体验。
▐ Metal二进制文件介绍
Metal二进制文件就是解决上述需求的方法之一,现在开发者们可以直接使用Metal为二进制文件来控制PSO的缓存。开发者可以收集已编译的PSO,然后将它们存储到设备中,甚至可以分发到其他兼容的设备中(同样的GPU和同样的操作系统),这种二进制文件可以看做一种Asset。下面是一些例程和示意图:
//创建一个空的二进制文件
let descriptor = MTLBinaryArchiveDescriptor()
descriptor.url = nil
let binaryArchive = try device.makeBinaryArchive(descriptor:descriptor)
//Populating an archive
// Render pipelines
try binaryArchive.addRenderPipelineFunctions(with: renderPipelineDescriptor)
// Compute pipelines
try binaryArchive.addComputePipelineFunctions(with: computePipelineDescriptor)
// Tile render pipelines
try binaryArchive.addTileRenderPipelineFunctions(with: tileRenderPipelineDescriptor)
//重用已编译的方法
// Reusing compiled functions to build a pipeline state object from a file
let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
// …
renderPipelineDescriptor.binaryArchives = [ binaryArchive ]
let renderPipeline = try device.makeRenderPipelineState(descriptor:
renderPipelineDescriptor)
//序列化
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let archiveURL = documentsURL.appendingPathComponent(“binaryArchive.metallib”)
try binaryArchive.serialize(to: NSURL.fileURL(withPath: archiveURL))
//反序列化
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let serializeURL = documentsURL.appendingPathComponent(“binaryArchive.metallib”)
let descriptor = MTLBinaryArchiveDescriptor()
descriptor.url = NSURL.fileURL(withPath: serializeURL)
let binaryArchive = try device.makeBinaryArchive(descriptor: descriptor)
总的来说就是这个Metal二进制文件可以提供开发者手动管理管线缓存的方法,这样就可以从一个设备中获取这些文件并部署到其他兼容的设备上,在iOS环境下,极大地减少了第一次安装游戏或应用以及设备重启后的管道创建时间。可以优化应用的首次启动体验和冷启动体验。
▐ Metal对动态库的支持
动态库将允许开发者编写可重用的库代码,却可以减少重新编译程序的时间和内存成本,这个特性将会允许开发者将计算着色器和程序库动态链接。而且和二进制文件一样,动态库也是可序列化和可转移的。这也是解决上述需求的方案之一。
在PSO生成的时候,每个应用程序都需要为程序library生成机器码,而且使用相同的程序库编译多个管线会导致生成重复的机器码。由于大量的编译和内存的增加,这个可能会导致更长的管线加载时间。而动态库就可以解决这个问题。
Metal Dynamic Library允许开发者以机器码的形式动态链接,加载和共享工具方法。代码可以在多个计算管线中重用,消除了重复编译和多个相同子程序的存储。而且这个 MTLDynamicLibrary是可序列化的,可以作为应用程序的Asset使用。MTLDynamicLibrary其实就是多个计算管线调用的导出方法的集合。
大致的工作流程如下:我们首先创建一个MTLLibrary作为我们指定的动态库,这个可以将我们的metal代码编译为AIR。然后我们调用方法makeDynamicLibrary,这个方法需要指定一个唯一的installname,在管线创建时,linker将会使用这个名字来加载动态库。这个方法可以将我们的动态库编译成为机器码。这就完成了动态库的创建。
对于动态库的使用来说:通过设置MTLCompileOptions里的libraries参数,就可以完成动态库的加载和使用了。代码如下:
//使用动态库进行编译
let options = MTLCompileOptions()
options.libraries = [ utilityDylib ]
let library = try device.makeLibrary(source: kernelStr, options: options)
▐ 开发工具介绍
最后
面试前一定少不了刷题,为了方便大家复习,我分享一波个人整理的面试大全宝典
- Java核心知识整理
Java核心知识
- Spring全家桶(实战系列)
- 其他电子书资料
Step3:刷题
既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。
以下是我私藏的面试题库:
加入社区:https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
最后
面试前一定少不了刷题,为了方便大家复习,我分享一波个人整理的面试大全宝典
- Java核心知识整理
[外链图片转存中…(img-zpGoRpa6-1725693774800)]
Java核心知识
- Spring全家桶(实战系列)
[外链图片转存中…(img-5DO3xRnw-1725693774801)]
- 其他电子书资料
[外链图片转存中…(img-MOJ7CFqT-1725693774801)]
Step3:刷题
既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。
以下是我私藏的面试题库:
[外链图片转存中…(img-4Ids13s9-1725693774802)]
加入社区:https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0