戴森球计划吧 关注:84,478贴子:736,622

《戴森球计划》开发日志 - 新多线程框架

只看楼主收藏回复


各位工程师,大家好!高考志愿填报已经接近尾声,祝所有刚刚经历高考的工程师们都能收获一份心仪的录取通知书,开启崭新的人生篇章!跟大家汇报个好消息!在过去的几个月里,工作室的小伙伴们也正在有条不紊地推进着《戴森球计划》的后续开发工作。每一行代码、每一个新想法,都凝聚着大家的心血,希望能给大家带来更多惊喜!

(载具系统,启动!)
坏消息,CPU绷不住了
在之前的开发和维护当中,我们愈发感觉到现在的程序性能已经逼近极限。如果将来实装载具系统,游戏中可能会多出上千个需要进行物理模拟的组件,以现有的架构,这样的开销是难以承受的。
在那个没有蓝图的年代,我们曾经以为性能极限能够支持千糖工厂(每分钟产1000白糖)就够了,但是玩家的热情和创造力远远超出了我们的想象——对于部分玩家来说,万糖不过是入门门槛。虽然紧接着我们就推出了多线程系统,并且经历了几年的优化,玩家们始终热衷于释放电脑的极限,每一次优化后,大佬们就会刷新产糖记录,后来他们相继做出了百万糖甚至是十万糖的存档!看来,是时候给程序喂点“性能强心剂”了。我们对现有代码架构进行了全面排查,发现多线程系统仍有巨大的优化空间。因此,最近我们工作的重点,正是对《戴森球计划》的多线程系统进行全面优化,为载具系统的后续开发铺路。

(某个十万糖工厂存档的性能开销,整个生产线的逻辑帧开销已经达到了80ms)
《戴森球计划》中的多线程
我们先简单介绍亿点点多线程的基本概念,为什么《戴森球计划》使用多线程,以及为什么我们要重构多线程系统。
以一个制造台的生产流程为例。在不考虑吞吐物流相关逻辑的前提下,大致可以分为以下三个阶段:
1. 计算自身电力需求阶段:制造台此时可能因材料不足、产物堵塞、或在制造过程中等处在不同的状态,对电力的需求也会不同。
2. 计算电网负荷的阶段:此阶段先统计所有发电设施的供电能力,再统计耗电设施的总耗电量,最后根据二者的大小关系或比值计算出电网供应率。
3. 计算生产进度增量阶段:根据制造台所在电网的负荷判断工作效率,再结合原料、产物的存储状态及增产剂喷涂情况,计算本帧应增加的生产进度。
看着计算量似乎不大,对于大多数玩家的电脑配置来说,想要执行完一个制造台这三个阶段的计算平均只需要大约数百到数千纳秒的时间。但当你想到后期动辄成千上万、甚至几十万个制造台的存档规模就很恐怖了,如果处理器按顺序挨个执行它们的更新逻辑,就可能需要几到几十毫秒,游戏帧率直接跳水。

(这一屏幕的制造台,背后是各种优化保证它们能流畅运转)
而现实中,大部分玩家电脑上的处理器都有多个核心,不同核心可以同时进行不同的计算任务。假设玩家的CPU有8个物理核心,只要平均分配一下任务,每个核心需要处理的制造台数量就会减少,计算消耗的时间自然就降下来了。
不过,执行每个制造台逻辑所消耗的时间是不同的,再加上处理器内部不同核心性能差异、系统其它应用程序征用等原因,大家最终完成任务的时间将长短不一,而我们总是要等待最慢的一个任务结束,就注定不可能将最大帧率提升8倍。
所以接下来,魔法开启!

