GPU程序缓存(GPU Program Caching)

GPU程序缓存

翻译文章: GPU Program Caching

总览 / 为什么

因为有一个沙盒, 每一次加载页面, 我们都会转化, 编译和链接它的GPU着色器. 当然不是每一个页面都需要着色器, 合成器使用了一些着色器, 这些着色器需要为tab选项卡重新渲染. 我们应该去缓存一些之前的缓存程序, 并在重新需要的时候, 直接使用他们.

我们通过一个GPU缓存完成这项缓存, 这里会使用基于内存, 或者磁盘的缓存来加速这一过程.

缓存等级

内存缓存(In-Memory Cache)

由于磁盘的访问时间未知(以及所需要的IPC调用), 二进制中所有命中的缓存都来自内存缓存. 基于磁盘的缓存加载在启动时进行.

内存缓存的思路关键

内存缓存主要存储在GPU的通道管理器中, 所以存储在GPU的生命周期的线程中. 因为这个原因, 我们可以假定在内存缓存的生命周期中, 相同着色器的二进制编码不会改变(驱动程序不会变, 供应商不会变等). 所以 我们的关键在与没有转换的着色器源组成. 因为我们并想要限制秘钥的大小, 我们只需要SHA1hash值对源进行散列.

当再次启动一个GPU程序(这里包含两个着色器(shaders)), 我们还需要在秘钥(key)中, 包含一个属性位置图, 因为它可以影响而二进制的结果, 并对相同的着色器加以区分. 所以, 我们对两个着色器的sha1, 做了一个SHA1的散列, 并放到了属性图中

磁盘缓存(Disk Cache)

磁盘缓存帮助内存缓存作为一种永久的缓存. 它拥有和内存缓存一样的最大容量, 并且所有的程序缓存到内存缓存的时候, 也会通知内存缓存.

允许磁盘缓存命中的选项中, 包含一个锁定GPU程序信息, 并在我们继续执行的时候, 异步读取二进制信息. 如果将来任何调用涉及到GPU程序, 那会一直等到异步加载完成. 然而, 因为这是一个普通的模式, 见检查了程序的链接状态后, 立即链接(所以, 程序是在异步执行结束后立即运行的), 让其忽略了这个选项.

磁盘缓存的思路关键

因为会一直存在磁盘里面, 我们需要包含任何会影响未被转换的着色器的二进制内容. 这包含了对驱动器和可能在chromium中转换器的更改. 所以我们想要包括:

  • 没有转换的着色器源(untranslated shader sources)

  • 绑定的属性位置图(bound attribute location map)

  • glGetString(GL_VENDOR)

  • glGetString(GL_RENDERER)

  • 驱动器版本号(Driver Version ID)

  • 供应商标识(Vender ID)

  • Chrome Build # *(打包后的chrome??)

    那是一个GPU程序不能使用的, 只能使用在chrome项目中. 如果磁盘缓存一直在chrome中, 应该没问题.

磁盘缓存的行文

磁盘缓存需要增加在程序启动时的缓存能力, 才能不造成任何性能问题. 因为磁盘缓存的访问时间未知(事实上仅仅编译和链接一个程序, 比从磁盘读取一个二进制的文件要快), 我们永远不会使用磁盘缓存作为缓存的提供者. 相反, 我们从启动一开始, 就加载来自内存的缓存.

为了获得最佳的行文, 磁盘缓存需要:

  • 启动一开始, 就加载二进制文件

    • 因为二进制大小都在1-20kb左右, 并且我们使用了IPC的方式, 所以我们不能一次性加载全部的
    • 磁盘缓存最坏的情况是, 每个文件都死空的, 所以这不应该阻塞启动, 相反, 我们需要在一个单独的线程上懒加载完成.
    • 应该在我们发送一个IPC之前的的时候, 进行"秘钥兼容性"的检查
  • 异步的方式执行缓存的更新/写入(没有读取, 只通过内存缓存保持最新)

  • 浏览器清理缓存的时候被删除

    实现的时候, 必须注意启动时的竞争情况, 那里就是合成器使用着色器的地方, 这些着色器可能来磁盘, 也可能不来自磁盘, 这会导致一个问题: 我们应该能够把一些程序标记为, 启动时立即加载吗? 合成器中的着色器使用的数据, 是从磁盘中获取快, 还是进行普通的链接和编译快呢?这是被认为是未来的事情, 尽管有些过头了.

回收(Eviction)

考虑过程(Considerations)

源数据加载的时候, 页面中最佳的回收方案是MRU. 这是因为同一份二进制文件无法使用两次, 我们只需要加载一次.

然而, 如果考虑到我们会运行访问不同的页面, 这不再可行, 因为页面的访问不遵循资源的加载模式 背景页会限制当前页可用的缓存空间.

所以, 一个更好的加载方式是页面使用LRU, 每个页面都有MRU. 所以, 你可以从最近最少使用的页面上的最进用过的程序上收集. 记住一点, 如果在新的tab页中, 程序再次被贴之前的标签, 程序会被老tab的MRU从队列中删除, 加入到新tab页的MPU队列中.

最后选择(Final choice)

