计算着色器的栅格化的制作方法
【专利摘要】描述了将计算机着色器程序划分为被称作线程循环的最大大小的区域的编译器算法。该算法可以移除原始的基于栅栏的同步,而因此变换的着色器程序在语义上保持等同于原始着色器程序(即,所变换的着色器程序是正确的)。此外,所变换的着色器程序服从经由现有编译器技术所进行的优化,并且能够被(多个)CPU线程有效执行。调度调用能够通过分配单个或多个CPU线程以执行线程分块而在CPU上进行负载平衡。此外,并发执行的线程分块的数量并不使得CPU过载。
【专利说明】计算着色器的栅格化
【背景技术】
[0001] 近来的趋势显示用于通用计算的GPU (图形处理单元)(GPGPU)的使用明显增长。 也就是说,GPU趋向于被用于并非必然与计算机图形相关的计算,诸如物理仿真、视频代码 转换和其它数据并行计算。此外,在GPU中引入片上共享存储器已经导致了诸如全前缀和 (scan)、柱状图计算、卷积、快速傅里叶变换(FFT)、物理仿真等广泛使用的计算密集型算 法的性能出现了引人注目的增长。微软公司提出了 Direct X (TM)HLSL (高阶着色语言) (TM)Compute Shader,作为访问和利用共享存储器容量的软件API (应用编程接口)。注意 到,Direct X、HLSL和Compute Shader将作为示例被提到,所要理解的是,针对其评论和讨 论可同样应用于诸如CUDA (计算统一设备架构)、0penCL (开放计算语言)等的其它着色语 言。这些将被统称为"计算着色器"。
[0002] 完整软件平台应当在CPU上提供计算着色器(等)的有效软件栅格化以便在GPU 硬件并不作为选项或者当软件平台在无头(headle SS)VM (虚拟机)情形中使用时提供回退 (fallback),而无需实施GPU和CPU硬件解决方案。也就是说,有时期望在CPU而不是GPU 上执行着色器语言代码。然而,将以GPU为中心的计算着色器有效映射到CPU上是很重要 的,这主要是由于线程栅栏(barrier)所施行的线程同步(或syncs)。
[0003] 虽然标量着色器代码的效率是重要的,但是这里的讨论涉及有效地将在计算着色 器中所找到的并行性映射到CPU上(与GPU相对)。计算着色器可以以不同方式表现出并行 性。例如,Direct Compute (TM)调度调用定义了线程分块的网格而在粗糙级别上表现出并 行性,这对于映射到CPU线程上并不重要。每个线程分块是多个着色器线程所执行的计算 着色器程序的实例(例如,着色器类似CUDA中的内核)。分开的着色器线程可以经由分块中 的线程共用但是线程分块所私有的共享存储器共享数据。每个线程分块的线程可以经由栅 栏进行同步而使得能够在不担心出现数据竞争的条件的情况下访问共享存储器。GPU通常 在线程群组(扭曲或波前)中经由硬件线程环境来执行计算着色器,并且每个环境可以合法 执行程序直至其遇到栅栏,该环境在该栅栏点必须等待所有其他环境都到达相同栅栏。在 GPU中进行切换的硬件环境是快速且重度管道化。然而,CPU并没有这样的硬件支持,这使 得难以在CPU上有效地执行计算着色器。
[0004] 以下所讨论的技术涉及将计算着色器程序变换为在CPU上传递可接受性能的等 同CPU程序。
【发明内容】
[0005] 以下
【发明内容】
被包括仅是为了对以下【具体实施方式】中所讨论的一些概念进行介 绍。该
【发明内容】
并非是综合性的且并非意在对结尾处呈现的权利要求所阐述的要求保护的 主题的范围进行限定。
[0006] 这里描述了将计算着色器程序划分为被称作线程循环(thread-loop)的最大大小 的区域的编译器算法。该算法可以去除原始的基于栅栏的同步,而因此变换的着色器程序 在语义上保持等同于原始着色器程序(即,所变换的着色器程序是正确的)。此外,所变换的 着色器程序服从经由现有编译器技术所进行的优化,并且能够被(多个)CPU线程有效执行。 调度调用能够通过分配单个或多个CPU线程以执行线程分块而在CPU上进行负载平衡。此 夕卜,并发执行的线程分块的数量并不使得CPU过载。注意,(多个)CPU线程执行多个线程分 块(计算着色器程序的实例)。调度调用可以指定数百万个实例的执行。如果所有这些线程 分块都一起启动,则可能出现诸如存储器过度消耗、高速缓存利用不良和频繁环境切换的 问题。
[0007] 以下将参考随后结合附图所考虑的详细描述而对许多附带特征进行描述。
【专利附图】
【附图说明】
[0008] 本描述将通过以下根据附图进行阅读的详细描述而被更好地理解,其中同样的附 图标记用来指示伴随描述中同样的部分。
[0009] 图1示出了计算着色器划分算法的环境。
[0010] 图2示出了划分算法的分析途径(pass)。
[0011] 图3示出了用于从所标记的IL代码形成最大大小的线程循环的步骤。
[0012] 图4示出了其中循环条件中断嵌套在线程循环中的示例输入。
[0013] 图5示出了导致相异控制流程(DCF)的返回的示例。
[0014] 图6示出了计算设备。
【具体实施方式】
[0015] 有若干种方式能够将计算着色器映射到CPU上。一种简单的方法是模仿GPU模型; 即在线程群组中对原始程序进行解释并且在遇到栅栏时产生执行。然而,该方法由于CPU 高的环境切换开销所以性能很差。可替换地,能够在栅栏处对着色器进行划分,例如: B1 //不执行栅栏的代码分块 栅栏//线程分块的所有线程必须到达这里并且只有在其之后才能继续执行 B2 //另一代码分块 被变换成 针对所有线程执行B1 [t] //其中B1 [t]是如通过线程t执行的代码分块实例 针对所有线程执行B2[t]。
[0016] 该技术是正确且有效的。线程循环(t循环)被表示为"针对所有线程执行"。如名 称所暗示的,t循环针对线程分块的每个线程t执行一次迭代。为了保持原始着色器程序 的语义,原始变量必须被私有化;即,每个线程使用其自己的变量集合,被称作t循环变量。 除其它方式之外,这例如可以通过将每个变量V形成为大小为T的数组来实现,T是线程分 块中的线程数量,从而线程t使用v[t]作为其副本。因此,指示原始代码分块B的表示形 式B[t]使用线程t所私有的变量集合。注意到,并非所有变量都应当被复制并且可能进行 一些优化。此外,虽然有效的标量代码生成是有用的,但是这里的讨论涉及到将计算着色器 并行性有效地映射到CPU上。因此,t循环也可以被视为针对所有循环都是并行的。如将 会看到的,t循环的迭代是独立的并且能够由一个或多个CPU线程以任意顺序合法执行。
[0017] 以上方法对于简单划分是直接的。然而,如果栅栏嵌套在控制流程构造之内(例 如,if语句、switch语句等),则必须注意不要破坏程序的辖域结构。由于HLSL和其它着色 语言具有妥善定义的辖域(Sub、If、Switch和Loop),所以这些可以被轻易优化。为了保留 辖域属性,每个t循环应当被适当嵌套。可替换地,可能需要任意的goto,这将明显使得优 化复杂化并且对于及时(JIT)编译可能是无法接受的。考虑以下示例: B1 if (cl) // cl 是变量 B2 栅栏 B3 endif B4 其被变换成 针对所有线程执行B1 [t] if(cl [0]) 针对所有线程执行B2[t] 针对所有线程执行B3 [t] endif 针对所有线程执行B4[t]
[0018] 注意到,任何栅栏都必须在统一控制流程(UCF)中执行(所有线程都执行该语句)。 换句话说,线程分块中的所有线程都必须到达正确程序中的栅栏。因此,以上示例中的 " if (cl) "必须是统一转移(transfer ),并且仅对一个实例进行检查就足矣,例如线程0- cl [0]中的cl实例。
[0019] 用于插入T循环的算法 图1示出了计算着色器划分算法的环境。最初,诸如HLSL计算着色器的着色语言源 代码100被编译以产生中间语言(IL) 102代码(S卩,中间表示(IR)代码、字节码等),其可以 预期用于GPU的并行性。可替换地,该算法能够被直接应用于源代码。按照编译器选项或 其它环境或配置设置,编译器108调用划分算法110,其对产生划分IL代码112的IL代码 102进行变换,该划分IL代码112具有最大大小区域(线程循环)的并且基于栅栏的同步得 以被去除。像任意其它IL 一样,划分IL代码112随后被编译并可能被优化为CPU可执行 的机器指令114以便在CPU中进行执行,该CPU并不是GPU。编译器108可以是在线JIT编 译器或者其可以是产生所存储的机器可执行程序的离线编译器。
[0020] 图2示出了划分算法110的分析途径120。如将要解释的,分析途径120标记用于 划分的代码并且被应用于IL代码102中的每个IL运算。入口子程序(着色器程序的入口 点)和可以执行栅栏自身或经由调用链执行的每个子程序能够如下进行划分。在步骤122, 针对入口子程序,标记子程序运算的开始和结束,这包含了其中主要子程序并没有嵌套的 栅栏的情形。在步骤124,对嵌套在子程序内的栅栏进行标记,但其并非处于被调用方(被 调用子程序)之中。在步骤126,标记可以执行栅栏的被调用方。在步骤128,通过标记嵌 套包括子程序辖域的栅栏的所有辖域而对每个栅栏等进行处理。这样标记的运算可以包括 开始和结束或者辖域运算以及相关的IR运算,诸如if语句的OpElse,以及switch语句的 OpSwitchCase和OpSwitchDefault。一个实施例还可以验证所有嵌套辖域在UCF中执行并 且其条件表达式(如果有)为统一数值(对于每个线程都相同)。注意到,确定子程序是直接 执行栅栏还是经由嵌套在子程序中的调用点执行可以通过在调用图上取得传递闭包而轻 易地进行计算。步骤124U26和128应当以图2所示的顺序来执行。该分析途径输出标记 IL代码130。
[0021] 图3示出了用于从标记IL代码130形成最大大小的线程循环的步骤132。步骤 132涉及按顺序遍历子程序运算并且针对每个标记运算(Op)执行任意的相关步骤134、 136、138、140。在步骤134,除非当前所处理的运算是子程序的开头,否则就在当前运算之 前插入OpEndThreadLoop运算。在步骤136,确定OpEndThreadLoop之前的运算是否为 OpBeginThreadLoop,并且如果是,则移除 OpEndThreadLoop 和 OpBeginThreadLoop,因为 它们表示空的t循环。为了详细描述,两个标记运算可能互相跟随。因此,代码将具有如下 内容 OpBeginThreadLoop OpEndThreadLoop 0pl OpBeginThreadLoop // 空 t 循环将 OpEndThreadLoop // 被移除 0p2 OpBeginThreadLoop OpEndThreadLoop 因此,存在仍然处于代码中的空白t循环,但是其并不进行任何操作并且能够被移除。
[0022] 在步骤138,除非当前运算是子程序的结尾,否则就在当前运算之后插入 OpBeginThreadLoop运算。最后,在步骤140,如果当前运算是栅栏或类似类型的同步运算, 则移除该运算,因为原始数据依赖性目前由线程循环的执行顺序所施行。
[0023] 注意到,划分算法110创建了最大大小的t循环而并没有破坏辖域结构并且所插 入的t循环被适当嵌套。然而,存在着控制流程转移运算,诸如可以将控制转移到t循环之 外的中断、继续和返回。这些可能需要被特殊对待以保留着色器程序的正确性。如果这些 运算并没有将控制转移到t循环之外(S卩,它们嵌套在其相对应的辖域之内),则它们以标准 方式得以被处置。
[0024] 关于术语"最大",该术语表示在不破坏程序的嵌套结构的情况下不可能增加 t循 环所包含的代码数量。最小大小的t循环(或区域)将包含单一运算一也可能包含划分,但 是该程序由于t循环开销而将是缓慢的。最大大小的t循环减少了 t循环的总体数量并且 因此降低了相关联的开销。
[0025] 图4示出了其中循环条件中断嵌套在t循环中的示例输入150。输入150如输出 152中所示地被变换。
[0026] 使用了四个助手标志变量,四种不同类型的控制转移(中断、继续、切换中断和返 回)中的每一个一个助手标志变量。每个这样的变量是标量,因为转移条件的数值是统一 的。如果它们在t循环中使用,则该变量在t循环之前被初始化为假。嵌套在统一 t循 环中的循环中断/继续将把bBreakFlag/bContinueFlag设置为真并且将控制转移至封 闭的t循环的下一次迭代,并且在t循环完成之后,如果bBreakFlag/bContinueFlag被 设置,则中断/继续相对应的循环。所执行的嵌套在统一t循环中的统一切换中断将 bSwitchBreakFlag设置为真并且将控制转移至封闭的t循环的下一次迭代,并且在t循环 完成之后,如果bSwitchBreakFlag被设置,贝U中断切换。最后,嵌套在统一 t循环中的统一 返回将bReturnFlag设置为真并且将控制转移至封闭的t循环的下一次迭代,并且在t循 环完成之后,如果bReturnFlag被设置则从子程序返回。
[0027] 控制在T循环之外的相异转移 可能出现的是,t循环之外的转移是相异的(非统一的)。图5示出了导致相异控制流程 (DCF)的这种返回的示例。DCF输入160如输出162中所示的那样被变换。该变换引入了 t循环掩码TLoopRetMask,其每个线程由一个实例,以捕捉哪个线程执行了相异返回。如果 子程序包含这样的返回,则插入TLoopRetMask的规则如下: (1) 在入口 t循环的每个开始处在子程序的入口上将TLoopRetMask初始化为真。如果 没有入口 t循环,则生成一个。 (2) 如果存在将控制转移至该t循环之外的至少一个DCF返回,则针对将控制转移至t 循环之外的每个DCF返回以及UCF返回将TLoopRetMask设置为假。 (3) 针对在DCF中开始的每个t循环,在t循环的每个开始处生成保护代码,其在 TLoopRetMask为假的情况下将会跳过迭代。 (4) 在出口 t循环的每个结尾处,在来自子程序的出口上将TLoopRetMask重新初始化 为真,除非其是主要子程序。如果没有出口 t循环,则生成一个。
[0028] 即使在若干子程序中可能有所要求,但是每个程序仅使用一个TLoopRetMask就 足够。虽然这降低了开销,但是其也成为了在来自子程序的出口上对掩膜进行重新初始化 的原因。使用跨线程复制的更多掩码变量,增加了存储器覆盖区(footprint)并且是不期 望见到的。
[0029] 将控制转移至t循环之外的循环中断和继续无法处于DCF中。如果中断或继续做 这样转移控制,则整个循环将不会处于DCF中(由于后沿)并且无法执行任何栅栏。并且,由 于t循环由栅栏所引发,所以这样的循环必须根据我们的划分算法嵌套在t循环之内。
[0030] 将控制转移至t循环之外的DCF切换-中断能够经由管理TLoopSwitchMask的状 态而以与DCF返回完全相同的方式进行处理。为了避免使用若干个这样的变量和复杂的代 码生成,TLoopSwitchMask可以在这样的切换之前被初始化为真,并且TLoopSwitchMask可 以在该切换之后被重新初始化为真。该方法正确地变换嵌套切换。假设切换S2嵌套在切 换S1的情况之中。如果S2要求TLoopSwitchMask,则S2包含嵌套的栅栏(否则,其将被嵌 套在t循环中),而使得S1和S2必须在UCF中开始。此外,该控制在S1的情况中至少直到 S2开始都是统一的(否则,S2将处于DCF中),因此TLoopSwitchMask在S2开始之前必须为 真。由于S2中的DCF切换中断的影响仅被传播至S2的结尾而对于S1中的控制流程类型并 没有影响,所以在S2之后对TLoopSwitchMask重新初始化是安全的,因为TLoopSwitchMask 还没有被S1的DCF中断的任何影响所捕捉(虽然其在S2中使用)。这里可能要做的是插入 TLoopRetMask和TLoopSwitchMask的重新初始化以基本上避免对于针对在性能计算着色 器中很少出现的情形要在何处恰好插入掩码初始化的复杂分析。最后,嵌套在切换之中并 且在DCF中开始的t循环的保护条件必须被设置为"TLoopRetMask&&TLoopSwitchMask"以 说明DCF返回和切换中断的影响。
[0031] T循环在CPU上的执行 为了实现高性能,通过具有相当粗糙的工作单元并且通过对处理器进行负载平衡而使 得同步的开销最小化会有所帮助。计算着色器调度调用的典型革新形成了计算着色器程序 的许多事例,其中每个实例通常大致地执行类似数量的工作。因此,自然使用单个CPU线程 来执行一个实例:工作的单元是粗糙的,执行t循环的并发性开销由于串行执行而并不存 在,并且负载平衡是合理的。
[0032] 可是,一些计算着色器被写入到工作的"流"单元而并非依赖于调度调用来进行流 传输;即调度调用仅实例化计算着色器程序的几个实例,并且每个实例具有逐个处理若干 个工作单元的流循环。在这种场景下,负载平衡由于一些CPU线程的利用不足而受到影响。 为了避免这种情况,能够使用若干个CPU线程来运行每个线程循环,由于t循环迭代是独 立的,所以这样是合法的。这些线程在每个t循环之前和之后必须同步并且使用原子增量 来获得t循环迭代的线程指标t。其结果是以小幅同步开销为代价的更好的负载平衡。注 意到,调度(及其在各种着色器语言中的等同形式)能够不仅为了执行而调用着色器语言代 码,而且调度或绘制调用可能还会对着色器语言中的各种图形对象加以引用,诸如像素、顶 点等。调度(及其等同形式)和线程分块的其它细节可在其它地方得到(例如,参见Zink等 人的 "Practical Rendering and Computation with Direct3D 11,',2011,CRC 出版社)。
[0033] 优化调度 通常,调度调用(及其等同形式)形成数百或数千个计算着色器实例。当在CPU而不是 GPU上运行时,同时执行所有实例将会(1)产生过多线程,(2)使用过多存储器,(3)导致许 多高成本的环境切换,以及(4)由于频繁的环境切换而污染高速缓存。这由于系统的过度 预定而导致了性能不良。因此,限制并发执行的计算着色器实例的数量可能会有所帮助,例 如,计算着色器实例的数量可以被限制为2加上CPU内核的数量。
[0034] 结论 图6示出了计算设备180。计算设备180是能够执行以上所描述的实施例的设备类型 的示例。计算设备180可以具有以下的一些或全部:显示器182、输入设备184(例如,键盘、 鼠标触摸敏感区域等)、CPU 186、GPU 188和存储媒体190。这些组件可以以计算领域已知 的方式协作。
[0035] 以上所讨论的实施例和特征能够以易失性或非易失性计算机或设备可读存储媒 体中所存储的信息的形式来实现。这被认为至少包括物理存储媒体,诸如光学存储(例如, 紧致盘只读存储器(CD-ROM))、磁性媒体、只读闪存(ROM)或者物理存储数字信息的任意器 件(排除载波、信号本身等)。所存储的信息可以为机器可执行指令(例如,经编译的可执行 二进制代码)、源代码、字节码或者能够用来使能或配置计算设备以执行以上所讨论的各个 实施例的任意其它信息。这还被认为至少包括易失性存储器,诸如在执行实施例的程序的 执行期间存储诸如中央处理器(CPU)指令的信息的随机访问存储器(RAM)和/或虚拟存储 器;以及非易失性媒体,其存储允许程序或可执行程序得以被加载并执行的信息。如这里所 使用的术语媒体是指物理设备和材料而并非是指信号本身、载波或者任意其它瞬时形式的 能量本身。实施例和特征能够在任意类型的计算设备上执行,包括便携式设备、工作站、月艮 务器、移动无线设备等。
【权利要求】
1. 一种对由着色语言的源代码所组成的计算着色器所编译的中间表示(IR)代码进行 变换的方法,包括: 执行标记和分析途径,其包括标记IR代码中的子程序的开始和结尾,针对每个这样的 栅栏标记嵌套辖域和相关运算标记嵌套在子程序中的存储器栅栏,并且标记子程序中被确 定为执行栅栏的调用点;并且 通过对子程序的运算进行处理而使用所标记的IR代码形成最大大小的线程循环。
2. 根据权利要求1的方法,进一步包括确定当前所处理的运算并不是子程序的开始, 并且作为响应在当前所处理的运算之前插入结束线程循环运算。
3. 根据权利要求2的方法,在开始线程循环运算紧处于结束线程循环运算之前时移 除结束线程循环运算和相对应的开始线程循环运算。
4. 根据权利要求1的方法,进一步包括确定当前所处理的运算并不是子程序的结尾, 并且作为响应直接在当前所处理的运算之后插入开始线程循环运算。
5. 根据权利要求1的方法,进一步包括输出更新的IR代码,其中IR代码中对应于源 代码中的栅栏语句的栅栏运算从IR代码中被移除并且线程循环被添加至该IR代码。
6. 根据权利要求5的方法,其中对栅栏被移除的经变换的IR代码进行编译并且在并 非GPU的CPU上执行,并且其中由多个CPU线程执行线程循环。
7. -个或多个计算机可读媒体,其存储信息而使得计算设备能够执行处理,该处理包 括: 执行在编译器接收到对应于着色器程序的代码时所调用的编译器算法,该着色器程序 执行非图形计算并且包括着色器语言代码;并且 通过该算法将着色器程序的代码划分为最大大小的线程循环并且从该代码中去除同 步栅栏。
8. 根据权利要求7的一个或多个计算机可读媒体,其中该划分被执行而使得着色器 程序中将被同步栅栏所施行的数据依赖性尽管在同步栅栏被移除的情况下也得以保留。
9. 根据权利要求8的一个或多个计算机可读媒体,其中该划分保留了代码中至少子 程序或控制流程构造的嵌套。
10. -种对由着色语言的源代码所组成的计算着色器所编译的中间表示(IR)代码进 行变换的方法,包括: 识别IR代码中的统一控制流程(UCF)转移和相异控制流程(DCF)转移; 当将IR代码变换为线程循环时,对线程循环之外的UCF转移进行优化并且对线程循环 之外的DCF转移进行优化;并且 在并非GPU的CPU上执行经变换的IR代码。
【文档编号】G06F9/30GK104106049SQ201380009663
【公开日】2014年10月15日 申请日期:2013年2月15日 优先权日:2012年2月16日
【发明者】A.格莱斯特, B.P.泰恩, D.塞西安斯, M.莱亚普诺夫, Y.多特森科 申请人:微软公司