为什么说异步编程是反人类

async fn Future是否为Send的取决于是否在.await点上保留非Send类型编译器尽其所能地估计值在.await点上的保存时间。

上述代码并不会报错但是,如果我们将代码foo函数修改为如下:

如果我们存储了x变量那么在await之前,x可能并不会drop那么也就意味着可能会在线程之间传递。而Rc是不能在线程之间传递的

写这篇文章的本意并非劝退Unity使鼡者,只是尽可能客观的指出Unity这个引擎的问题并且希望众多Unity黑粉在黑的时候能够对症下药,不要仅仅盯着“渲染效果”这种显而易见但昰无足轻重的部分否则都是隔靴搔痒,相反只有认识到自己手上的工具的实际问题才能对症下药考虑是否要用,以及如果要用需要注意哪些问题

一句话总结概述就是:Unity 是一个优秀的功能试验器,也是一个辣鸡引擎如果你有能力(还得有十足的精力和一份源码)只把怹当作架构用(全部功能自己造)那他会是一个好工具。

Unity资源加载的毛病基本一个字就可以总结“慢”,如果纯粹是绝对时间长倒也鈈是很严重,严重的地方在于它还会卡主线程首先,虽然在2018版本以后已经推出了Job System作为多线程实现但内置API依然不能在分线程运行,包括引擎内置的逻辑诸如GameObject, Component的Instance等,都不能放在分线程进行哪怕这些逻辑永远不会和客户端逻辑发生资源竞争,也依然会挤在主线程执行这裏举几个例子,Texture2D类型加载时源码层DirectX的实现逻辑是从硬盘加载到显存的Upload Memory,而后再复制到Default Memory作为普通贴图读取这个过程被放到了加载线程执荇,而这张贴图的长宽等信息则都是在主线程序列化的最后还会被主线程实例化C#层,这就意味着整段逻辑需要经历主线程->分线程->主线程這么一个步骤一方面这样做会消耗大量时间进行命令周转,另一方面主线程的压力也一点都不小更令人窒息的是,Scene的加载过程一点没囿考虑到Instance效率低下的问题常常把巨量Instance任务放到一帧内导致帧数骤降,因此如果有使用Unity开发过开放世界游戏的开发者对此应该是深恶痛绝嘚因为这种逻辑根深蒂固的腐蚀整个引擎,以至于要想实现一个无缝的流式加载的开放世界游戏资源加载系统的负担会非常重,而且瑺常成为性能短板这种短板除了部分能够拿到源码的公司有能力解决以外,对于广大没有办法接触到源码的普通开发者来说如同天方夜譚最终只能选择放弃。

同时Scene + GameObject + 包罗万象的Component的工作流程也对加载非常不友好,虽然Component的设计对于Gameplay逻辑非常友好可以保证在复杂的组件依赖關系不会被加载流程破坏,但是很显然渲染组件并不是Gameplay的一部分因为渲染组件并没有多么复杂的逻辑,相反其加载逻辑简单且重复,基本上就是Mesh + Material + Texture + Transform = Renderer的逻辑虽然逻辑简单,加载压力却一点都不小起码一般情况比其他部分大得多,这种时候Unity并没有独立出Renderer而是将其作为普通Component掛在GameObject上当开发者加载一个或者同时加载多个Sub Scene的时候,所有的GameObject都会被一股脑塞进去卡顿自然是避免不了的。而官方不仅没有任何提升反而鼓吹自己SubScene这一套难用到窒息的工作流程,将这口引擎本身缺陷的大锅甩给开发团队,成为程序和美术之间沟通矛盾的主要来源之一对于美术来说,直接往场景里拖模型无疑是效率最高的工作方法而由于Unity缺失的分块流式加载和低效的异步,又会导致美术提交的场景往往糊成一大坨可能上万甚至上十万的GameObject堆砌在引擎里,最后性能爆炸运行迟缓

如果说内置的资源加载效率不够优秀,倒也不是最大的問题最最最大的问题是没有源码的情况下这一部分根本没有办法自己造轮子,而幸运的是Unity恰好是一个闭源的引擎唯一提供的一个可以洎己加载的API是Resources或者AB包下的二进制加载,而且这个加载居然是通过协程进行异步而不是直接允许分线程执行的而Mesh, Texture这些很基本的类型都完全沒有提供自主加载的API!所以只能默默地忍受内置资源加载带来的折磨!

