Using asynchronous compute on Arm Mali GPUs: A practical sample

Using asynchronous compute on Arm Mali GPUs: A practical sample

1. 异步计算 (Asynchronous Compute) 简介

核心观点

异步计算并非一种具体的图形技术或算法,而是一种最大化 GPU 硬件利用率编程思想与优化方法。其本质是通过现代图形 API(如 Vulkan, D3D12),将原本需要串行执行的任务,分解成多个可以同时提交给 GPU 的命令流,从而让 GPU 的不同硬件单元并行工作,填补流水线中的空闲,提升整体性能。

关键要点

  • 目标: 提升 硬件资源利用率 (Hardware Utilization)。现代 GPU 内部有众多计算单元,如果只有一个任务在运行,很可能部分单元处于空闲状态。异步计算旨在让这些空闲单元也“忙起来”。
  • 起源与发展: 这个概念最早在上一代游戏主机硬件上得到应用并证明其有效性,现在已成为 VulkanD3D12 等现代图形 API 的标准能力之一,是图形程序员工具箱里的重要工具。
  • 挑战: 如何有效应用异步计算并非易事,它需要开发者对目标硬件的架构有深入的、针对具体实现的知识 (implementation-specific knowledge)。简单的“开启”异步计算并不能保证性能提升,错误的用法甚至可能导致性能下降。

2. 核心机制:GPU 命令队列 (Command Queues)

核心观点

实现异步计算的底层硬件基础是 GPU 内置的多个命令队列 (Queues)。不同硬件平台的 队列拓扑结构 (Queue Topologies) 差异巨大,这直接决定了我们在特定平台(如 Arm Mali)上实践异步计算的策略。

关键术语与对比

  • 命令队列 (Command Queue): CPU 向 GPU 提交渲染、计算等命令的通道。GPU 从这些队列中取出命令并执行。
  • 队列拓扑 (Queue Topology): 指 GPU 内部不同类型队列的组织方式、数量和能力。这是理解和用好异步计算的关键所在

桌面级 GPU 队列模型

  • 通常包含一个“全能”的 图形队列 (GRAPHICS Queue):可以处理图形、计算、传输等所有类型的任务。
  • 同时配备多个专用的计算队列 (COMPUTE Queues):它们只能执行计算着色器 (Compute Shader) 相关的任务。
  • 这种模型的优势:可以将纯计算密集型任务(如物理模拟、AI、部分后处理)从图形渲染主流程中剥离,放到专门的计算队列上与图形任务并行执行,互不干扰。

GPU硬件队列与渲染管线依赖

一、 不同GPU架构的硬件队列模型

1. 桌面级GPU (如 NVIDIA/AMD)

  • 核心观点: 这类GPU通常采用一种非对称的队列设计
  • 关键术语:
    • GRAPHICS queue (图形队列): 一条功能全面的主队列,能够处理图形、计算等所有类型的任务。
    • COMPUTE queues (计算队列): 多条专用的、只能执行计算着色(Compute Shader)任务的队列。

2. 移动端TBR GPU (以 Arm Mali 为例)

  • 核心观点: 作为一种基于分块的延迟渲染(Tile-based Rendering, TBR) 架构,其硬件队列的设计直接映射了其两阶段的渲染管线。
  • 关键术语:
    • Vertex Shading and Tiling Queue (顶点/分块队列): 硬件管线的第一阶段,负责处理顶点着色、几何体处理和Tiling(将场景几何信息分配到屏幕上的各个Tile中)。
    • Fragment Shading Queue (片段队列): 硬件管线的第二阶段,负责对每个Tile进行光栅化和片段着色。
    • Compute workloads (计算任务): 通常与顶点/分块队列并行执行。从硬件角度看,顶点着色本质上可以看作一种计算着色。

3. Vulkan抽象:VkQueue 在Mali上的映射

  • 核心观点: 在Arm Mali上,Vulkan的 VkQueue 并非直接映射到单一的硬件队列,而是一个更高层次的抽象。
  • 关键细节:
    • 一个 VkQueue 对象代表了向整个两阶段硬件管线(即顶点/分块队列 + 片段队列) 提交指令的能力。
    • 即使你从同一个 Queue Family 创建多个 VkQueue,它们也并不代表不同的硬件单元。它们仅仅是向同一套硬件提交任务的独立的、并行的指令流 (separate streams of commands)。因此,在Mali上,开发者通常只需要关注一个主要的Queue Family。