不开玩笑了,其实多线程还有别的问题。比如多个处理器核心同时工作时,还可能会有内存限制、数据共享、伪共享、上下文切换等问题,比如不同的线程都要访问和修改同一个数据。想要保证这一步不会出问题,就得增加一步通信机制,这一步不仅会增加额外开销,而且总有一个线程需要等待另一个线程完成访问。
除此之外,还有一些时序逻辑上的问题,还是以制造台的三个阶段为例:如果要执行第二个阶段,那必须等待第一个阶段中所有制造台的数据都更新完成才行,不然在计算电网需求功率时,拿到的可能就是上一帧的数据。于是,戴森球的多线程系统将游戏一帧的更新逻辑中计算量较为庞大的部分划分为了若干个阶段,其中某些阶段内逻辑的执行顺序对结果不会产生太大的影响。比如制造台计算自身这一帧的耗电需求这件事,其它设施的耗电需求与制造台自己的耗电需求无关,完全就可以多个线程分别负责多个制造台的计算同时进行。

旧版多线程出现了什么问题
我们的多线程系统毕竟是老东西了,执行效率只能说差强人意。并且原先的多线程系统在设计上就决定了没法安排太多种类的多线程任务,且每次执行一个多线程任务阶段都必然会有较大的同步成本。随着新内容的不断开发,每帧需要执行的逻辑逐渐增多。贸然将某个逻辑转为多线程,不仅性能上未必有足够大的提升,还会大大增加代码的维护难度。
为了能够更加直观准确地分析不同逻辑占用CPU的时间段,以及旧版多线程系统到底是哪里出现了问题,我们做了一个性能监测器,下图就是旧版多线程系统中的一个例子。

(旧版多线程系统的性能开销)
这张图中每一行是一个线程,横坐标是时间。不同逻辑或个体用不同的颜色表示,比如白色的部分就是每个分拣器逻辑在对应线程中的具体的执行时间和消耗,白色部分最上方的红色条是整体分拣器任务的耗时,在这一帧的分拣器开销约为3.6ms。而这一帧所有逻辑的总开销约22ms。

(红框中间的区域是分拣器任务开始到所有分拣器任务完成的耗时)
我们放大后可以发现一些问题,最明显的就是,不是所有的线程都同时开始同时结束任务,而是一种参差不齐的状态。再仔细观察,如下图红黄色的分割线,可以看到,有些线程甚至需要等待别的线程工作完毕才开始执行自己的任务。

(图中可以看到,标记1、2、5的线程结束后,标记3、4、6的线程才开始工作)
造成这种现象的原因有很多,比如系统需要执行别的程序的任务,其中不乏优先级很高的任务会挤占计算资源,导致并不是所有核心都能优先处理游戏内的逻辑。
又或者是因为单一一个线程在跑的部分占的时间太长,导致此时部分操作系统可能会因为活跃的线程任务少、常常有核心空闲而出于省电等目的在多线程利用率低的情况下关停几颗核心。
总之,系统自动调度核心与线程这种近乎于黑箱的机制,最终导致有核心却没能用上。并不是说16个核心只用上了15个所以效率只有15/16,而是只要有一个线程因为上述原因掉链子了,到最后就会所有人都等它一个,导致整体效率被拖累。比如下图中,实际让CPU执行任务的时间(白色区域)可能只有不到整体的2/3。

(黄色区域是大块的效率低下的区间)
并且,即使没有这个调度问题,也能从图里看出来不同线程完成任务所花费的时间差别明显,即使没有那几个晚开始的线程,最快的一个线程完成任务时的时间可能只有最慢那个线程的50%左右。

最后观察一下每个阶段的结束部分。在最后阶段结束,到下一个阶段开启之间,也有一个明显的“断裂”。这是因为每个阶段之间用了比较粗暴的阻塞锁,阻塞锁带来的开销会达到约50μs,这是一个比较高的水平。
新版多线程,堂堂登场!
为了优化出CPU的空间,我们完全摒弃了旧的多线程代码,从头到尾重新开发了一套全新的多线程系统以及新的游戏逻辑更新流水线。
在新版多线程中,我们需要尽可能地让每一个核心发挥作用,下图是截至目前写这篇日志时新版多线程的性能开销截图:

