写点什么

瞧不上 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/

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

发布了 940 篇内容, 共 532.8 次阅读, 收获喜欢 1105 次。

关注

评论

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

【LeetCode】检查句子中的数字是否递增Java题解

Albert

LeetCode 5月月更

error: conflicting types for xxx in c

codists

c

OpenMLDB 实时引擎性能测试报告

第四范式开发者社区

人工智能 机器学习 数据库 性能分析 特征平台

JAVA什么是反射?

源字节1号

软件开发

对于编程思想和能力有重大提升的书有哪些?

宇宙之一粟

书单推荐 编程思想 5月月更

全链路压测(十四):生产全链路压测SOP

老张

性能测试 全链路压测 稳定性保障

JAVA SPI机制

源字节1号

一键式打造DAO,M-DAO或成Web3新宠儿

股市老人

为什么人工智能需要可解释性?

博文视点Broadview

朱啸虎称赞的Web3,进入MOVE PROTOCOL将直达

股市老人

助力传统游戏转型GameFi,Web3Games推动游戏发展新航向

One Block Community

区块链 黑客马拉松 gamefi Web3.0

Golang 的艺术、哲学和科学

宇宙之一粟

Go 语言 5月月更

Java【开发入门学习】笔记一

恒山其若陋兮

5月月更

LabVIEW控制Arduino采集电位器电压(基础篇—4)

不脱发的程序猿

单片机 LabVIEW LIAT Arduino Uno 采集电位器电压

Maven 跳过测试的几种方式

HoneyMoose

电子邮件的传送过程

工程师日月

TCP 5月月更

Docker下RabbitMQ四部曲之三:细说java开发

程序员欣宸

Java Docker RabbitMQ 5月月更

什么是区块哈希?哈希趣投娱乐竞猜游戏开发逻辑(成熟源码)

开发微hkkf5566

[Day46]-[数组]-三数之和

方勇(gopher)

数组 双指针 LeetCode

哈希能作弊吗?哈希竞猜游戏防作弊系统开发逻辑(稳定运营)

开发微hkkf5566

今天爬,明天没,天津市XX网 详情页加密逻辑拆解,文中关键字已经加密

梦想橡皮擦

5月月更

LabVIEW控制Arduino流水灯(基础篇—3)

不脱发的程序猿

单片机 LabVIEW Arduino LIAT 流水灯

LabVIEW控制Arduino实现PWM呼吸灯(基础篇—5)

不脱发的程序猿

单片机 LabVIEW LIAT Arduino Uno PWM呼吸灯

一键式打造DAO,M-DAO或成Web3新宠儿

BlockChain先知

2.3 廷克图(TinkerGraph)介绍

Geek_古藤模根

Gremlin 廷克图 图数据库 TinkerGraph

Windows编译环境介绍

Loken

音视频 5月月更

基于云服务MRS构建DolphinScheduler2调度系统

华为云开发者联盟

大数据 MRS 华为云 DolphinScheduler 调度处理

模块二,微信朋友圈架构

泋清

#架构实战营

跨平台应用开发进阶(十七) :uni-app实现内嵌H5应用

No Silver Bullet

uni-app 5月月更 内嵌H5应用

使用 Provider 搞定 Flutter 的局部刷新

岛上码农

flutter ios 前端 安卓开发 5月月更

客观的聊一聊,裁员这件糟心事

互联网 职场 裁员

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