二、 TBR架构的管线依赖与性能关键

1. 保持硬件队列“喂饱”的重要性

  • 核心观点: TBR的性能关键在于避免管线停顿 (pipeline stalls),最大化硬件利用率。
  • 性能黄金法则:
    • 最理想的状态是让Fragment硬件队列100%的时间都处于繁忙状态。
  • 原因分析:
    • 顶点/分块 (Vertex/Tiling) 阶段带宽密集型任务,其工作效率相对较低,单独运行时很容易因为等待外部内存带宽而造成硬件空闲。
    • 通过让Fragment队列持续工作(处理前一帧或前一个pass的Tile),可以有效地隐藏顶点/分块阶段的延迟,形成高效的流水线作业。

2. 关键的依赖关系模式

  • 核心观点: 任务之间的依赖关系直接决定了渲染管线是否会发生停顿。

  • ✅ 理想的依赖关系: VERTEX / COMPUTE → FRAGMENT

    • 描述: 顶点或计算阶段产生数据,然后由片段阶段消费。
    • 原因: 这完全符合TBR硬件从前到后的自然数据流,可以实现完美的流水线并行,性能最高。
  • ❌ 应避免的依赖关系: FRAGMENT → VERTEX / COMPUTE

    • 描述: 片段阶段产生的数据,需要被后续的顶点或计算阶段使用。
    • 原因: 这会造成严重的管线停顿。因为位于管线后端的Fragment阶段必须先完成工作,才能让位于管线前端的Vertex/Compute阶段开始执行。整个管线被迫同步等待,硬件资源被大量浪费。

三、 案例研究:计算着色器用于后期处理 (Compute for Post Effects)

  • 承上启下: 接下来的内容将以一个具体的案例——使用Compute Shader实现后期处理效果——来深入探讨和解决上述 FRAGMENT → VERTEX / COMPUTE 这种不良依赖所带来的性能问题。

使用计算着色器(Compute Shader)处理后期效果

本部分将探讨在渲染管线中使用计算着色器(Compute Shader)进行后期处理时,可能遇到的性能瓶颈,并分析其根本原因。

一、 计算着色器在后期效果中的应用

  • 核心观点: 现代游戏引擎中,传统的**主光栅化通道(Main Pass Rasterization)**在渲染预算中的占比越来越小,而使用计算着色器进行后期处理变得愈发普遍。

  • 关键术语:

    • 后期效果(Post-effects): 指任何依赖于当前帧**片段着色(Fragment Shading)**结果的计算通道。常见的例子包括:
      • 高动态范围(HDR)辉光(Bloom)
      • 景深(Depth-of-Field)
      • 模糊(Blurs)
    • 计算着色器的优势: 对于一些用片段着色器实现起来非常别扭的操作,计算着色器更具吸引力。一个典型的例子是处理 HDR 时所需的缩减操作(Reduction passes),使用计算着色器可以避免创建一长串最终缩减到 1x1 像素的渲染通道(Render Pass)。

二、 “问题气泡”:后期处理中的流水线瓶颈

  • 核心观点: 在渲染管线中不恰当地插入计算着色器,会形成一个破坏流水线并行性的“问题气泡”,导致严重的性能下降。

  • 问题流程的产生: 一个典型的场景渲染与后期处理流程如下:

    1. 顶点着色(VERTEX)→ 片段着色(FRAGMENT): 完成场景的主要渲染。
    2. 计算着色(COMPUTE): 基于前一阶段的渲染结果进行后期处理(例如,计算辉光)。
    3. 如何上屏?: 计算着色器处理完的数据需要通过某种方式显示到屏幕上,这通常需要再次调用片段着色器。
  • 致命的执行序列: 这就导致了一个非常低效的执行序列:

    FRAGMENT → COMPUTE → FRAGMENT
    
    • 关键问题: 这个序列在 FRAGMENT → COMPUTECOMPUTE → FRAGMENT 之间形成了同步屏障(Barrier)。这个屏障会中断 GPU 的流水线,导致硬件单元无法被充分利用。
    • 具体影响: 在等待计算着色器完成时,负责片段着色的硬件单元处于空闲状态,反之亦然。这种现象被称为 “饿死”(Starve)片段着色器,是必须极力避免的性能陷阱。