白色部分还是分拣器的开销,我们看到现在分拣器的各个线程的基本上紧密排列,几乎同时开始同时结束,看起来非常的漂亮!开销也降到了2.4ms左右(这是同一个存档),总体开销更是从一开始的22ms降低到了11.7ms,实现了约88%的效率提升(仅逻辑帧效率)!在戴森球计划这款游戏中,这个提升在大多数情况下甚至比CPU从14400F升级到14900K带来的提升还要大!至于为什么能有这么大的提升,还请看下面的一一分析。
1. 可自定义的核心绑定:以前的多线程系统中,线程是没有绑定在特定的CPU核心上。操作系统会自动给线程分配核心。这些近乎黑箱的机制让我们的程序几乎无法掌控多线程的内部分配逻辑,可能出现上文中各种处理器核心利用不充分的情况。而现在,玩家可以指定将某个线程绑定在特定的核心上,从而避免系统调度做出这些"骚操作"。

(放大之后观察,新版多线程系统<右>已经不会像旧版<左>那样明明还有空闲核心却仍有大量线程串行)
2. 动态分配任务:虽然现在已经可以绑定线程至核心了,不过仍然可能因为任务分配不均或者不同核心能力差异导致出现个别线程拖后腿的情况,还有可能有的核心就是在执其它进程或因为别的原因导致其中一个线程晚一点开始工作。于是我们为任务分配加上了动态调整的机制。
我们的思路是这样的:先大致均匀地分配任务,然后先完成任务的线程从剩下的工作最多的线程中拿取一半的工作,直到剩余工作最多的那个线程的剩余工作数量也低于某个特定值为止。这样既尽可能减少了动态分配的开销又避免了最终总是"一核有难,八核围观"的情况。观察下图也能发现,即使可能因为别的原因,有一个线程开始的时间落后了很多,最终也是所有线程在差不多同时结束。

(即使有时有一个线程开始得缓慢,但基本所有的线程都同时结束了运算)
3. 更灵活的框架设计:我们放弃了原本一次多线程阶段只能执行一种任务的设计,而是先把所有逻辑都分类梳理成多种任务,再自由地将这些任务排列组合。即使是在同一个阶段内,也可以同时执行多种任务。下图中黄色框选部分是将流速监测器、喷涂机和物流站的传送带出口逻辑并行后的性能截图。

(流速监测器、喷涂机、物流站的传送带出口逻辑并行以后总计占用了不到0.1ms)

(而原先这部分是单线程逻辑,开销达到了约0.6ms)
同样,也正因为这种灵活的设计,某些之前只能待在主线程上的逻辑也有了更多的选择。比如下图红色箭头所指的蓝色区域就是研究站在研究模式时的逻辑,虽然我们还是把它放在主线程上,但是可以“藏在”制造台等各类设施的逻辑底下,既高效地利用了处理器的各个核心,又不用担心会发生逻辑冲突。

(比之前等待别的任务结束后再进行计算更加的灵活)
从上图也可以看出来,多种不同的任务同时执行,只要最后一种任务加了动态分配,即使前两种任务不使用动态分配,也能让所有线程大致在同一时间完成。利用这一点,我们就可以将方便做动态分配的任务放在不便做动态分配的任务之后,就能“填满”这一部分的CPU性能空间。

(将敌方防御塔和黑雾单位的逻辑和我方电网的逻辑放在一个阶段更新,可以将参差不齐的电网逻辑后面空闲的CPU时间也利用起来)
4. 多线程通信等待机制:之前的多线程系统中的一个阶段内最后一个任务结束时主线程往往需要0.02~0.03ms才能反应过来并进行下一个阶段,而下一个阶段开始时又需要经过一段时间第一个任务才能开始执行,从截图中可以看出,离上一阶段的分拣器最后一个任务结束到下一阶段传送带的第一个任务开始,经过了约0.065ms。而新版多线程系统的这一时间只需要0.0065ms即6.5μs,二者之间相差了近10倍。

