写点什么

瞧不上 C++ 和 D 语言,国外程序员将 5.8 万行代码迁移到 Jai 语言,到底图什么?

  • 2022-12-02
    北京
  • 本文字数:3633 字

    阅读完需:约 12 分钟

瞧不上 C++ 和 D 语言,国外程序员将 5.8 万行代码迁移到 Jai 语言,到底图什么?

本文中,我将向大家分享自己把游戏开发成果移植到 jai 语言的经历。我的游戏之前主要是用 D 和部分 C_++ 编写的,总代码有 58620 行(不包括库)。其实这事我已经筹划了很久,还专门记录下了最初的期望、移植过程和最终结果。下面,就请大家随我一道回顾这段历程。


为什么要移植


很多朋友可能好奇,为什么会有人要大费周章把这么些代码移植成另外一种语言?把项目做完,之后再用新语言不好吗?我当然有自己的考虑,主要基于以下几点:


  • 现状让我的日常工作非常痛苦

  • 我有称手的系统移植工具,所以移植过程应该会比较顺利

  • 对我来说,Jai 似乎比 C++ 或者 D 更有吸引力


为什么不用 C++


网上关于 C++ 缺点的讨论已经很多,所以这里不再赘述。简而言之,C++ 几十年发展下来积累了太多错误决策,而且永远都摆脱不掉了。它的标准库堪称灾难,代码几乎不可能在交给他人后顺利运行。而且不知道为什么,C++ 每次新增功能都有坑。所以 C++ 的使用感受确实很差,时时刻刻在折磨着我,而且这个项目的发展方向在我看来也有问题。总之,我想尽快离开 C++ 生态系统。

为什么不用 D

我的游戏开发之旅始于 2019 年,当时我已经感受到 C++ 的问题了,但并不知道哪种语言更好。于是我选择了 D,理由如下:它很像 C++,只是去掉了不好的部分。但很遗憾,这只是我的一厢情愿,D 在很多方面都跟 C++ 一个德行。


必须承认,D 跟 C++ 比确实有一些优势,比如更强大的元编程、无需标头、没有未初始化的值等。但很遗憾,这些优点根本就抵消不掉现实缺陷。


我在 Windows 上的两种 D 编译器(dmd 和 ldc2)之间反复横跳了四年,最后发现至少在 Windows 平台上,D 语言的状态也就是个业余项目、往好听了说也是极不成熟的水平。很难相信这是种已经发展了 20 多年的语言。截至目前,我不建议任何人在 Windows 上用 D 开发严肃项目,甚至还不如继续用 C++。从我亲身经历的问题来看,目前最大的麻烦是 Windows 上的调试信息完全 损坏:


  • 在 90% 的情况下,this 指针会缺失或出错

  • 函数堆栈上的变更经常部分 / 完全丢失或出错

  • 有时会报告变量值错误,但却没有任何其他可见的问题

  • 静态 foreach 扩展处理不当,令调试工具无所适从

  • minxins(相当于 D 中的宏)生成的调试信息会令调试工具找不到正确文件(只能按步进行反汇编)

  • 在 Visual Studio 中将指令指针移回上一行,经常导致程序在执行下一条指令时崩溃在 D Language Code Clulb 讨论版上,我看到“DMD 以往曾经出过很多关于调试信息的问题”,但号称正在处理中。可很遗憾,ldc2 也根本没好到哪去。而且除了调试之外,元编程这边也有不少缺点和问题:

  • 不同的编译器阶段会诡异地相互影响,导致元编程出现意外,引发误导性错误

  • ldc2 的编译速度非常慢,但我别无选择,因为 dmd 有 bug

  • D 提供所谓 betterC 模式,其中禁用垃圾收集;但在使用此模式时,标准库不编译而且元编程也会受到严重阻碍

  • 说明文档缺失

  • 还有其他几十个更具体的问题


总而言之:虽然 D 在某些方面确实比 C++ 强点儿,但其他地方的问题反而更多,导致使用体验痛苦万分。其中最大的问题就是糟糕的调试信息和极具破坏力的垃圾收集机制。我承认,我只是想把 D 当成改进版的 C++ 来用,而 D 的开发者并不认同这种理解。所以 D 不适合我,而且我现在根本就不敢信任它的调试器。


为什么选择 Jai


种种遭遇,把我引向了 Jai。这是 Jonathan Blow 从 2014 年开始开发的一种语言,而且直到 2019 年 12 月才向编译器敞开大门。目前封闭测试仍在进行中,我也是约两个月前受邀体验的一员。Jai 的诞生源自 Blow 对 C++ 的失望。而且跟 D 不同,Jai 确实朝着我所认可的、能够对 C++ 做出有意义改进的方向前进。在我看来,Jai 的最大优势有二:更快的编译速度,还有以不受限制的编译时执行进行元编程。