三、 挑战:纯计算着色器方案的可行性

  • 核心观点: 尽管在理论上可以将整个渲染帧完全在计算着色器中完成并直接提交显示,但在移动端,这种方案因 UI 渲染的效率问题而变得不切实际。

  • 方案设想: 可否将所有渲染(包括最终合成)都在计算着色器中完成,然后直接呈现(Present)?

    • 可行性: 在 Vulkan API 中理论上可行,并且一些桌面游戏确实采用了这种方法。
  • 移动端的主要障碍:

    • UI 渲染: UI 元素通常使用传统的渲染通道进行绘制。如果采用纯计算方案,流程会变成:
      1. 在渲染通道中绘制 UI。
      2. 将 UI 渲染结果写回内存。
      3. 启动一个计算着色器,从内存中读取 UI 贴图,并将其与场景合成。
    • 性能瓶颈: 这种“写回再读出”的操作是极其**消耗带宽(Bandwidth Intensive)**的。在对带宽非常敏感的移动端 GPU 上,这是一种巨大的浪费,应尽可能避免。
  • 结论: 讲座中的示例将从一个包含上述性能问题的渲染管线开始,并逐步探索如何解决这些效率问题。

利用异步计算(Async Compute)优化渲染管线

一、 识别问题:渲染管线中的“气泡”(GPU Bubble)

核心观点: 当GPU的不同硬件单元(如片段着色器、计算着色器)无法被持续、并行地利用时,就会在渲染时间线上产生空闲间隙,这被称为GPU气泡(GPU Bubble)。这种“气泡”是GPU资源浪费的直接体现,也是性能优化的关键目标。

1.1 案例分析:一个典型的渲染帧

讲座中以一个包含重度计算后处理的场景为例,其渲染流程如下:

  1. 阴影贴图生成 (Shadow Map): 8K分辨率,使用顶点/片段着色器(VERTEX / FRAGMENT)。
  2. 主通道渲染 (Main Pass): 4K分辨率,前向着色,使用顶点/片段着色器。
  3. 后处理 (Post-Processing): 阈值处理 + Bloom模糊,使用计算着色器(COMPUTE)。这个阶段代表了各类复杂的计算密集型后效。
  4. 最终合成 (Final Composite): Tonemap + UI,使用顶点/片段着色器,完成帧的最终输出。
1.2 性能瓶颈分析

通过硬件性能计数器,我们观察到以下现象:

  • GPU总周期与片段着色周期不匹配: 例如,GPU总计活跃 787M cycles/s,但片段着色单元仅活跃 600M cycles/s。在非CPU瓶颈且未开启垂直同步的情况下,这通常意味着存在性能“气泡”。
  • 计算与片段着色的“跷跷板效应”: 最关键的指标是,当计算着色器(Compute Shader)负载升高时(执行Bloom),片段着色器(Fragment Shader)的活动出现明显下降。这清晰地暴露了串行执行导致的资源闲置:在执行计算任务时,为图形任务设计的硬件单元正在“等待”。
1.3 如何获取硬件性能数据

对于移动端GPU(以Arm Mali为例),可以通过以下方式获取底层的硬件性能数据:

  • 专用库: Arm提供了一个可访问硬件计数器的库,可以集成到应用中。
  • 开发框架集成: 讲座中提到 Vulkan Samples 框架集成了这个库,能够实时读取硬件计数器,非常便于调试和分析。
  • 专业性能分析工具: 这些数据与 Arm Mobile Studio 这类专业工具所提供的数据是一致的。

二、 解决方案:引入异步计算(Async Compute)

2.1 并行执行,填补“气泡”

通过将计算密集型的Bloom后处理放入异步计算队列,我们实现了以下并行执行:

  • 图形队列 (Graphics Queue): 开始处理 下一帧的阴影贴图生成
  • 计算队列 (Compute Queue): 同时处理 当前帧的Bloom后处理

