??xml version="1.0" encoding="utf-8" standalone="yes"?> SALVIA 0.5.2 的优化经历是一个“跌宕v伏”的q程。这个过E的l果很简单:(x) 在Core 2 Duo T5800(2.0GHz x 2)上,Sponza的性能提升?0%QComplexMesh性能提升?6%?/p> SALVIA的整个渲染流E主要是以下几部分:(x) 用于试的场景:(x) 最初的版本QV1231Q中Q性能的主要瓶颈在插值阶D,各种耗时林林Ld?jin)一半以上(50% - 70%Q?/p> 相比之下其他阶段Ҏ(gu)能的媄(jing)响要么有限,要么没有多少优化I间。所以最q一周的优化Q就都集中在?jin)“插值”上?/p> U性的插值算?/strong>常见的实现有两种Q?/p> W一U是拿UV插|W二U是用ddx和ddy累积?/p> UV是先计算像素的u和vQ基本方法是用面U比Q不记得复?fn)一下中学几何吧Q,然后用插值公式:(x) pixel = v0 * u + v1 * v + v2 * (1-u-v) 后者的步骤是选一个主点Q然后计这个顶点的ddx和ddyQ最后用 pixel = v0 + ddx * offset_x + ddy * offset_y 计算出相应顶炏V?/p> 但是在图形学中,我们q需要对插D行透视修正Q获得在3DI间中线性的插值结果?/p> 我们步骤修正到透视I间Q?/p> 先将v0Qv1Qv2弄到透视I间中,变成projected_v0, projected_v1, projected_v2 对于UV的插值是 pixel = ( projected_v0*u + projected_v1*v + projected_v2 * (1-u-v) ) / pixel_w 对于用ddx和ddy的篏U公式是Q?/p> pixel = ( projected_v0 + projected_ddx * offset_x + projected_ddy * offset_y ) / pixel_w 何咏QGraphixerQ大之前也写了(jin)一个渲染器Q比我快许多Q大概是4-6倍)(j)Q用的是UVQ?/p> gameKnife大神两个C拜写成的渲染器Q速度比我用五q写出来的半成品要快7倍,用的办法是Lerp到Scanline上,再Lerp到像素?/p> SALVIA采用?jin)篏U法Q?/p> 注意那个MAX_ATTRIBUTE_COUNTQ这个值通常比较大,在v1231中,它是32?/p>
不过Q显然我们不需要对所有的属性进行计。敏敏在q里q用?jin)一点小的技巧进行了(jin)优化Q只计算必要的属性。同ӞZ(jin)减少分支的用,他甚至用 q合函数指针的Ҏ(gu)Q以?j)ɾ~译器展开循环Q减分支?/p>
不过从实际生成的汇编来看Q这个部分ƈ没有被展开到期望的形式Q可能是~译器认为x86的Branch Predication性能已经_高(sh)(jin)吧?/p>
q个“优化”在v1231中就已经具备?jin)?/p>
W一个Profiling是用BenchmarkPartOfSponza和Sponza跑的QunprojectQoperator +=和operator = 加在一起大U占用了(jin)15-20%的时间。单独的unproject 最初的实现是普通的标量。既不要求对齐,也没有用SIMD?/p>
所以当然会(x)以ؓ(f)用了(jin)SIMD后,优化效果?x)很好。于是在v1232中,中间点和像素输入的分配都以16字节寚wQunprojQ?=?也都使用?jin)SSEq行?jin)重写?/p>
从跑分来看,PartOfSponza性能提升?0%。但是,在测试ComplexMesh和SponzaӞq未发现帧率有显著提升?/p>
其实在进行优化之前,何咏告诫过我,因ؓ(f)CCPU的一些技术,比方说超标量啥的Q四个数据宽度的SSE和标量运相比,只?0%的性能差距?/p>
q且q些函数的指令已l极为简单,瓉也很明确的落在计指令上。例如Unproject优化后,性能焦点p在_mm_mul_ps上(3.7%Q,几无优化余地?/p>
在进行第二轮优化之前同样q行?jin)一ơProfiling。因为对PartOfSponza性能基本满意Q因此这ơ优化的目标主要在Sponza上?/p>
排名前几位的函敎ͼ分别是sub_nQunprojQ?= 和tex2D。对sub_n例行优化后,性能没什么变化。当?dng)q也是意料之中的事情?jin)?/p>
因此Q第二轮优化便着重考虑在插值算法本w上?/p>
在优化之前,我尝试对代码成本做个_略的评伎ͼ(x) 在现有算法下Q假设每个像素有N个需要插值的属性,则^均每个像素有 QcornerQ?N/16个读 + 2N/16个乘?+ 2N/16个加?+ N/16个写 QxQ?=Q?N个读 + N个加?+ N个写 QxQ?Q?nbsp; N个读 + 1个标量除?+ N个乘?+ N个写 QyQ?=Q?N/4个读 + N/4个加?+ N/4个写 QyQ?Q?N/4个读 + N/4个写 因ؓ(f)每个都是函数指针Q所以这些都是优化不掉的。因此首先将一些操作合q了(jin)一下,比如?= ?合ƈ以减一下读写操作。只可惜效果也不是很明显?/p>
W二刀q到算法的头上。因为篏加本w是Z(jin)减少乘法的运用,但是q可能带来了(jin)多余的存取开销?/p>
因此直接套用公式Q?/p>
pixel = ( projected_v0 + projected_ddx * offset_x + projected_ddy * offset_y ) / pixel_w q样有Q?N读,2N乘法Q?N加法QN个乘法和N个写Q假讑֯存器够用的话Q。不Corner的计成本,q样比较一下,q于是3N/4个读QN/2+N个写QN/4个加法来换取2N个乘法的旉。本来以Z为IO瓉的应用,q样可以提高?sh)些性能。不q结果证实这个买卖实在是很不划算Q整体性能不增反减?/p>
虽然所有的操作只针对已使用的属性,但是I间上还是浪费了(jin)许多?/p>
考虑到内存占用较大也?x)导致一些性能损失Q于是将MAX_ATTRIBUTE_COUNT?2下调C(jin)8?/p>
l果令h大跌眼镜。性能瞬间提升?0-30%之多?/p>
再加上SSE也不知道Z么开始发力了(jin)Q用上之后性能大约又有?0-15%的提升?/p>
我猜可能是因ؓ(f)换页频率下降Q以?qing)Cache的命中率提升。不q手上没有VTuneq种工具Q所以也不太好验证?/p>
在这轮优化之后,PartOfSponza出现?jin)精度问题。因锥体的上下左叛_个面都没有ClipQ所以可能会(x)出现非常大的三角形。这L(fng)U的时候一旦v始点选择的不好,׃(x)出现比较大的误差。在之前版本中,使用/fp: precise来减这一问题出现的机?x)。但是因Z用了(jin)SSEQ也让这个问题再难解冟뀂因此我选用?jin)一些办法,来改善精度问题。在大问题都修正以后Q换?fp: fast来编译整个SALVIAQ最l也获得?-10%左右的性能收益?/p>
对于q算和IO都密集的E序来说Q优化真可能是牵一发而动全n的问题。比如在我的例子中,所有猜是性能瓉的地方,都没有得到预想中的改善?/p>
倒是在内存占用这个地Ҏ(gu)?j)插査I才得以柳暗花明,而且q让别的优化Ҏ(gu)体现?jin)h(hun)倹{所以如果你不像qiaojie大牛那样对x86?jin)如指掌Q还是要?fn)惯于从多方面猜,例如内存占用Q对齐或紧羃Q计强度,访存密度Qƈ行度{多个角度进行设惛_ƈ用实践去验证。尽可能会(x)遇到很多挫折Q但是,只要是直觉上有优化的余地Q一般都可以扑ֈ合适的Ҏ(gu)?/p> 谨以此文Q纪忉|在五q中作出来的各种傻逼决定? SALVIA出现的原因其实很可笑?7q底的时候我正在写一paperQ讲GP-GPU的。那个时候还没有CUDA一cȝ东西Q一切都要靠Shader来。本来我手上的显卡是一?550的SDRAM的简版。但是论文快l束的时候,H然q卡的风扇就|工?jin)。然后我降频用了(jin)大概一个多月,卡也废掉?jin)。因为没׃新显卡,我就打算写一个比D3D REF快的软g渲染器?/p> 07q底的时候,实现?jin)第一版的SALVIAQ当时还叫SoftArt。第一版的SALVIA其实q算不错Q流水线的完整程度到现在都还没超q,包括Cpp的Vertex Shader和Pixel Shader、纹理采栗光照什么的一应俱全。在开发过E中Q主要参考GL 2.0的SpecificationQ也阅读?jin)一些同cd软g的代码,例如Muli3D和Mesa?/p> 一些对线臛_重要的概念,例如透视修正、固定管U上U理采样的LoD Level、Clip都是借助于Spec和这些实现徏立的?/p> 如果是固定管U的话,那么SALVIA做到q些Ҏ(gu)也p够了(jin)。但是从SALVIA一开始,我就希望让它成ؓ(f)一个Pure Shader的管U,固定线的那些状态实在太?ch)h?jin)。本来Cpp实现的Shading language能满绝大部分的需要了(jin)Q但是有一个特性彻底难倒了(jin)我:(x)Pixel Shader的差分函数ddx/ddy?/p> q个东西的工作原理是q样的:(x) 比方说我有一Dshader函数Q?/p> 在Pixel Shaderq行的时候,它一ơ性执?x2的一个小块,所有的指o(h)对于整个块内都是同步执行的。遇到ddx(x)后,四个像素都正好执行到q里Q然后把x方向上的盔R两个像素的局部变量x求个差,可以得出ddx?jin)?/p>
q个要求在C++中很隑֮现?/p>
其实如果要较真,当然q是有办法的Q?/p>
但是q些实现Q要么因为切换上下文而变得奇慢无比;要么是完全没有q_UL性。想来想去,q是要让代码按照g的方式SIMD执行?/p>
所以我最l横下一条心(j)Q要为它做Shading Language Compiler。然后开始了(jin)漫长的Compiler开发。后来我看团镉K个《O无止境的八月》的时候,直就是对着镜子照自q傻逹{所以我才更黑团ѝ?/p>
08q到09q我都在外面实习(fn)Q一周上六天班,一天得q上十个多小时。从2008q初?月䆾Q我都一直在看编译原理和成熟的语法库。底子薄Q看h很吃力。到?月䆾开始设计Shader的EBNF。设计语aQ不外乎是三个方面:(x)应用场景、语法和库的支持。尽有现成的HLSL和GLSL作参考,但对于我?开始设计语a来说Q这些语a的语法和语义都过于复杂了(jin)。我需要让语言Ҏ(gu)慢慢的dq来?/p>
考虑到HLSL和C比较接近QC的文法参考资料又很多Q于是我选择?jin)从C开始裁剪语法。但是文法这个东西,q不单单是树(wi)状的l构Q树(wi)上的M一个语法节点,都可能会(x)引用到其它的文法规则。因此修改了(jin)一条规则后Q你?x)发现它可能会(x)和其它规则冲突了(jin),二义了(jin)。于是裁剪计划完蛋了(jin)?/p>
当然Q如果我现在来设计语法,肯定?x)和陈汉子一P直接从Use Casep把EBNF写出来,再稍微规范一下,一门不那么复杂的语a成?jin)。当然像C++q种变态语aQ这样做是做不出来的。但当时我显然不具备那样的能力。从七月份开始就磕l绊地裁剪了(jin)一些语法特性之后的语言Q到?jin)八月䆾才出了(jin)个千疮癑֭的方案?/p>
作ؓ(f)完全不懂~译器的矬货Q设计语a一定要和编译器的开发放在一h能有Ҏ(gu)莗我用过Flex/BisonQ用qANTLR。但是当时我对编译器特别的陌生,l织Build的能力也比较弱,因此它们在用上J琐和难于调试给我带来了(jin)很大的困扰。不q那时我Ҏ(gu)ѝ元~程和Boost已l相当熟(zhn)了(jin)Q无论是开发、阅M码还是Debug都能L应付Q所以我挑了(jin)半天Q选了(jin)Boost.Spirit?/p>
Boost.Spirit是个很奇葩的东西。它惛_C++里面提供一个类gEBNF、可以定义语法分析规则的方言。要让C++看v来像一个方aQ当然是要用神出鬼没的操作W重载。当?dng)即便是修饰后的语法,看v来也q是?x)有?gu)怪的。EBNF中的规则 在Cpp中最单可以表C成 虽然看v来有点丑陋,但是它已l完全满一个DSL的要求了(jin)Q直观的面向解决Ҏ(gu)?/p>
不过如果牉|到实现细节,在C++里面要写一个又单、又可用Parser GeneratorQ那几乎是不可能完成的Q务。v码对于Combinator-based Parser来说Q它够简单,但是没有CPS的支持会(x)令错误恢复这一cȝ周遭设计变得极ؓ(f)可怕;如果Rule只是grammar definitionQ不牉|CQ何Parser的构造,那解析这个definition的复杂度和调试难度又不亚于ANTLR或者Yaccq样有单独脚本的工具。所以这工作,q是交给Haskellq样的语a来完成吧?/p>
通过使用Spirit、设计编译器、折腾文法,让我对Compiler和Cpp的理解都递进?jin)一大步。再加上08q全q都在做GUI相关的东西,也让我对~译器的理解有所加深?/p>
09q下半年我一直都比较动荡Q不q到q底ȝ是安定了(jin)下来?/p>
09q?2月䆾的时候,Boost升?jin),Spirit也到?jin)V2。到?月䆾Q我费了(jin)点功夫,把V2的Spirit折腾到SALVIA的前端上。Parser也有所变化Q前一版的Parserq比较草率,q一版的Parser我几乎是完全按照Spirit的Demo中的Ҏ(gu)q行的。此时我也开始尝试着撰写语义分析。怎么做函数重载都是在那个时候开始点的技能树(wi)Q虽然在现在看来都是歪的。ؓ(f)?jin)执行生成的代码Q我设计?jin)半个虚拟机Q然后还准备写点教程。但是我思前惛_Q对于Shaderq样一U钟要调?0Mơ的函数Q无论如何虚拟机都是不合适的?/p>
所以我开始筹备自q后端。要求就是一个字Q快。那个时候,陈汉子正在学怎么写x86的JIT。但是我的语a到x86有很长的路要走。怎么d配寄存器Q怎么把类型{换到x86的NativeQ怎么选择指o(h)Q我都是一知半解的。凭我当时的知识Q这一定是不可能完成的?/p>
于是在阅dIntel Architecture手册和优化指南后Q我军_L一个合用的后端。考虑q很多可选的办法Q例如生成C++的Code然后~译成DLLQ用Tiny CQTCCQ;或者是JIT。但是它们缺炚w是很明显的。编译成DLL必须要自p剪一个GCC出来QTiny C的效率ƈ不是很好QJIT很复杂(L(fng)在那个时候是q样Q。不q?月䆾的时候,敏敏q是谁指点了(jin)我一下,说你可以ȝ看LLVM。然后我M看,牛|是我要的东西!然后我就开始学LLVM。LLVM的IR很好学,一个下午就搞了(jin)个Hello world?/p>
q个时候,minmin也在SALVIA上实C(jin)Half-Space的光栅化法?/p>
那个时候我t躇满志Q意气风发,三月赶英Q五月超?/p>
可没想着p么掉坑里面去?jin)?/p>
M大h真是,五个字就概括?jin)?010q一q的努力?/p>
所以整个一q中QSALVIA的开发就是写写停停,停停写写。可以说08q初的锐气,已经消磨的差不多?jin)。到?月䆾的时候,我毕业了(jin)Q新工作也基本上定和熟(zhn)了(jin)Q我和minmin_(d)从现在开始我写半q报吧,讲述一下半q来的进展。于是便有了(jin)W一项目简报?/p>
也正是从那个时候,我决定要把SALVIA作ؓ(f)一Ƒ֮验品来对待,用上所有我不会(x)的或者新学的东西。单元测试,CMake工具链,为Shader设计的PipelineQ语义分析和后端的原型都在那一q加入了(jin)SALVIA。虽然从实现上它们已l与现在相距甚远Q但是v码一切都q是往好的方向发展?/p>
另外Q?8q到09q期间在实习(fn)的时候积累的教训开始慢慢的酝酿和发酵,敏捷也逐渐成ؓ(f)?jin)我开发过E中的主要指南?/p>
基本上,那个时候积累了(jin)很多必要的经验和教训。当然绝大多数是教训?/p>
?1q的春节期间Q我l于无法忍受Spirit的麻?ch)?jin)Q?/p>
要知道,以上q些q是应用?jin)Spirit指南中的~译速度优化Ҏ(gu)之后的结果?/p>
q一切原因,都是因ؓ(f)Boost.Spirit对于Parser TreeQ是用了(jin)完全?rn)态的分析?wi)结构。每条规则的q回值都?x)是完全不同的类型。这直接Dcd数量极ؓ(f)庞大Q代码膨胀的厉実?/p>
于是11q的寒假我花?天的旉重新山寨?jin)一个文法分析器的生器Qƈ做到DSL几乎完全和Spirit一致。只不过Parser Tree不再是静(rn)态类型;模板的用量也减轻?jin)很多?/p>
C(jin)四月份的时候,Shading Language Semantic/System Value已经在语法上支持?jin),语义上也能分析出哪些变量是System ValueQ哪些变量是Uniform的。ƈ且通过生成Ҏ(gu)的函数签名,Shader满?jin)以下几个需求:(x) C(jin)11q?月䆾的时候,l于把Shader全线贯通。虽然很多Operator和Instrinsicq(sh)支持Q但是v码有?jin)个可以看的Demo?/p>
LLVM用上?jin);VS完整?jin),PS也有?jin)个雏ŞQ预处理器什么的都有?jin)?/p>
Unit Test也有?jin)原型。我为每个Stage都做?jin)Unit testQParserQSemanticQCodeGen和JIT?/p>
某种意义上来_(d)q几个月来在后端上顺利进展,让我多少有点得意忘Ş。再加上梁ȝ帮助QSoftArtq个名字Ҏ(gu)SALVIAQLOGO也有?jin),我在部门内部做的一些Introduction也帮助我梳理?jin)思\。于是从4月䆾开始,我就{备着要把SALVIA正式发布出去?/p>
11q??PSALVIA Milestone 1.0 发布。有Change LogQ有Binary DemoQ有Snapshot?/p>
三周后,发布?jin)第一个有Vertex Shader的Demo 在Milestone 1.0发布后,我开始做Pixel Shader的特性。本以ؓ(f)半年之内p搞定Q发?.0扬眉吐气一下。但是实践证明,我真是他妈的太盲目乐观了(jin)?/p>
我先来说一说Pixel Shader的特点和需求。比方说我有四个pixelQ每个pixel都是一个float?/p> 然后我要计算一下,q个data加上1.0之后是多。我前面说过Q我要让指o(h)看v来是四个像素同一时刻执行的,那么昄我生成的代码׃(x)cM于这P(x) 昄q里是可以优化的Q将四条指o(h)q作一条SIMD指o(h)?/p>
那么q个时候,有两个需求是要满的Q?/p>
只有一个域当然好办。如果struct很复杂呢Q比方说下面q样Q?/p> 那就?x)衍生出各种问题Q?/p>
每个Ҏ(gu)都一定能完成Q每个方案都有明昄~陷。最初我是想试四个像素完全独立的办法,q样实现h最方便。但是出于对性能的追求,我又惛_展^的。展q的Ҏ(gu)做到一半,发现太复杂了(jin)?/p>
此外Q还有几个非怸重的问题Q发生在LLVM上?/p>
一个是ABI。一个符合C Calling Convention的LLVM函数Q它对堆栈的理解与VS完全不同Q特别是参数传入或者返回Struct的时候。这P直接用LLVM的函数Export出来后,让VC去Call它就一定会(x)p|。ؓ(f)?jin)解军_Q我׃(jin)q两周的旉Q设计了(jin)一个ProxyQ让函数避免用Struct来传递,一切数据,除了(jin)和寄存器同样大小的float和intQ其余数据都通过指针来做。同Ӟ我需要将一些函数注入到LLVM中,比方说纹理采P此时ABI同样是个?zhn)。ؓ(f)?jin)让Code Gen正确的识别函数是LLVM的调用协议还是我自己定制的调用协议,q生正的代码。我做了(jin)各种奇葩和傻逼的Ҏ(gu)。有一些方案被废弃?jin),但是主要的IdeaQ仍然沿用到现在?/p>
一个是临时变量Q包括SpillerQ的寚w。在Linux/GCC上,栈顶和栈基指针一定是16字节寚w的。如果编译器需要分配一个(f)时变量,那么它只要通过ESP - 0x10*np获得一个对齐的地址。但是在VC中,x86下完全没有这L(fng)限制Q除非函C使用?jin)__m128Q这个时候在q入Frame之后?x)有一个SUB/AND的指令把栈顶搞到16字节寚w。)(j)。但LLVM生成的所有代码,又是ZGCC的假设。SALVIA生成的局部变量,q可以控制地址Q但是对于编译器临时生成的变量来_(d)完全不可控?jin)。在3.1之后因ؓ(f)引入?jin)AVXQ需?2字节寚wQ这个问题就更加变本加厉?jin)。在x86上,我还可以通过嵌入汇编Q来强制调整栈。但是在x64上,又启动了(jin)AVX的情况下Q我彻底没有办法了(jin)。这个问题(sh)直gl到现在Q如果我不动手去Debug LLVM的话Q就只能{他们什么时候想h修复q个问题?sh)(jin)?/p>
Pixel Shader的执行模型是SIMD的,q要求每个像素上同一时刻都执行相同的指o(h)。如果没有分支,那自然是单无比。一旦有?jin)分支就打破了(jin)这个约定。在DX9.0b?qing)之前,q当然没问题?/p>
但是Shader Model 3.0正式支持Dynamic Branch开始,q个问题凸现出来了(jin)Q分支要怎么处理Q?/p>
对于Pixel Shader来说Q会(x)面(f)三种分支Q静(rn)态分支,准静(rn)态分支(q个名字是我瞎v的)(j)和动态分支?/p> 我们来分情况讨论一下:(x) 实际上,我们真正要解决的Q就是动态分支?/p>
对于SIMD模型来说Q动态分支有三种处理办法?/p>
对于SAVLIA来说Q蟩转执行和写掩码是两个可能的选择。因为写掩码的代码生成v来更加轻松一些,所以目前的SALVIA的实现是写掩码的。在x86/x64q_上,对于AVX以上的指令,q可以用blend。但是对于其他指令而言Q基本上只能是通过跌{实现写掩码。所以这部分的开销其实很大。等到造出?jin)自qSSA之后Q再来考虑分支执行的事情吧?/p>
对于写掩码的掩码要怎么计算Q一开始我?j)里挺没q。特别是有了(jin)QContinue和Break之后Q情况就?x)变得复杂v来。一开始我没法信自己的方案是正确的。后来看?jin)MESA的Gallinum以后Q看见了(jin)Continue Mask和Break Mask两个变量Q瞬间就明白?jin)?/p>
具体怎么思考的不多说了(jin)Q这里写下几个结论:(x) 管遇到?jin)各U难处,但是很多Ҏ(gu)q是利的做出来?jin)。方案和Ҏ(gu)之间差异很大Q要想顺利移植,必须要有Test?/p>
之前也说q,一开始我的Test是按照ParserQSemanticQCode GenQJIT分开做的。但是呢Q这样一来,不同Stage之间的Test复用性非帔R。而且因ؓ(f)Stagel常变化Q包括Stage的接口。这时候Test完蛋了(jin)。Test本n也很枯燥Q变量名都不好vQ,所以Test重写h难过的要歅R?/p>
于是我重新审视了(jin)一下需求。发现我最l只兛_(j)JIT~译出来的函数的q行l果Q其实ƈ不关?j)中间的q程。而且随着我对~译q程理解的逐步变化QCompiler Stages几乎每隔两个月就要进行比较大的修正。测试的量稍微大一点,没有办法维护Test Case?jin)。ƈ且,对于单条语句或者非常短的函数来_(d)从词法到最lJIT出来的函数所覆盖的编译器代码非常之少Q可?-4个函敎ͼ代码出来了(jin)。即便有问题Q对比过ȝ版本Lp分析出来。再加上大量的AssertionQ诊断v来更加容易?/p>
因此Q在q几个月中我完全重写?jin)Test CaseQ让JIT的测试粒度更低,试更丰富;取消所有的中间Level的测试。新的测试回归v来非常容易,Z(jin)问题?sh)很好找到。在Test Case写完后,正好看到Martin Fowler喯度TDD的问题,真是感同w受?/p>
试需要吗Q当焉要。但是选择合适的LevelQ做合适的试是非帔R要的。结合之前实?fn)的时候的Unit Testl验Q有以下几点感受Q?/p>
在完成了(jin)Test的改造后Q终于有?jin)一个合适的发布前评估。所以到?1q?1月后Q发布的速度明昑֏快了(jin)许多。快速的发布对于做一个长期项目来说非帔R要。这也和敏捷的想法不谋而合。不是从品质控制上、还是进度追t上Q或者是说对开发者自信心(j)的增强,都需要有短^快的开发周期?1q也正好是Autodesk推行敏捷的一q。同事里面有很多的h反应说敏捷会(x)D软g品质的下降,短期目标?x)导致过于追逐眼前利益?/p>
但是从我的经验来看,对于个hQ敏捯短^快。但对于团队Q敏捯从长计议。不是所有的iteration都需要开发新Ҏ(gu),必须要保留够的iteration来完成重构、整理、设计方案的反省和讨论。对于以qؓ(f)单位的长周期产品来说Q可以每个季度有3-5天的旉Q每个h都提出对框架的改q计划;每年有两周的旉Q完成框架的重构和修正。更的重构Q可以安排的更加短小的时间?/p>
?1q?月䆾开始到现在Q就一直在做Demo、优化、特性的完善Q以?qing)一些新Ҏ(gu)的思考?/p>
ȝ来说Q这一q半的时间里面,很多工作已经不像早先几年做的那么吃力Q但是仍然在很多的点上有所斩获?/p>
?012q底为SALVIAq行?jin)局部的重新设计Q也是“学”与“习(fn)”的C轮“习(fn)”。新的SSA?qing)Shader优化、JIT化的线、对性能有要求的新前端、瞄准DX11以上Shader Model Features、JIT的调试符Pq些一定会(x)l我带来许多l尽脑汁想不明白的问题,但同时我也会(x)学习(fn)到、实践到许多新的知识?/p>
我相信时间会(x)教给我们一切?/p> 出错处理和错误提C,是编译器开发过E中重要而繁琐的部分?/p> 诊断信息的格式因~译器和IDE而不同?/p> SALVIA采用Visual Studio的格式,?文g + 行列 + cdQ等U)(j) + ~号 + 出错信息。例如:(x) d:\programming\salvia\sasl\test\cgllvm_test\function_test_basic.cpp(16): error C2061: syntax error : identifier 'te' 因此在出错分析的时候,也需要提供如上的一些信息?/p> 在以上信息中Q文件名和行列号可以在词法分析的时候获得,我们它作ؓ(f)属性附加在Token中?/p> cd和编P对于同一个编译器而言是相对固定的Q尽我们可以用ID来表C,但是它ƈ不直观,~译器检查也较少。与参数匚wӞ也比较容易出错?/p> SASL中的诊断信息每个错误都使用一个类型来表达Q?/p> q样的好处在于可以用Combinator的风格来撰写错误信息。例如这P(x) diagnostic_chat.report<unrecognized_identifier>().token( err_tok );
q且׃~译器的保证也比较不Ҏ(gu)写错?
但是q种写法也有一个很关键的问题,需要ؓ(f)每个错误都定义一个类Q工作量很大。SASL对这一问题的处理,自然是传l的大杀器:(x)q用脚本q行生成?
Clang使用?jin)它内置的代码生成工具td来完成生成的工作?
3. 诊断信息理器Diagnostic Chat
Chat是诊断信息的理工具。它主要要完成以下需求:(x)d和清理诊断信息,以及(qing)在诊断信息的d清理时提供回调操作?
后者是很有用的Q尤其是在调试编译器的时候。你得分清楚I竟是真正的E序错误呢,q是~译器出?jin)错?
Diagnostic Chat的原型如下:(x) 同时Q我们也Treat Warning As ErrorQError CountQDisable WarningQStop compiling when error occurs{状态和功能所需要的支持d到Chat中?
所以,Chat除了(jin)提供理之外Q也要具有相应的诊断信息的统计功能?
4. qo(h)器Diagnostic Filter
Filter主要配合IDE使用Q从Chat中取出符合条件的诊断信息。Error Count和Disable Warnings{功能也可以通过它来完成?
5. Formatter
Formatter用于DiagnosticItems中的信息转换成h可读的字W串。目前SASL只打支持Visual Studio的格式,但是怿支持GCC的格式以更好的和Eclipse{第三方IDE集成q不困难?
在C#里面Q我们可以用“We need ‘{0}?not ‘{1}?”这L(fng)方式来分description templateqg期的产生格式化的字符丌Ӏ但是在C++中,q种做法q不Ҏ(gu)。C的sprintf很难h延期、渐增的l定模板的特定,对自定义cd的字W串化的支持也不Icd安全也比较差Q而stream的话Q也?x)面临着好端端的格式化字符串割裂的问题。SASL使用?jin)boost.formatQ从一定程度上搞定?jin)这两个问题Q从而像C#一P使用格式化字W串的功能?/p> q是?010q?月䆾以来Q?SALVIA Milestone 1.0之后最重要的发布! 0.3开始,SALVIA正式支持?jin)Pixel Shader?/p> ?008q中开始的Shader设计与实现工作基本完成?/p> 未来一q内QSALVIA的工作将集中以下几点Q?/p> 随着SALVIA整体的逐渐成熟Q我们也希望有其他的朋友能来参与和支持这个项目,一同进步?/p> 如果(zhn)希望参与到q个目中,请mail联系我:(x)wuye9036 __at__ gmail dota com.
]]>
背景
插值算?/h1>
插值算法的选择
struct transformed_vertex { float4 attributes[MAX_ATTRIBUTE_COUNT]; };
transformed_vertex projected_corner;
// 计算角点的坐?
projected_scanline_start = projected_v0 + projected_ddx * offset_x + projected_ddy * offset_y;
// 像素的透视修正?
float inv_w;
// 最l输出的4x4个像?
pixel_input px_in[4][4];
for(int i = 0; i < 4; ++i)
{
projected_pixel = projected_scanline_start;
for(int j = 0; j < 4; ++j)
{
// 透视I间转换到线性空间ƈ输出到px_in?
px_in[i][j] = unproject( projected_pixel );
// 累加x方向上的|透视I间Q?
projected_pixel += projected_ddx;
}
// 累加y方向上的|透视I间Q?
projected_scanline_start += projected_ddy;
}
本轮优化之前Ҏ(gu)值算法的优化试
template <int N>
void sub_n(out, v0, v1 )
{
for(int i = 0; i < N; ++i) {
out.attributes[i] = v0.attributes[i] ?v1.attributes[i];
}
}
首轮优化Qunproject函数Qoperator += ?operator =
二轮优化Q插值算法的调整
三轮优化Q减内存占用,x花明
四轮优化Q精度敏感性下降的额外U利
l论
1. 2007q??- 2007q?2月:(x)可笑的动机,可笑的雏?/h1>
动机与原?/h2>
Z么要有Shader Compiler
float shading_pixel( ... ): COLOR0
{
float x;
// Expression for calculating x
return ddx(x);
}
2. 2008q初 - 2009q?2月:(x)黎明前的黑暗
Shader的文?/h2>
:(x)Boost.Spirit
Rule ::= Token SubRule0 [OptionalSubRule1]
rule = token >> subrule0 >> optional(OptionalSubRule1)
3. 2009q?2月?010q?月:(x)长征的开?/h1>
后端与前?/h2>
4. 2010q?月?011q新q_(d)(x)苦难的行?/h1>
苦难Q复杂的问题
行军Q些微的q展
5. 2011q?月?011q?月:(x)新Shader的v?/h1>
坑神QBoost.Spirit的灭?/h2>
Shader的阶D|成?/h2>
W一个版本与发布前的完善工作
6. 2011q?月?012q?月:(x)坂道?.0
Pixel ShaderQ需求与设计
struct pixel_input
{
float data;
};
pixel_input pixel_block[4];
struct pixel_input
{
float data;
};
struct pixel_output
{
float data;
};
void shading_pixel(pixel_input* in_data, pixel_output* out_data)
{
// TMP = IN_DATA.DATA + 1.0
float tmp0 = in_data[0].data + 1.0;
float tmp1 = in_data[1].data + 1.0;
float tmp2 = in_data[2].data + 1.0;
float tmp3 = in_data[3].data + 1.0;
// OUT_DATA.DATA = TMP
out_data[0].data = tmp0;
out_data[1].data = tmp1;
out_data[2].data = tmp2;
out_data[3].data = tmp3;
}
Pixel ShaderQ优化与问题
struct
{
float;
float2;
int3;
struct
{
float2[3];
float;
};
};
坑神IIQLLVM
SIMD执行模型下分支的处理
float branches( uniform float udata, float vdata: POSITION): COLOR0
{
const float zero = 0.0;
if(zero < 1.0)
{
// Static branch
}
if(udata)
{
// Semi-Static Branch (我自己造的)
}
if(vdata)
{
// Dynamic Branch
}
}
坂道のTest
坡长路远Q小步快?/h2>
6. 2012q?月及(qing)以后Q现在与未来
新特性,新思?/h2>
2. 诊断信息Diagnostic Itemclass diagnostic_item
{
};
class unrecognized_identifier: public diagnostic_item
{
public:
unrecognized_identifier& token( token_t tok );
private:
static int level;
static int id;
static std::string description_template;
private:
std::string ident;
size_t row, col;
// Other properties
};
class diagnostic_chat
{
public:
template <typename T> T& report();
void add_report_diagnostic_handler( DiagnosticHandlerT handler );
};
]]>
目主页Q?http://code.google.com/p/softart/
更新列表如下Q?/p>
0.2.2 - 0.2.5 (Feb 24, 2012)
目主页Q?a title="http://code.google.com/p/softart/" >http://code.google.com/p/softart/
新的~译步骤如下Q?/p>
1. 下蝲最新的CMakeq安装?/p>
2. 下蝲Python 2.7q安装?/p>
3. 下蝲boost 1.44或更新的版本Q解压到某个目录下?/p>
4. Clone或下载SALVIA代码包,执行根目录下的build_all.py文g。第一ơ运行的时候会(x)生成一个project.pyQ编辑project.py讄相应属性,包括boost代码目录Q编译器Qconfiguration{?/p>
5. 再次q行build_all.pyQ编译程序?/p>
本文讨论如何在LLVM上生成正的内存寚w和调用协议的代码?/p>
在这里ؓ(f)?jin)方便和标准赯Q假定应用LLVM的语a的Extending和Embedding的对象都是C?/p>
调用协议
先来讨论调用协议。调用协议用于保证调用方和被调用方在二进?汇编一U上是相容的。合适的调用协议可以帮助构造出以下代码Q?/p>
一般来说调用协议包括参C递和q回g递和堆栈q三个部分。在x86q_上的C/C++~译器中常见的调用协议有cdecl, fastcall和stdcall。具体的协议内容请参见MSDN?/p>
在C++中还有一cȝD的调用协议thiscallQ用于调用对象的成员函数。但是这一c调用协议不同的q_Q不同的~译器实现皆有不同,既无书面标准Q也无事实标准,再加上virtual call{复杂的情况存在Qƈ不适合用于做跨语言的调用?/p>
对于x64q_而言Q在windows下和linux下分别有两种调用协议?/p>
先来看x86。由于x86在cdecl和fastcall上是有着跨^台的标准的,因此LLVM对它的支持是比较完整的。程序只要在创徏Function的时候指定Call Convention卛_?/p>
但是对于x64QLLVM的支持便不是那么完善。以windowsZQwindows的x64调用协议要求以rcxQrdxQr8Qr9寄存器传递前四个不大?4bit的参敎ͼ其余参数攑֜栈上。如果参数大?4bitQ则要求传递它的指针。Q点用xmm0-3来传递。但是对于LLVM而言Q一旦参数大?4bitQ它便会(x)整个对象而不是指针压到栈上传递。因此在遇到x64Ӟ需要小?j)处理API部分的调用协议?/p>
在这里,我们需要将所有超q?4bit的结构体处理成指针(或者拷贝后处理成指针)(j)传递?/p>
同时QLLVM提供?jin)readonly和byval两个参数属性(AttributeQ来保参数的D义。前者意味着传入的指针所指向的值是不被修改的,Q类gT const*Q,而后者会(x)对传入的指针做一份内存拷贝,保写g被传递出函数Q类g值拷贝)(j)。这PLLVM生成的函C可以MSVC生成的x64代码正确调用?jin)?/p>
内存寚w
与移动^台的体系l构相比Qx86对内存对齐的条g是相当宽松的了(jin)。大部分的指令对内存寚w基本上是没有Ҏ(gu)要求的。只有一些SIMD的指令会(x)对内存对齐有所限定Q例如movaps?/p>
Z(jin)方便后端生成SIMD代码QLLVM提供?jin)vectorcdQ例如vector<float, 1>。在代码生成的时候,vector?x)编译成最有可能的SIMDcd。因此在x86q_上,vector<float, 1-4>都被处理成类g__m128的类型,更长的vector则被拆分成多个__m128cd?/p>
q实际上意味着Q所有的vector都应该遵?6Bytes寚w的原则?/p>
考虑到我们的需求,cM于struct{ float[3]; }q样的结构,如果能表CZؓ(f)vector<float, 3>昄适合一些数学运,例如shuffleQ逐元素的addQsubQmulQ同时LLVM指o(h)的选择也更加灵zR但是显?dng)q个l构体有两个条g是不满的:(x)16字节寚w?6字节的大(movups和movaps都是一ơ取16字节Q。这?x)造成边界下读写的内存界。因此非常可惜,q些数据必须表示为struct{ float ,float, float }。在d的时候,也会(x)生成正确的指令:(x)movss?/p>
那么Q对于一般的非对齐的vec4应用vector<float,4>行不行呢Q?/p>
{案是,很困难。对于LLVM而言Q他们在设计的时候就没有q多的考虑vector在非寚w时候的应用。尽load和store都能够指定alignment以生成非寚w的内存操作(例如movupsQƈ且确实会(x)hQ但是由于代码优化、(f)时存取等Ҏ(gu)的存在Q导致一些非load和store的内存操作仍然是要求寚w的(例如生成?jin)addaps xmm, [addr]Q。此时仍然有可能为非寚w的数据生成了(jin)内存寚w的指令?/p>
因此l合权衡QSASL在API界面上用了(jin)struct{float x,y,z,w;} q样的ABI来表C数据,在代码生成时Q会(x)首先struct的数据{换成vectorQ然后再执行其它的操作,兼顾ABI与SIMDQ同时对于IntrinsicQ由于ƈ不暴露给HostQ所以它们仍然尽可能使用VectorQ便于LLVMq行优化?/p>
源码下蝲地址Q?/font>
Demo截图Q?/strong>