因为我们无法准确的知道GPU进程中, 我们在的选项卡/页面上, 我们使用LRU协议进行回收. 只有我们不能将某个页面的所有的gpu程序全部放入到缓存中的时候, 才会导致问题. 因为我们会回收第一个程序在页面上的缓存, 重新加载后, 我们会进行重新编译. 目前二进制的大小容易管理(加载Mini Ninjas和From Dust导致最少小6mb或者二进制), 但如果这编程了一个问题, 那之后的回收计划, 应该就像上文说述.

存储状态(Status Storage)

我们获取或者保存程序二进制之前, 会做一系列的'状态'检查, 来避免装换过程中/编译过程中/链接过程中的着色器被缓存. 所以我们以下的状态信息:

  • 着色器编辑(Shader Compilation)

    • SHA1(untranslated shader)*
      • 编译状态中(成功, 未知)
      • 引用程序的链接计数(Reference count of linked programs)**
  • 程序链接(Program Linking)

    • SHA1( SHA1(untranslated vertex shader)*** + SHA1(untranslated fragment shader) + attribute location binding map)
      • 链接状态(成功, 未知)
  • *: SHA1用来避免在内存中保存, 无边际, 未转换的脚本.

  • **: 我们保持一个参考数值, 以便在移出着色编译器状态或者回收程序的时候, 有所存储.(因为相同的一个着色器可能被用在不同的程序中)

  • ***: 链接的秘钥使用的是着色器的SHA1, 所以我们可以在回收期间, 引用着色器的编译状态, 不需要访问未转换的着色器源.

二进制缓存

内存中

链接程序或者确定程序在缓存之后, 我们访问内存中存储的二进制. 存储的秘钥和上面程序链接状态的秘钥相同, 并且, 我们在缓存中存了下面这些值:

  • SHA1(untranslated vertes shader) (未翻译的顶点着色器)

  • SHA1(untranslated fragment shader) (未翻译的片段着色器)

  • vertex shader attribute to shortened name map (顶点着色器属性被压缩在名称地图中)

  • vertex shader uniform to shortened name map (片段着色器属性被) 统一顶点着色器

  • fragment shader attribute to shortened name map(片段着色器) 片段着色器属性压缩到简称映射表中

  • fragment shader uniform to shortened name map(片段着色器统一到简称映射表中)

  • glGetProgramBinary的值

    • length (长度)
    • format (格式化)
    • data (数据)

    我们可以存储哈希过后的着色器, 所以我们可以正确回收编译状态内存

磁盘

存储方案还没确定, 但是我们需要适应以下情况:

  • 存储的所有东西是内存中二进制存储需要的
  • 存储的所有东西都需要创建一个对应内存的key
  • 若没有在磁盘缓存的秘钥对称表中匹配到缓存, 就不去加载到内存中

直方图(Histograms)

下面的直方图会帮助我们调整缓存

  • 程序二进制大小 - 每一个被链接的二进制, 不是每一个被使用的

  • 程序缓存大小(存储前+存储后)

  • 二进制缓存命中的时间

  • 二进制缓存未命中链接时间

  • 状态缓存命中时间

  • 状态缓存未命中编译时间

    注释: 二进制缓存命中时间从8月20到22号, 产生的直方图不正确.

缓存大小分布计算(Cache Size Distribution Calculation)

这是12/21/8普通的内存使用情况简图. x轴表示大小, kb, y轴表示可不短总数

直方图

我认为随着更多基于web的游戏出现, 存储会继续上升. 从Dust启动后, 缓存的大小约3MB或者4MB.

代码结构(Code Structure)

主要类(Main Classes)

  • 程序缓存(program_cache): 这是基础的程序缓存类, 保存状态信息并提供对二级制的保存/加载的虚拟方法
  • 内存程序缓存(memory_program_cache): 内存中的程序缓存, 没有磁盘后端
  • 程序缓存lru助手(program_cache_lru_helper): 一个帮助lru策略的实用状态类

测试

程序缓存Lru助手(ProgramCacheLruHelper)

  • 不重复使用LRU收回命令(LRU eviction order w/o reuse)
  • LRU eviction order w/ reuse(ps: 不知道w/o 表示什么)
  • 清空工作正常
  • peek/pop工作正常(集成到命令测试中)

程序缓存(ProgramCache)

  • 编译状态存储, 确保key被复制
  • 编译器不知道源代码更改
  • 链接状态存储, 确保key被复制
  • 链接无法得知顶点和片段源更改
  • Eviction w/o shader reuse
  • Eviction w/ shader reuse
  • 清除工作正确

内存程序缓存(MemoryProgramCache)

  • 二进制保存时, gl正常调用, 链接状态正确
  • 加载时gl正常调用, 属性和统一映射设置正确, 二进制保存的是返回的二进制
  • 不同源不能返回同一个程序
  • 不同的属性映射表不能返回同一个程序
  • 缓存已满时保存进行适当回收

程序管理(ProgramManager)

  • 编译时缓存未命中, 调用glCompile, 把状态设置未成功
  • 在编译器报错的期间, 不能设置状态
  • 编译器状态成功时, 不能编译
  • 链接程序缓存未命中
  • 缓存未命中+链接的状态下, 重用未编译的编译着色器
  • 正确的程序缓存设置(调用LoadLinkedProgram, 不再链接和再次缓存)
  • 如果正在加载缓存程序, 进行编译和链接的话, 返回错误
原文地址:https://www.cnblogs.com/zhangrunhao/p/11057825.html