最终效果: 观察性能计数器可以发现,片段着色器队列(Fragment Queue)变得持续饱和,之前因执行计算任务而产生的下降消失了。这表明GPU的着色器核心得到了充分利用,成功“压扁”了性能气泡。

2.2 核心原理:工作负载的互补性

这种并行策略之所以高效,关键在于两种任务负载的特性是互补的

  • 阴影贴图生成 (Shadow Map Pass):

    • 这是一个典型的**光栅化绑定(Rasterization Bound)**任务。
    • 它主要压力在于GPU的固定功能硬件(如光栅器、ROP),因为着色器通常非常简单(只需输出深度),导致**着色器核心(Shader Cores)**大部分时间处于闲置状态。
  • Bloom后处理 (Compute Pass):

    • 这是一个典型的**计算绑定(Compute Bound)**任务。
    • 它几乎完全依赖着色器核心进行大量的数学运算,而很少使用光栅化等固定功能硬件。

结论: 将一个主要使用固定功能硬件的任务(阴影贴图)与一个主要使用着色器核心的任务(Bloom计算)并行执行,就像让两个人同时使用一把多功能工具的不同部分,实现了资源的完美互补,从而显著提升了GPU的整体利用率和帧率。

利用GPU空闲周期——异步图形与多队列技术

1. 发现并利用GPU的空闲周期

核心观点

在渲染管线的某些阶段,特别是顶点处理(Vertex Shading)任务繁重时,硬件资源(如片段着色器)可能处于空闲状态,造成GPU利用率不足。我们可以利用这段时间注入计算任务(Compute Workloads),从而提高整体性能。

要点解析

  • 性能瓶颈的转移:当渲染负载主要集中在顶点阶段时,GPU的着色器核心(Shader Core)并未被充分利用,存在性能优化的空间。
  • 机会窗口:将部分原本属于片段着色器的工作,或是其他独立的计算任务,转移到这个“空闲”时段,可以与顶点处理并行,有效提高着色器核心的利用率(Shader Core Utilization)
  • 性能提升案例
    • 在Mali-G77 GPU上的特定场景中,该技术带来了约5%的帧率提升
    • 重要提示:这是一个高度**依赖具体渲染内容(content specific)**的优化,并非普遍适用。
  • 性能提升的关键:即使从分析数据上看,片段处理的周期(Fragment cycles)可能增加了,但由于顶点和片段处理共享同一个着色器核心,真正的性能增益来自于着色器核心调度器能够利用空闲的线程立即派发新的工作,从而填补了流水线中的“气泡”,使得GPU整体运行更加饱满。

2. 核心技术:异步图形 (Async Graphics)

核心观点

传统的异步计算(Async Compute)通常指计算任务与图形任务并行。而这里提出的异步图形(Async Graphics) 是一种更进一步的概念,它利用多个 Vulkan队列(VkQueue) 来人为地创造一个并行的“图形管线”,从而打破原有的线性依赖关系。

实现思路

如果渲染管线本身不存在天然的并行机会,我们可以通过多队列的架构,凭空创造出一个并行管线。这不仅是计算与图形的异步,也是图形任务内部不同部分之间的异步。

3. 实现细节与Vulkan机制

3.1 利用队列打破依赖链

关键术语: 队列优先级 (Queue Priorities), 抢占 (Pre-emption)

  • 队列优先级:在Arm Mali等现代GPU上,可以为不同的VkQueue设置不同的优先级。这个特性最初因VR应用的需求而普及。
  • 抢占机制高优先级的队列可以抢占(pre-empt)低优先级队列的执行。这意味着当高优先级任务就绪时,GPU可以暂停当前正在执行的低优先级任务,转而执行高优先级任务。
  • 打破依赖:在Vulkan API中,将工作提交到不同的队列是打破命令之间隐式依赖关系的一种有效方式。

3.2 理解Vulkan中的依赖关系