(新多线程等待时间<左>明显比旧多线程<右>更短)
我们使用了响应更快的自旋锁(约10ns),并且混合了自旋-阻塞两种模式。在代码执行时间极短的场景中使用自旋锁,在需要长时间占用CPU的场景中使用阻塞锁,从而在性能和资源消耗之间取得平衡。我们看到最后的结束阶段几乎没有了之前那种明显的“断裂”。
当然,新版多线程系统还有很多可以优化的地方。我们的线程分配策略还需要经过测试后继续优化以适应不同规格的处理器,还有很多逻辑并没有加入到多线程中。我们之后会出一个公开的测试分支,在这个分支中,我们开放了不同的设置选项让玩家手动定制各种多线程的分配策略和等待策略,以收集不同环境下新多线程系统的表现,这对我们后续的优化非常重要。

(多线程系统定制策略界面)
由于我们完全重制了游戏的主干逻辑流程,很多不同种类的任务现在可以并行运算了(比如可以同时更新电网和物流站的传送带输出),导致游戏中CPU性能测试面板中的数据已经没有参考价值了。为了适配新的逻辑结构,在端出来之前我们不得不重做这一块内容,并且我们还需要向玩家提供一个全新的性能分析工具。通过它,可以很清晰地看到新逻辑的运行方式与效率。

(我们知道大家可能会觉得前面的图挺酷的,不用担心,我们会把它端上来!)
今天的开发日志就到这里了,谢谢大家阅读!我们计划在接下来的几周内开启一次公开测试,所有已经购买游戏的玩家都可以直接参与,欢迎大家第一时间上手体验新版多线程系统。希望通过大家的共同参与,帮助我们验证新多线程系统在不同硬件环境下的稳定性与流畅度,为后续正式上线打下坚实基础。
再次祝愿刚经历高考的朋友们——志愿填得顺、大学上得爽!悄悄说一声,高考结束的暑假和空调房中玩戴森球计划更配哦~
本篇完


IP属地:重庆1楼2025-06-27 22:48回复
    沙发


    IP属地:江苏来自iPhone客户端2楼2025-06-27 23:10
    回复
      2025-09-22 14:18:33
      广告
      不感兴趣
      开通SVIP免广告
      这是真想把大家教成工程师


      IP属地:辽宁来自Android客户端3楼2025-06-27 23:12
      收起回复
        宝宝我还以为你跑路了😭你不在的日子,我每天都在想你😩😩


        IP属地:北京来自Android客户端4楼2025-06-27 23:13
        回复
          沙发


          IP属地:广东来自Android客户端5楼2025-06-27 23:13
          回复
            太硬核了,不过强力支持


            IP属地:湖南来自Android客户端6楼2025-06-27 23:15
            回复
              太长不看,有无省流


              IP属地:江苏来自Android客户端7楼2025-06-27 23:15
              收起回复
                太强了高低要给你们磕一个


                IP属地:浙江来自Android客户端8楼2025-06-27 23:15
                回复
                  2025-09-22 14:12:33
                  广告
                  不感兴趣
                  开通SVIP免广告
                  听不懂,但是好厉害的样子


                  IP属地:云南来自Android客户端9楼2025-06-27 23:22
                  回复
                    看的出来很高兴啊干前端的羡慕了,压根用不了多线程


                    IP属地:福建来自Android客户端10楼2025-06-27 23:24
                    回复
                      既然这样就原谅你们这么久不更新了


                      IP属地:北京来自Android客户端11楼2025-06-27 23:40
                      回复
                        打卡


                        IP属地:辽宁来自Android客户端12楼2025-06-27 23:45
                        回复


                          IP属地:河南来自Android客户端13楼2025-06-27 23:46
                          回复
                            前排支持,那么我的万糖工厂可以继续加量了


                            IP属地:吉林来自Android客户端14楼2025-06-27 23:48
                            回复
                              2025-09-22 14:06:34
                              广告
                              不感兴趣
                              开通SVIP免广告
                              不赖


                              IP属地:江苏来自Android客户端15楼2025-06-27 23:53
                              回复