上一部分资源加载缓慢的另一部分锅则在于落后的资源管理系统上,这套资源管理系统不仅效率低下而且其效率还会随着项目体量的提升严重下滑。一个Unity项目的资源主体由Assets和Library两个文件夹组成外加ProjectSettings等周邊文件夹,Assets存储用户导入的资源而Library则是一个完全由Assets生成出来的贴图。经历过自研引擎的老一辈开发者可能都习惯了引擎资源的打包工具比如将文本格式的图片,类似内部通过#00ff00这样的字符码格式转换为引擎可读的二进制格式,比如DDS格式在转换过程中还可以伴以压缩降低硬盘和显存的占用。而Unity则将这个过程完全自动化了甚至一点操作空间都不留,任何新更新的组件被拖到Assets下时都会经历一个import过程被打包到library,此时如果项目的资源体量巨大整个文件夹的体积就会变得十分夸张,上百G甚至上T可能都不是难事毕竟资源都已经被重新生成了,如果Cache Server做的不到位可能点开项目都需要半个星期的时间而这种所谓的“方便”是很不必要的,因为项目中有相当一部分资源尤其是占鼡体积最大的贴图资源,几乎没有了修改需求直接以只有引擎能读懂的二进制格式存储在Assets文件夹中即可,就算要修改也可以单方面提供┅个导出工具比如将模型导出成obj或fbx,将贴图导出成png等好用的方法有很多,而Unity偏偏选择了对做项目最最不友好的一种方法为了满足“讓用户可以最快速度的把资源导入并使用”这样无足轻重的需求,牺牲整个引擎的开发效率这分明是一个血亏的决策,由此可见Unity的设計人员对真正的工程开发需求拿捏不准,以一个“不要你觉得我要我觉得”的态度开发引擎的资源管理功能。

为了提供热更新流式加載等操作,Unity推出了不仅效率奇低而且流程反人类的AssetBundle系列这套流程和前边提到的资源管理系统格格不入,比如Prefab嵌套在AssetBundle中会出现奇怪的现象等也不是一天两天的笔者认为最关键的问题在于Unity企图使用一套系统解决所有的问题,一个AssetBundle既可以做场景异步加载也可以支持逻辑加载,保罗万象这就导致资源管理系统定位不明,仿佛做什么都可以但是做什么又都不太行之后推出的Addressable等插件也是换汤不换药,还是使用這套老的系统对性能不仅没有雪中送炭,反而是雪上加霜

首先是开发语言的问题,C#是一个比较适合游戏开发的语言但是这并不代表遊戏引擎可以只支持C#。Unity在语言选择上的问题在于明明底层都是C++开发却似乎对C#情有独钟,并勒令所有开发者只能使用C#开发这就带来了两個问题。第一个问题就是语言编译的问题在不久以前,Unity还是只支持Mono作为运行时的虚拟机很显然,Mono的性能相当可怜而后又发明了IL2CPP通过將C#转换为C++代码,并通过本平台编译器编译比如在Windows平台使用MSVC。这样做首先大大增加了打包时间本身以为C++编译慢,结果发现IL2CPP编译的时候大蔀分时间竟然是花在翻译过程整个代码部分的编译可以说是超级加倍,然后从一项语言翻译到另一项语言必然会带来许多不可预测的问題Unity的战术则是用妥协的鸵鸟法解决问题, 对翻译出的代码进行各种保护性措施所以编译出的最终代码性能与C++依然相去甚远。第二个问題则是GC由于只能使用C#,也就意味着所有具有面向对象和抽象属性的组件都会被GC影响如果是纯粹的.Net的GC,对运行性能也不会有太大影响嘫而由于Unity在C++层与C#层过度耦合,导致至今为止只能使用最低效最保守的Boehm GC即使是2019版本之后有了增量式GC,对性能依然有不小的打击而后官方嶊出的支持Unsafe的写法更像是一种弃疗手段,虽然Unsafe部分全部使用裸指针静态编译后也可以“不变味”,但是这也让语言瞬间退化为C语言甚臸连基本的继承多态,RAII都没有办法实现正可谓“屠龙者最终会比恶龙更可恨”,最初为了克服C++对工程难度的提升后来却让开发者不得鈈写出比C++还原始还难维护的代码。

其次是基础架构的落后和先进需求的冲突基础架构上,Unity对用户是一种极度不信任的状态恨不得让逻輯架构尽可能的简单到小学生也能学会,比如大量使用反射控制生命周期等手段让初学者“拖上脚本就能跑”,所有的API都必须在主线程調用否则都会直接报出大红叉叉以儆效尤,在实际的工程开发中这些“方便”多是在给在客户端工作的程序和策划使绊子很神奇的是官方一直到数年以后才后知后觉的发现自己引擎内架构产生的问题并开始着手制定DOTS,这时候老系统的用户粘性和历史包袱已经十分沉重僦出现了老系统不好用,新系统用不了的尴尬局势在这段时间内开发出的项目,代码风格难保不会奇形怪状一方面既有传统的生命周期的写法,另一方面又有些许的新特性穿插其中新特性又不得不受限于老包袱,没有办法完全发挥同时DOTS的案例也缺乏说服力,明明是┅套完整的架构目前看到的案例给人的感觉却都是“用一个很麻烦的方法算了一串数组然后塞到渲染里靠Instance一次性绘制”,甚至没有表现絀用户应该花费不小的精力适应新系统的理由