为了理解为何多队列能打破依赖,必须先掌握Vulkan中控制执行顺序的核心机制:屏障(Barrier)信号量(Semaphore)

  • 管线屏障 (Pipeline Barrier)

    • 核心作用vkCmdPipelineBarrier 将单个命令缓冲区(Command Buffer)内的指令流清晰地划分为 **“屏障前”**和 **“屏障后”**两个部分。
    • 执行顺序:“屏障后”的操作必须等待“屏障前”的特定阶段完成后才能开始执行。
    • 控制粒度:具体的等待关系由阶段掩码(Stage Masks) 来精确定义,例如,等待前一个操作的顶点着色阶段完成,才能开始后一个操作的片段着色阶段。
  • 信号量 (Semaphore)

    • 核心作用VkSemaphore 用于同步跨队列的操作,是实现异步图形的关键。
    • 发出信号 (Signal):当一个队列完成了信号操作 **“之前”**的所有指令,该信号量被置为信号态。
    • 等待 (Wait):另一个队列中的等待操作会阻塞其 **“之后”**的所有指令,直到它等待的信号量变为信号态。
    • 与屏障的相似性:信号量同样受到阶段掩码的约束,可以精确控制需要等待的生产阶段(Producer Stage)和可以开始执行的消费阶段(Consumer Stage)。

利用多队列与优先级优化Vulkan渲染管线

1. 单队列同步的瓶颈:管线气泡 (Pipeline Bubble)

当渲染流程中出现某些特定的依赖关系时,单一的指令队列(VkQueue)会遇到性能瓶颈。

  • 核心问题: 在单个队列中,一个形如 FRAGMENT → COMPUTE → FRAGMENT 的工作流依赖,会强制导致 管线气泡 (pipeline bubble)。这意味着GPU必须完全停止前一个片段着色阶段,清空管线,执行计算着色,然后再重新启动管线以执行下一个片段着色阶段。这个等待和切换的过程造成了GPU资源的闲置和浪费。

  • 关键限制: Vulkan中的同步屏障 (Barrier) 只在单个 VkQueue 内部有效。它们用于协调同一个队列中指令的执行顺序,但无法解决这种跨渲染阶段切换导致的硬件停顿。

2. 解决方案:拆分任务与多队列流水线

为了打破这种强制的线性依赖,我们可以将一帧的渲染任务拆分到两个独立的 VkQueue 中,并通过信号量(Semaphore)进行协调,从而实现真正的并行处理。

  • 核心策略: 将单帧任务拆分为两个逻辑部分,并分配给不同的队列,实现流水线作业。

  • 实现步骤:

    1. 队列 #1 (低优先级): 执行所有用于主渲染通道 (Main Pass) 的工作。这通常包括场景几何体、阴影、G-Buffer生成等耗时较长的渲染任务。
    2. 跨队列同步: 在队列 #1 的任务完成后,发出一个信号量 (Signal a Semaphore)
    3. 队列 #0 (高优先级): 等待 (Wait) 来自队列 #1 的信号量,然后执行所有后续工作,例如后处理、UI渲染,并最终提交呈现 (Present)
  • 核心优势:实现帧流水线 (Frame Pipelining)

    • 通过这种方式,我们避免了在任何单个队列中出现 FRAGMENT → COMPUTE 这样的“致命”屏障。
    • 当队列 #0 正在处理第 N 帧的后处理和呈现任务时,队列 #1 可以立即开始处理第 N+1 帧的主渲染通道
    • 这种重叠执行极大地提高了GPU的利用率,有效地隐藏了延迟,消除了原本会产生的管线气泡。

3. 关键技巧:队列优先级 (Queue Priorities)