值得注意的是,这里讨论的可不是编译速度提高 20%、或者元编程功能选项略有增加之类不痛不痒的小改进。Jai 的编译速度提高了 10 到 100 倍,而且能在编译期间执行任何操作。特别是与编译时编译器 API 相结合的元编程,已经带来了具有深远意义的影响:例如消除对构建系统的需求,也摆脱了对复杂的非启发式自定义检查的依赖。除此之外,Jai 还对 C++ 做出了其他改进,例如更好的默认值、更简单的语法、更实用的标准库、命名函数参数、上下文、using 等。这就让我有了信心,打算通过从 D 移植到 Jai 让自己的游戏获得以下收益:


  • 将编译时间从现在的 60 秒左右降低到 5 秒以内,最好能达到 1 秒上下

  • 调试器终于能用了 

  • 用 Jai 代码替代了 build-script

  • 用元编程引入自定义编译检查,借此捕捉更多错误

  • 用更简单的命令式代码替代复杂的元编程代码

  • 提供一系列语法改进,减少了代码中的噪声

  • 删除了以往使用多种语言时无法避免的重复部分 ##

    为什么不考虑其他语言


的确,我明确不想用的只有 D 和 C++,而略感兴趣的是 Jai……那为什么不试试别的语言呢?


最无法回避的选项应该就是 Rust 了。它风头正劲、社区活跃,但我还是感觉 Rust 在设计权衡方面有点问题。支持 Rust 的开发者们似乎有种“内存安全是第一要务”的集体心态。没错,很多问题都源自内存安全问题,所以我大体能够理解这样的判断。但也有很多对于安全和质量要求没那么极端的软件需求。


比如,我相信如果 C 和 C++(基本就是公认的「最不安全」语言)能够去掉零终止字符串、默认初始化值和适当的指针 + 长度类型,那就足以用边界检查取代 90% 的指针算法、解决临时内存管理的需求了。另外,我觉得很多人在追求“安全”代码时,其实是忽略掉了软件漏洞层出不穷的根本原因:文化上对于复杂性的容忍,甚至是鼓励 14。总之,我不愿意忍受 Rust 那漫长的编译时间,也不太认同它的文化定位。跟 Rust 不同,jai 就很关注如何控制复杂度,这一点更符合我自己的文化判断。


还有其他一些人气稍逊的选项,例如 Zig。我只能说它们可能也有巨大的潜力,但我不太相信这些会是正确的选择。不是好或者不好,只是没那么强的吸引力。


如何移植


刚开始,我还在心里给自己鼓劲、祈祷移植过程能顺利完成。之所以比较乐观,是因为我在游戏中设置了两套有助于移植的系统:


  • 游戏会将游戏会话的输入(HID、加载文件、网络等)记录到文件内并稍后重播。在重播时,记录的输入输出确定性能让游戏循环达到完全相同的状态,精确无误。

  • 游戏在执行过程中的各个点位上,都会对游戏状态进行哈希处理,并将相应的哈希值保存在不同文件当中。这样在重播时,文件内容即可用于检查重播是否跟原始执行完全匹配。


这两项功能间有一些细微差别,但对整体运行影响不大。依托这些功能,我的移植计划如下:


  • 将一小段代码由 D 或 C++ 复制到 jai,而后编译。

  • 调用新的 jai 代码,替代旧有 D 或 C++ 代码。

  • 重播录制的游戏会话。

  • 如果重播发生分歧,则说明移植引入了 bug,修复此 bug。

  • 如果重播无分歧,则移植成功,继续下一步。


这种方法的关键在于:


  • 是不是所有引入的 bug 都会导致游戏状态发生显著分歧?

  • 代码能否以较小的增量进行移植,以便易于知晓 bug 存在、找到 bug?


第一个问题,取决于状态哈希覆盖到多少代码。有些代码需要知晓游戏是否正在重播,这些部分的内部行为会有所不同,因此无法得到有意义的哈希值。例如,写入文件功能会在重播时丢弃一切数据,因此如果移植后的写入文件中出现了 bug,就会被哈希过程注意到。幸运的是,大部分代码并不属于这一类。最初的哈希在代码库中极少被用到,但最近我开始将其扩展到插入动态数组的过程,例如记录动态数组的大小和容量。


如此一来,当有 bug 导致动态数组的插件会改变游戏逻辑时,问题就会被及时注意到。因为我代码中的几乎所有功能都是靠动态数组实现的,所以即使是在任何庞大而复杂的数学算法当中,每一点微波的行为变化都能引起注意。


第二个问题则属于经典编程问题:你的代码解耦程度到底有多高?这一点非常有趣,因为我得把代码库里的所有偶发复杂性元素找出来。首先就是模板函数:它们无法直接移植,因为函数定义和调用站点必须在同一编译器之内,才能让模板正常起效——除非对模板进行手动实例化。我的代码库里有不少模板化代码,但它们跟容器和序列化没什么紧密关联,所以我觉得这应该不会惹出太大的麻烦。


继续推进