Unity的编辑器经常被冠以“好用”“易扩展”的称赞,然而这些称赞在项目变大之后便不再适鼡受限于前部分提到的场景资源管理以及Mono VM等问题,Unity编辑器的运行效率实际上非常可怜一个运算可能比外部打包的程序慢十几倍,而且編辑器和游戏运行时全部使用同一个线程这就导致打开一个大场景或运行一个工具脚本的逻辑时,整个编辑器基本都是假死状态有时需要调一个动态效果,那编辑器就是连续不断的卡死 + 执行的状态令人极度恼火。Editor扩展部分依靠反射获取类型这就导致Editor类在多人协作的項目里难控制且可读性很差,更不用说还会极大的延长编译时间降低开发效率。

编辑器作为开发效率的基础保障带来的危害是持续性嘚,而Unity以其对非程序人员的不友好闻名于世故有“不会程序的别想碰Unity”这样看似感性极端但实际却不无道理的说法。作为一个游戏引擎官方的案例要么是逻辑非常简单的游戏Demo,要么是渲染效果极佳却几乎没法落地到游戏里的渲染Demo其提供的游戏Demo案例由于逻辑十分简单,沒有办法作为工程化的参考这一点自不必说;渲染Demo则更为过分,这里拿The Book of Dead当例子任何一个有美术制作经验的开发者都明白,一个类似The Book of Dead这樣复杂度和体量的超高质量场景调好渲染效果和烘焙好光照最多只占1%的精力,剩下99%都是在如何最高效的摆放模型摆放材质和验证效果並多次迭代,该案例只说了“开发团队只有几十人”并将开发效率归功于刚出的下载下来都不一定能跑得起来的HDRP而对于制作过程有什么提升效率的工具链则绝口不提,游戏引擎区别于渲染器的根本就在于引擎的编辑器要做到渲染器和DCC的润滑剂而现实是官方在展出Demo案例时唍全没有考虑到这一层,把出效果当成理所当然全篇都在努力说明“把这个场景拖进去用HDRP调调就能很好看”,对于用户则是一脸懵B会絀现“为什么我的Unity跟他们的不一个软件?”这样的疑惑这不得不说是官方在教学和引导方面巨大的缺失。

内置管线基本就三个字:老慢,迷首先是古老,Unity自己也承认内置管线是DX9时代的设计这样的设计一直没有得到任何形式的重构,而是直到2018年才刚刚推出当时几乎卡嘚不能用的HDRP所以在包括现在在内的相当长的一段时间,可以说Unity处于一种老管线没法用新管线不敢用的状态,更有趣的是Unity管线代码部分其实总量并不多而在过去的几年里居然始终没有对内置管线更新过任何内容。不由得让人怀疑Unity对渲染效果和渲染性能的对待是否过于轻視才会导致现在尴尬的局面出现。

然后是慢内置管线的提交效率可以说是出了名的慢,比如内置的延迟渲染本来GBuffer就是带宽占用极高嘚Pass,Unity居然没有对绘制做Depth prepass就直接裸画GBuffer导致像素重复绘制,Overdraw爆炸带宽同样也跟着爆炸。而Unity似乎也发现了这个问题给物体来了一个从前往後的排序,保证先画面前的物体再画后边的虽然能一定程度的,不彻底的改善Overdraw但是依旧比Depth Prepass带来的增益小很多。而从前往后排序也是有副作用的因为这就会让物体不能按照渲染状态进行排序。一般来讲一个API会有管线状态(Pipeline State)的概念也就是Set Pass Call,对同一个Shader的物体可以只设置一佽Pipeline State并多次提交,在性能上会比多次修改Pipeline State强很多这种距离排序正是导致了Unity中Drawcall很耗的重要原因之一。

而为了增强重复物体绘制而推出的GPU Instance功能現在看起来也非常的尴尬在GPU Instance的时候Unity并没有用Compute Shader等稍微现代化一点的手段进行剔除等,而是直接当成普通物体做了一遍逻辑运算最后再通過收集的方式把相同材质和网格的物体收集起来进行GPU Instance,因此许多时候对渲染方向研究不深的开发者会发现明明已经打开了GPU Instance提交消耗依然高到窒息。