为了确保这种多队列方案能够稳定、高效地运行,必须合理设置队列的优先级。

  • 核心观点: 负责最终呈现 (Present) 的队列(队列 #0)必须拥有更高的优先级

  • 原因: 队列 #0 的工作是完成一帧并将其提交到屏幕上,这个任务是时间敏感的 (time-sensitive)。如果它被低优先级的队列 #1 阻塞,可能会导致渲染任务无法在垂直同步(V-Blank)信号到来之前完成,从而造成画面卡顿或掉帧 (missing V-Blank)

  • Vulkan实现:在设备创建时指定优先级

    • 在Vulkan中,队列的优先级必须在逻辑设备(VkDevice)创建时通过 VkDeviceQueueCreateInfo 结构体静态声明。
    • 你需要从同一个队列家族(Queue Family)中请求多个队列,并为它们提供一个优先级数组。
    // 示例:创建两个队列,一个高优先级,一个低优先级
    VkDeviceCreateInfo device_info = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
    VkDeviceQueueCreateInfo queue_info = { VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
     
    device_info.queueCreateInfoCount = 1;
    device_info.pQueueCreateInfos = &queue_info;
     
    // 查询物理设备以获取 queueFamilyIndex 和 queueCount
    queue_info.queueFamilyIndex = 0; 
    queue_info.queueCount = 2; // 我们需要从这个 family 创建两个队列
     
    // 定义优先级数组,1.0f 为最高,0.5f 为较低
    static const float prios[] = { 1.0f, 0.5f };
    queue_info.pQueuePriorities = prios;
     
    vkCreateDevice(gpu, &device_info, nullptr, &device);
     
    // 获取队列句柄,索引 0 对应优先级 1.0f
    VkQueue high_prio_queue;
    vkGetDeviceQueue(device, 0, 0, &high_prio_queue); 
    // 索引 1 对应优先级 0.5f
    VkQueue low_prio_queue;
    vkGetDeviceQueue(device, 0, 1, &low_prio_queue);

Compute Shader vs. Fragment Shader 的实践权衡

一、指令提交策略与延迟

这部分探讨了通过调度来优化GPU工作负载的两种思路,并重点分析了其对延迟的影响。

1.1 使用不同优先级的硬件队列

  • 基本概念: 现代图形API(如Vulkan)允许在创建设备时请求具有不同优先级的队列。
    • 示例代码片段:
      // 请求一个高优先级队列和一个普通优先级队列
      vkGetDeviceQueue(device, 0, 0, &high_prio_queue);
      vkGetDeviceQueue(device, 0, 1, &normal_prio_queue);
  • 核心思想: 开发者可以将关键的、需要快速完成的任务(如玩家输入的响应)提交到高优先级队列,而将不那么紧急的后台任务(如资源加载、AI计算)提交到普通优先级队列,从而更好地管理GPU资源,保证关键路径的性能。

1.2 跨帧指令重排 (Cross-Frame Submission Reordering)

  • 核心观点: 跨帧重排指令虽然理论上可行,但会增加至少一帧的输入延迟,这对于游戏等实时交互应用是不可接受的。
  • 具体原因: 为了在不同帧之间找到最优的指令执行顺序,引擎必须持有(Hold back)至少一帧的数据,等待下一帧的指令提交后才能进行比较和重排。这个等待过程直接转化为用户感受到的延迟。

二、将图像处理迁移到 Compute Shader 的潜在问题

将所有任务都迁移到 Compute Shader 并非总是最优解,尤其是在处理图像时。与专门为光栅化和像素处理设计的 Fragment 流水线相比,Compute Shader 在某些场景下效率稍低,主要有以下几个原因:

2.1 丢失硬件压缩特性

  • 丢失帧缓冲压缩 (Framebuffer Compression):

    • 关键术语: AFBC (Arm Frame Buffer Compression)
    • 问题描述: 当使用 storage image(计算着色器写入图像的常用方式)时,会失去 AFBC 这类由硬件提供的、用于减少内存带宽消耗的无损压缩技术。
    • 直接后果: 内存带宽消耗会显著增加,尤其是在高分辨率下,可能成为性能瓶颈。
  • 丢失事务性剔除 (Transactional Elimination):

    • 核心概念: 这是一种带宽节省技术,也称为冗余贴图写回消除 (Eliminating Redundant Tile Write-backs)。如果一个 Tile(分块渲染中的基本单元)在渲染后其内容与之前内存中的内容完全相同,硬件就会跳过这次写回操作。
    • 问题描述: 使用 storage image 时同样无法利用此项优化。
    • 直接后果: 即使图像内容未变,也会产生不必要的内存写入,增加带宽压力。

2.2 间接“饿死”片元着色器 (Indirect Starvation)

  • 硬件背景: 在某些移动端 GPU 架构上,COMPUTE 和 VERTEX 工作负载共享同一个硬件队列
  • 问题描述: 如果一个耗时很长的 COMPUTE 任务(例如,一个全屏的后处理特效)长时间占据了该硬件队列,那么紧随其后的 VERTEX 任务就无法被及时处理。
  • 连锁反应: VERTEX 的阻塞会进一步导致 FRAGMENT 阶段没有图元可处理,从而间接“饿死”了片元着色器,造成 GPU 流水线中出现“气泡”(Bubble),即硬件空等,导致整体效率下降。

三、核心最佳实践:避免反向依赖

基于以上问题,讲座重申了一条重要的最佳实践原则。

  • 核心规则: 不要使用 Compute Shader 来处理由 Fragment Shader 刚刚生成的图像。
  • 问题所在: 这种 Fragment -> Compute 的流程会产生反向依赖 (Backwards Dependency)。这打破了GPU流水线通常从前到后(例如 Vertex -> FragmentCompute -> Vertex/Fragment)的自然流动。
  • 负面影响: 反向依赖会导致 GPU 流水线中产生 “气泡” (Bubble)。GPU 必须等待前一个 Fragment 阶段完全结束、数据落到内存后,才能调度并开始执行 Compute 任务,这严重破坏了流水线的并行性和效率。
  • 推荐做法: 如果一个渲染通道(Render Pass)的输出需要被后续的渲染通道消费,应尽量保持 Fragment -> Fragment 的流程(例如,一个后处理接另一个后处理)。这样能让数据在渲染管线(甚至在 Tile-local 的高速缓存)中更平滑、高效地流动,避免不必要的同步和等待。

总结与展望 — 发挥 Vulkan 在 Arm Mali 上的潜力

核心结论:Mali GPU 上的高效计算着色器是可行的

本讲座通过一系列实例和分析证明,尽管在 Arm Mali 架构上高效利用计算着色器(Compute Shaders)存在挑战,但这并非无法实现。关键在于采用正确的策略和 API。

  • 可行性:我们完全可以在 Arm Mali GPU 上实现高效的计算着色器应用,从而避免图形与计算任务之间的互相阻塞和性能瓶颈。
  • 前提条件:这并非一项简单的优化。它需要开发者进行周密的设计和审慎的思考(fair amount of consideration)
  • 实践是检验真理的唯一标准:任何优化都不能凭空猜测,对结果进行精确的性能测量(measuring the results)至关重要,以确保优化真正带来了正面效果。

Vulkan 的独特优势:多队列架构

现代图形 API 的选择是实现这一目标的核心。Vulkan 在这方面展示了相较于传统 API 的巨大优越性。

  • Vulkan 的力量:实现高效并行的关键在于以一种非常特定的方式(very particular)来使用 Vulkan API。它提供了直接访问硬件底层特性的能力。
  • 关键机制:多队列(Multiple Queues):Vulkan 允许开发者操作多个独立的硬件队列。这是实现异步计算(Async Compute),让图形和计算任务在硬件层面真正并行执行的基础。
  • OpenGL ES 的局限:作为对比,OpenGL ES 的 API 设计中完全没有多队列的概念。因此,它从根本上无法实现讲座中讨论的这种深层次、基于硬件队列的并行优化。

异步计算的实践哲学

最后,讲座对异步计算这项优化技术给出了一个清晰的定位和建议。

  • 精细的性能调优工具异步计算(Async Compute) 是一种相对 **“精细”且“敏感”(temperamental)**的优化技术。它不是一个普适的解决方案,更像是挖掘硬件潜力的最后一把钥匙。
  • 榨取极限性能:当应用得当时,它可以帮助我们榨取出最后百分之几的极限性能(squeeze out the last few percentages of performance)。对于追求极致体验的高性能应用(如3A游戏),这部分性能提升可能至关重要。
  • 拥抱现代 APIVulkan 能够以 OpenGL ES 等旧 API 无法企及的方式来利用硬件。对于追求极致性能的渲染工程师来说,投入时间去学习和利用这些现代 API 的高级特性,是打破性能瓶颈、实现技术突破的必经之路。