下面来点更直观的统计数据和图片吧。先来看我这代码库的当前状态:



总体来说,这里有 45701 行 D 代码和 12919 行 C++ 代码,总共 58620 行。编译时间如下:



在调试模式下,ldc2 的编译过程大概需要 3 分钟,最高占用 8 GB 内存。这时候如果打开浏览器,那我这台 16 GB 内存的笔记本电脑就会进入满负荷运行。发布模式内存占用量更大,为 11.5 GB。




如果一切顺利进行,那在两张图中,一切现有色块都应该会被新色块取代。能成功吗……


最后,我想用数字来解释移植的收益,特别是我当初的预期错得有多离谱:


  • 我预计整个过程需要 160 个小时(每周 40 小时,共耗时一个月)。但我这个估计差得太多了,很可能根本用不上一个月的时间。

  • 我希望编译时间能从 1 分钟左右下降到最多 5 秒,能到 1 秒上下最好。至于移植后的最终结果如何,我将持续保持更新。


原文链接:

https://www.yet-another-blog.com/porting_the_game_to_jai_part0/

公众号推荐:

跳进 AI 的奇妙世界,一起探索未来工作的新风貌!想要深入了解 AI 如何成为产业创新的新引擎?好奇哪些城市正成为 AI 人才的新磁场?《中国生成式 AI 开发者洞察 2024》由 InfoQ 研究中心精心打造,为你深度解锁生成式 AI 领域的最新开发者动态。无论你是资深研发者,还是对生成式 AI 充满好奇的新手,这份报告都是你不可错过的知识宝典。欢迎大家扫码关注「AI前线」公众号,回复「开发者洞察」领取。

2022-12-02 16:214649
用户头像
李冬梅 加V:busulishang4668

发布了 813 篇内容, 共 380.6 次阅读, 收获喜欢 999 次。

关注

评论

发布
暂无评论
发现更多内容

Python应用之计算阶乘

智趣匠

函数 10月月更 阶乘计算

Vue3入门指北(六)列表渲染

Augus

Vue3 10月月更

React-Hooks源码深度解读

goClient1992

React

JWT 和 JJWT 还傻傻的分不清吗

HoneyMoose

MyBatis 学习笔记之配置文件

openpark

mybatis 10月月更 mybatis配置文件

Docker下,两分钟极速体验Nacos

程序员欣宸

Docker Spring Cloud 10月月更

Linux操作系统——进程管理、RPM与YUM

胖虎不秃头

操作系统 Linux tar 10月月更

Linux操作系统——日志管理

胖虎不秃头

Linux 操作系统 10月月更

SAP 电商云 Spartacus UI 的响应式 UI 实现细节

Jerry Wang

前端 前端开发 web开发 10月月更 breakpoint

什么是虚拟服务器?一共有多少种虚拟服务器?这篇文章带你扫盲!

wljslmz

10月月更 虚拟服务器

React源码解读之任务调度

flyzz177

React

2022-10-02:以下go语言代码能否通过编译?A: 能;B: 不能;C: 不知道。 package main import ( “fmt“ ) type worker interfa

福大大架构师每日一题

golang 福大大 选择题

react的jsx和React.createElement是什么关系?面试常问

beifeng1996

React

写过自定义指令吗,原理是什么?

bb_xiaxia1998

Vue

【LeetCode】检查二进制字符串字段Java题解

Albert

LeetCode 10月月更

Python基础(七) | 文件、异常以及模块详解

timerring

异常 Python Monad 10月月更

MyBatis 学习笔记之MyBatis入门开发

openpark

mybatis 10月月更 mybatis入门

Collections之 Arraylist源码解读(二)

知识浅谈

ArrayList 10月月更

怎样对react,hooks进行性能优化?

beifeng1996

React

面试官:vue2和vue3的区别有哪些?

bb_xiaxia1998

Vue

带你实现react源码的核心功能

goClient1992

React

细说react源码中的合成事件

flyzz177

React

业务实时监控服务

穿过生命散发芬芳

10月月更 业务监控

大数据ELK(十一):Elasticsearch架构原理

Lansonli

elasticsearch 10月月更

JS继承有哪些,你能否手写其中一两种呢?

helloworld1024fd

JavaScript

如何在 Linux 中删除超过 30 天的文件

wljslmz

Linux 10月月更

能否手写vue3响应式原理-面试进阶

helloworld1024fd

JavaScript

使用 RxJs 实现一个支持 infinite scroll 的 Angular Component

Jerry Wang

前端开发 angular RXJS web开发 10月月更

js事件循环与macro&micro任务队列-前端面试进阶

loveX001

JavaScript

【SSM】Spring系列——IoC 控制反转

胖虎不秃头

spring ssm 10月月更

Spring Lombok 实体类死循环问题

HoneyMoose

瞧不上 C++ 和 D 语言,国外程序员将 5.8 万行代码迁移到 Jai 语言,到底图什么?_编程语言_Simon van Bernem_InfoQ精选文章