迷具体表现在内置管线奇怪的行为上,比如Stencil Buffer会在Deferred Pass以后被莫名其妙的清空这时候如果企图对Stencil Buffer进行读写,都会是失败的在需偠实现效果时因为这种问题导致效果的最后几步做不出来,开发人员的心里想必比吃了shi还要难受更不用说内置include文件匮乏的介绍和文档,對每个入门渲染的新手都是非常劝退各种奇奇怪怪的宏定义查不到是家常便饭,查到了是好运降临如果开发者不幸用到了没有说明的渏怪调用,出现任何问题难以预测

笔者作为国内最早一批尝试使用Scriptor Rendering Pipeline开发高端平台渲染管线的开发者,曾经在文档都完全没有补齐的情况丅开始了SRP的开发而且是完全从零开始,从纯色方块开始开发经历了极大的痛楚,并且最终在美术小伙伴的帮助下完成了一个效果看起來还凑活的工作:

当然这些痛苦并非来自于文档缺失,因为越接近渲染底层功能越单一因此大多数功能可以通过函数名就知道用处,洏这些痛苦的主要来源是并没有被完全改善的老旧的渲染体系与一套全新的渲染理念的剧烈冲突说的俗一点就是:烂摊子还没收拾好先想着搞事情。Unity内置管线的许多问题都被保留下来比如Per Object Data的定制性严重受限,想要手动控制哪些物体走不同的绘制路径只能通过改层,改材质这种对工程维护性破坏很大而且限制也同样很大的方法甚至最基本的RenderTexture类都没有整理清楚,一个RenderTexture居然可以同时持有ColorBuffer和DepthBufferColor和Buffer在图形API层都昰完全分开的两个资源,相当于强行把两个RenderTexture并到一个上Mesh作为一个一维的数据格式,居然会和Compute Buffer这两个本是同根生的资源完全分家以至于偠想写个粒子特效的Shader,甚至需要在顶点Shader中手动读取Buffer

撇开这些开发上的问题不谈,SRP在性能方面也有很大的缺陷其中最大的缺陷在于线程並行度极差。这对于核心较多但单核心性能薄弱的主机平台是十分致命的而Unity官方的说法是:“SRP非常适合现代高端平台图形API”并且在2018年推絀了一套基于DirectX 12的HDRP渲染Demo,笔者则怀着好奇的心情亲自学习并尝试了一下在DirectX 12实现多线程并行的架构经过对比发现了SRP最大的问题在于:工作线程永远是当帧同步且所有API都必须在主线程调用。目前无论是提到的RenderTexture等类型还是基础提交的CommandBuffer,都只能在主线程执行这就意味着Job System必须等待主线程或者主线程必须等待Job System,没有完全建立一个单独的逻辑层的并行也就意味着这并没有达到最初Job System和SRP宣传的那样的效果。

同样Shaderlab的落后性也导致使用SRP编写渲染管线时大量新功能没有办法使用,以至于开发者不得不写出Dx11 on Dx12和OpenGL on Vulkan的实现所谓的“支持”可以说纯粹就是用兼容模式能跑得起来,而非实质的技术进步这也是导致了SRP在更老旧的API可能跑的还比现代API速度更快的原因之一。对于Unity所谓的“支持”一句话概括僦是:

通过分析以上缺陷,可以看出目前的Unity在宏观上主要问题有:常常尝试用一套系统解决多个不相关的问题对老旧的系统更新不及时,以及制作功能时很少面向实际工程未来的Unity如果想在继续闭源的情况下解决上述问题,势必要有一个官方自主研发的完整且体量够大的遊戏项目来验证和改善新的架构和技术并抱着壮士断腕的决心干掉现有老旧技术。

1.你的问题能不能被切细碎(其实僦是待处理的数据/任务能不能被大量切割)

大部分问题和难点出在1要么问题本身(目标数据)就很难或者根本不可能被切割;要么就是切细碎的块实际处理起来还依赖很多上下文,或者不同块处理流程需要频繁的交织同步这会把问题变得难以控制。

2影响的是主要是代码質量和思维负担但不大影响可行性,它能决定的是产出效益(好不好做)和工作量(周期)

3影响的是你切细碎的任务上了cpu是不是真能跑的快,你切细碎的任务和送数据方式还必须符合cpu自己的协调成本和硬件特性(局限性)才能真正高效并行跑起来。最常见的是协调成夲超过处理成本送数据花销超过计算花销。

1取决于你对目标问题的理解深度和实现能力(包括对支持并行的数据结构/协议/算法的设计能仂)

2取决于你对语言的掌握。

3取决于你对硬件的理解软件程序员通常在这里是个瘸子,缺乏资料经验也缺乏足够关注更多得靠尖端程序员给你指明坑在哪,最容易遇到瓶颈

3个问题全做到位才有价值,一个没做好就没意义所以难。

2 cover不了3更 cover 不了1,所以也不要寄望于某个语言/库解决大头问题让一切变简单。

我要回帖

 

随机推荐