本集简介
双语字幕
仅展示文本字幕,不包含中文音频;想边听边看,请使用 Bayt 播客 App。
欢迎来到《软件即兴谈》。
Welcome to Software Unscripted.
我是主持人理查德·菲尔德曼。
I'm your host, Richard Feldman.
今天我跟安德鲁·凯利聊天,他是Zig编程语言的创造者。我最近花了很多时间研究Zig,因为我这几年一直在开发的Rock语言,现在正要重写编译器,改用Zig来实现。
Today, I'm talking with Andrew Kelley, the creator of the Zig programming language, which is a language I've been spending a lot of time with lately because the rock programming language, which I've been developing for the past few years, is getting a compiler rewrite in Zig.
我们聊了很多话题,从内存管理,到编译器中间表示、字节码的序列化和反序列化,再到进程间共享内存,甚至还有防病毒软件这些意想不到的内容。
We talk about all sorts of things from memory management to serialization and deserialization of compiler intermediate representations, byte code, what that means, shared memory across processes, and antivirus of all things.
《软件即兴谈》由Patreon提供支持。
Software Unscripted is supported on Patreon.
如果你喜欢这个节目,想成为支持者,请访问patreon.com/softwareunscripted。
If you would like to become a supporter, please check out patreon.com/softwareunscripted.
接下来,有请安德鲁·凯利。
And now Andrew Kelley.
好的。
Alright.
安德鲁,谢谢你来参加这次对话。
Andrew, thanks for joining me.
嗨。
Hey.
是的。
Yeah.
很高兴能来这里。
Good to be here.
太好了。
Cool.
你创造了Zig语言。
So you made Zig.
我最近用Zig感觉特别顺手。
I am enjoying using Zig.
首先,真心感谢你开发了Zig。
So first off, thanks for making Zig.
我现在用得比以前多多了。
I've been using it a lot more now.
你知道的,咱们聊这事儿都好几年了,以前只用在 Rock 标准库,现在整个编译器重写都在用它。
I mean, as you know, because we've been, you know, talking about this for years, used to use it for Rock Standard Library and now we're using it for the whole compiler, in the rewrite.
最近很多人在讨论 Zig 的新 IO 设计。
And one of the things that a lot of people have been talking about when it comes to Zig lately is the new IO design.
要是有人还不了解,要不要简单介绍一下?
For people who haven't heard about this, do want to just give a real quick recap?
当然可以。
Yeah, sure thing.
所以我做了一个决定,不再硬编码直接调用操作系统的 API,比如文件系统、网络、定时器这些,而是把所有人的代码都打乱了——现在任何想做这些操作的函数,都得传一个 IO 参数进去,具体怎么实现,就看你用的是哪种执行模型。
Yeah, so I made the decision that instead of having cross platform OS abstractions that were hard coded to call directly into operating system APIs, like file system, networking, timers, and all this stuff, I made the decision to break everyone's code, a lot of it, and you're going to now have to take an IO parameter to any function that wants to do any of that stuff, and then that's going to have a different implementation depending on your execution model.
对。
Right.
这跟分配器 API 很像,就像你想做内存分配,也得传一个分配器进去一样。
And so this is pretty similar to the allocator API, where much like allocators, like if you wanna do allocations, you gotta pass an allocator around.
所以,关于这个在 Hacker News 之类的平台上的反应,奇怪地集中在你博客里顺带提过的一个点——就是那个‘你的函数是什么颜色’的问题。
So the, the response to this on, you know, hacker news and places like that has been, weirdly focused on like some aspect of the blog posts that you happened to mention where you were kind of talking about the, what color is your function thing?
人们说:‘这跟我理解的函数颜色完全不是一回事啊。’但说实话,我觉得整个事儿挺离谱的,咱们先别纠结这个岔开的话题。
And people are like, well, that's not what I think about when I think of what color is your function, but I don't know that, that whole thing just seemed very strange to me because like, before we kind of actually let's not get into that distraction for a second.
我觉得有意思的是,一方面,确实意味着你得重写所有跟 IO 相关的代码,加上一个额外参数。
One of the things that strikes me about this is like, okay, on the one hand, yes, it does mean that you need to rewrite all of your IO related code to take an extra parameter.
这倒也没错。
Fair enough.
但另一方面,据我经验,IO 相关的代码通常都集中在代码库的某一个特定部分。
However, it's also the case that like, at least in my experience, IO related code tends to always kind of be in like one section of the code base.
当然,这也得看你具体在做什么东西。
I guess it really kind of depends on what type of thing you're doing.
比如我要是用 Zig 写个电商网站的服务器,可能得不停地跟数据库打交道,但我猜这应该不是大家用 Zig 的主流场景。
Like if I were doing a, I don't know, like an e commerce type web server in Zig, maybe I'm doing a whole lot of like database stuff constantly, but I'm kind of assuming that's not what people are usually using Zig for.
反正我写编译器的时候,所有 IO 操作都集中在一个地方。
So I don't know, like, I mean, me working on a compiler, it's like all the IO happens at one place.
所以我们说的并不是整个代码库需要大改。
So we're not talking about like, you know, a ton of, like the code base needing to change for this.
你见过谁的代码里IO部分到处都是,非得改一大堆函数签名吗?
Do you know about people actually needing to like have a ton of, of IO like scattered all over their code base that's really going to change a lot of function signatures?
是的,这点说得对。
Yeah and that's a good point.
我觉得改成带IO参数的版本其实特别简单,一点难度都没有。
I think actually updating to the IO parameter it's going to be easy peasy, lemon squeezy, no big deal.
不过我事先决定,得先重新评估一下读写流的API,结果我把它们也给打破了。
But I decided though, as a prerequisite, that I needed to reevaluate the readerwriter streaming APIs, and I also broke those.
这事我们已经处理完了,改动也合并进去了。
That's our and I already landed that breakage.
我们把这次变动戏称为‘Writergate’。
We called that scandal Writergate.
我们有个传统,把重大破坏性改动都以总统丑闻来命名。
We have a practice of naming our big breakages after presidential scandals.
但只用那些过去重要时期的经典丑闻命名。
But only the good old scandals from the old days when they were important.
总之,重点是,升级起来其实挺难的,因为写流式实现时,高度依赖你手头可用的接口。
Anyway, point being, that actually is difficult to upgrade to because, it turns out writing a streaming implementation, it's very, very dependent on the interface that you have available to you.
特别是,这种新流式设计的核心理念是:缓冲区放在接口里,而不是实现里。
In particular, the kind of key design concept with this new way of doing streams is that the buffer is in the interface, not the implementation.
有意思。
Interesting.
明白了。
Okay.
所以你可以自己定制,对吧?
So you can kind of You can customize it yourself, essentially?
是的,你可以这么做,但更重要的是,接口里有很多辅助方法,都能直接基于这个缓冲区来优化关键路径。
Yeah, you can do that, but more importantly, like there's all these helper methods in the interface that all can then just like put the hot path as operating on that buffer.
而慢路径则会调用你的流式实现,比如执行写入、读取之类的操作。
And then the slow path is gonna call into your streaming implementation that calls, you know, write or read or whatever.
明白了。
Okay.
这说得通。
That makes sense.
对。
Yeah.
我们还没怎么接触过流式处理。
We haven't done anything with streaming yet.
目前只有一个地方可能用到,就是下载包的时候,特别是那种带大型主机二进制文件的平台。
There's only one place where we might do it, which is like when you're downloading a package, because it might be, especially if it's like a platform and it's got like a big host binary in there.
我们可能希望一边下载一边解压,类似这样的操作。
We might want to like download and decompress as we're downloading, stuff like that.
但也就仅限于此了。
But that's kind of it.
所以到目前为止,这对我们完全没有影响。
So there hasn't yeah, That hasn't affected us at all at this point.
是的
Yeah.
对
Yeah.
好
Okay.
那咱们聊聊这个博客文章吧,这事儿特别有意思。
So so let's let's talk about this blog post thing because this is really interesting to me.
先给不太了解‘函数的颜色’这类博客文章的人做个背景介绍。
So background for those who aren't familiar with the, like, what color is your function blog posts.
这篇文章是鲍勃·尼斯特罗姆写的。
This is, Bob Nystrom wrote it.
他最出名的作品是《构建解释器》。
He's the author of Crafting Interpreters, most famously.
他写了这篇帖子,主要讲的是 Node。
And he wrote this post that basically talks about Node.
JavaScript 就是异步加回调函数的 API。
Js is like async, and callback based APIs.
大家的抱怨主要集中在 Node 上。
And sort of the complaint is like in Node.
JavaScript 里,现在其实也有同步 I/O 的功能。
Js you have and in JavaScript in general, I guess now you have like synchronous IO.
在浏览器里,这只能用本地存储。
So in the browser, this will only be local storage.
但在 Node 里,
But in Node.
你可以做同步的文件读写操作。
Js, you can have like synchronous file reads and file writes.
你还可以用 async 关键字和 Promise 做异步,或者单独用回调函数。
And you can also have async using the async keyword and promises, and then you can also separately have callback based.
问题是,如果你从一种方式切换到另一种,不光是那个要改的函数要变,所有调用它的上层代码也得跟着改。
And the idea is that if you switch from one of these to the other, you have to change not only the function that actually wants to change it, but also all the callers have to change as well.
比如你用异步的话,所有调用它的函数都得改成异步;要是从同步切换到异步,所有上层调用者也得跟着改,等等。
Like if you're async, all of them have to become or if you're switching from sync to async, all the callers have to be async, etcetera.
所以你在博客文章里 basically 是说,这其实就是个函数着色问题——至少这是我理解的。
So you in the blog post were basically like, Hey, this is the function coloring problem because now we're sort of, at least this is my understanding.
你也可以告诉我,要是我理解错了,你并不认同这个说法。
You can tell me if this is, you know, you you would disagree with it.
但核心是,我们现在把这个问题抽象掉了。
But essentially the the point is that we're now abstracting over that.
所以,如果你想做任何 I/O,确实得传点什么进去,这免不了。
So it's like, if you wanna do any IO, yes, you do need to like pass something around.
但如果你只是想换 I/O 的实现方式,比如从同步改成异步,或者换一种异步风格、不同的执行器之类的,这完全不会破坏现有代码。
But if you wanna switch how that IO is being done from synchronous to asynchronous or from one async style to another, different, like, executor or whatever, that's a nonbreaking change, basically.
你可以在链路上的任何地方随时切换,就从这儿开始,我们改用新的方式了。
You just do that wherever you want at any point in the chain, it's like, From now on, we're doing it.
好吧,从现在起,我们用异步了。
Guess we're doing async now.
没错。
Exactly.
然后大家在 Hacker News 上提出的反对意见是,说,不,其实不是这样的。
And then the the objection that people had in Hacker News was like, well, no.
但就像函数着色真正发生的情况是,如果你在调用栈的深层有一些代码,现在我就得去加一个参数。
But, like, the real, like, thing that happens in function coloring is that if you have some something deep in your call stack, now I have to go add a parameter.
我得给所有相关的函数都加上这个参数,在我看来,这有两个原因让人觉得没抓到重点。
I have to add the parameters to all the colors, which to me feels like missing the point for two reasons.
其中一个原因是,其实很多时候,我不是说真的只传一个参数。
One reason is that, well, quite often, I mean, you're not literally passing one parameter.
你通常会传一个环境结构体之类的东西,然后从里面取数据。
You have like an environment struct or something that you're passing around and just pulling it off of that.
所以如果你本来就已经在往下传这个环境结构体了,其实根本不需要额外再加参数。
So if you already had the environment struct being passed down, you actually don't need an extra parameter.
你就直接像 env.xxx 那样调用就行,要知道以前可没有这种写法。
You're just like env dot or whatever, that that, you know, didn't used to have that.
现在它已经有了。
Now it has it.
但第二点,这和异步不一样。
But secondly, which is different from Async.
Node 里并没有那种异步机制。
There's no Async like in Node.
JavaScript 里也没有对应的实现。
Js equivalent of that.
但再说第二点,回到之前的话题,以我经验来看,Zig 开发者通常用的那些应用,I/O 操作基本都集中在一处。
But secondly, again, to the earlier point, at least in my experience for most type of applications that Zig authors are gonna be using, I would imagine all the IO is gonna be concentrated in one place anyway.
所以,这其实没什么好惊讶的。
So, like, like, it's not gonna be a big surprise.
哎呀,真是的。
Like, oh, man.
以前我根本没在编译器里做任何 I/O,直到后来才在文件系统那部分加上了。
I, you know, previously I was like not doing any IO in the let's do some IO on the file system part of the compiler.
而现在我确实在做了。
And now I am.
其实我一直都在那里做 IO 操作。
It's like, no, I was always doing IO there.
所以我也说不准。
So I don't know.
这两个反对意见,至少就我使用 Zig 的方式来看,我觉得根本不会影响我。
Those both neither of those objections feel like they would impact me at least the way I use Zig.
不过再说一遍,我不清楚别人怎么用 Zig,你肯定比我还清楚,但对我来说,这感觉有点吹毛求疵。
But again, I don't know how other people are using Zig and you would know better than I would, but to me it just felt like kind of a pedantic point to
这个反对意见是说,从不做 IO 变成做 IO,这时候把 IO 参数往下传,我觉得反而是个好做法。
That objection is about going from not doing IO to doing IO, in which case, threading a, you know, like, a IO parameter down your call stack is, like, good, I think.
挺搞笑的,我在 Zed 工作,顺便说一句,用一个以 Z 开头的三个字母的词,天天跟另一个以 Z 开头的三个字母的词打交道,真的特别容易混淆。
It's funny because so I work at Zed, and by the way, it's so confusing, like, using a three letter thing that starts with Z and working on a three letter thing that starts with Z, like, all the time.
我昨天还真跟一个人聊了,我们有一阵子没联系了。
I literally yesterday, I was talking with someone and I was like, we hadn't talked in a while.
我们在聊聊近况。
We were catching up on stuff.
我就跟他说,我最近在Zed工作,还在用Zig做Rock之类的项目。
And I I was talking about like, you know, oh, I'm working at Zed and I'm using Zig on Rock and stuff like that.
然后他说,哦,我最近也特别迷Zed。
And then he was like, oh yeah, I've gotten really into Zed lately.
我一直在用它做个东西。
I've been using it to build this thing.
我当时就说,我觉得你应该是说Zig吧。
I was like, I think I think you mean Zig.
他就是把两个词搞混了,你知道的。
It's like, he just swapped it, you know?
是的。
Yeah.
哦,对啊。
Oh, yeah.
不过说实话,我觉得如果你希望代码能方便测试,那就非得这么做不可,或者搞个更复杂、在我看来更糟糕的方案。
But yeah, it's, it definitely feels to me like the the if you want to have stuff testable, you need to do something like this or something that's like more complicated in my opinion, worse.
所以在 Zed 用 Rust 的时候,我们就这么干。
So like at Zed in Rust, we do this.
我们传的是一个叫 FS 的东西,不是 IO。
We pass around a thing called FS, not IO.
就是专门针对文件系统的那个。
It's like, you know, specific to the file system.
HTTP 之类的我们也各有独立的模拟器,但主要是因为测试时,我们想模拟一些奇怪的文件系统状态,又不想在真实文件系统里反复重建这些场景。
We have separate ones for like HTTP and whatnot, but it's like, yeah, because in our tests, we wanna simulate these like weird file system states that we don't wanna have to recreate on the real file system in tests.
我们想能直接问:如果这次写入因为这个原因失败了怎么办?
We wanna be able to be like, what if this write fails for this reason?
如果这次读取因为某个冷门原因失败了又怎么办?
What if this read fails for this esoteric reason?
所以,据我所知,要实现这种模拟器,大概就两种主流做法。
And so like, if you want a simulator, as far as I'm aware, there's like two popular ways to do that.
一种是像我们现在这样,传一个参数,然后把操作委托给它。
One is you pass a parameter like we're doing and you delegate to that.
另一种是搞那种面向对象的依赖注入,像Java里常见的做法——我早年当Java程序员的时候用过,现在回头看,真不明白为什么要这么麻烦。
And then two is you do some sort of object oriented dependency injection Java thing, which to me, like having done that in my career as an early, in the early days of my career as a Java programmer, I just look back on that and I was like, why?
明明只要传个参数就行,参数不一样也没关系,何必搞那么复杂。
It's so complicated to like do all that stuff when you could just be like, just pass a parameter around and sometimes the parameter is different.
就这么简单,完事了。
That's it, the end, so simple.
是的。
Yeah,
这方法不错。
that's nice.
等这个参数被广泛使用后,我们第一件要做的事就是这种注入式测试。
Like one of the first things we're gonna do after we have widespread usage of this parameter obviously is this kind of injection testing.
我们已经有个函数叫‘测试所有分配失败’,它会先运行你的单元测试,统计出总共分配了多少次内存,这个数字就是n。
We already have this function, test all allocation failures, where it will run your unit test first to figure out how many allocations you make, Then that number is n.
然后它会再运行你的单元测试 n 减一次,每次让不同的内存分配失败,确保你正确处理了内存不足的情况。
Then it will just rerun your unit test n minus one times, each time failing a different allocation to make sure that you handle out of memory correctly.
我们还可以对文件不存在、文件系统错误,甚至只是执行顺序做同样的事情。
We can then do the same thing for file not found, or file system errors, or even just ordering.
如果你有并发操作,那就用单线程跑单元测试,尝试各种不同的执行顺序,确保结果始终正确。
Maybe you have some concurrent thing, then, like, you actually run your unit tests single threaded with, like, multiple combinations of, of orderings to make sure that it's correct.
对。
Yeah.
在 Zed 里,我们对实时协作功能就这么做,因为整个系统都是基于 CRDT 这类技术的。
So at Zed, we do that for the real time collaboration stuff, because it's all based on, like, CRDTs and stuff.
所以得考虑各种情况:如果数据以这个顺序到达,或者那个顺序到达,权限该怎么处理,诸如此类。
And so there's all this, like, really elaborate, like, what if it comes in in this order or that order and the rights for, you know, like because there's yeah.
这种组合情况会变得特别复杂,你必须确保所有可能的场景都稳定可靠,不然生产环境里会出现那种特别难复现、特别难调试的 bug。
There's the the combinatorics of that can get pretty wild, and you definitely wanna have reliability across all the different scenarios that can happen because, otherwise, you get these bugs in production that are really frustrating to debug because the circumstances required to reproduce them are extremely difficult and like rare to come by.
所以在测试环境里能完全掌控这些情况,真是太好了。
So like having total control over that in testing environment is awesome.
在 Rock 的新设计中,我们也有意采用这种方式,当然不展开太多细节,因为平台开发者对所有 IO 接口都有完全的控制权。
This is also something we're planning on embracing in Rock, with the new design basically, without going into too much detail, because platform authors have total control over all IO APIs.
我们可以选择像 Zig 那样做,或者像我们在 Zed 里对文件系统 API 那样处理,当然也可以不这么做,完全由我们决定。
We have the option of either doing something like what Zig is doing and what we're doing at Zed for the file system APIs or not, we don't have to.
我觉得对于脚本来说,可能没那么必要,但其实也很简单——你只要在脚本开头写一行,比如 fs = 给我一个真实的文件系统,事情就差不多解决了。
I think for scripts, maybe it's not, like really required, but at the same time, it's also really simple because you just put at the top of your script, like, you know, fs equals, give me a real fs and then you're kind of done.
不过说实话,我觉得这种做法值得推广,因为如果你希望测试代码时不用真的操作文件系统,这本来就是你想要的。
But yeah, I, me, seems like, just a worthwhile thing to do to sort of standardize on because if you want to test your code in a way that doesn't require using the actual file system, it's like what you want anyway.
那我们为什么不鼓励这样做呢?
And why wouldn't we encourage that?
反正这正是我平时想用的方式。
That's what I would like to use anyway.
我们计划在测试中做的另一件好事是,因为我们完全掌控了 IO 系统,也能判断一段代码是不是纯函数,所以可以划分出一类测试:只要没有副作用,我们就说,如果这个测试依赖的任何代码都没变,那就根本不用重新运行。
And the other nice thing about something that we're planning on doing with our tests is because we have full control over the IO systems and whatnot, and we know when code is pure versus not, we can have there be, okay, well we can already have there be a category of tests where you don't do any effects, where we just say, hey, if the none of the dependencies of this test changed, like, if none of the code going into this test changed, we don't need to rerun it.
这一点我们是清楚的。
Like, we know.
我们知道答案是什么。
We we know what the answer is.
全是纯函数。
It's all pure functions.
根本没必要重新运行。
So, like, no no need to rerun at all.
而且,如果你的所有代码都是纯函数,或者那些副作用都被模拟了——也就是说,我们根本不会去调用平台的真实实现,而是用一些简单的占位函数来代替,核心思路是:所有IO操作都可以通过一个内置的简单原语来模拟,比如从一个全局内存存储中读写。
But also it's the case that if all of your code is either pure or the effects are simulated, meaning like we're not actually going to the platform to get the real implementations of anything, but rather we're just sort of stubbing them out with like kind of the idea is all IO can be simulated by just using a really simple baked in primitive that's like read to and write from some global memory store.
如果我们把这种模拟机制传给某一类测试,比如你打算做这种模拟测试,把所有IO实现都替换成这个统一的存储器。
And if we just pass that into the a certain type of test where you're like, okay, wanna do one of these simulation tests where like, I'm going to replace all the IO implementations with this one store.
那我们就拥有了同样的特性:没有任意的IO操作发生。
Then we have the same property where it's like, okay, well we don't have arbitrary IO happening.
所有的IO都通过这一个存储器进行。
All the IO is going through this one store.
因此,对于所有这类测试,只要代码实现和依赖没有变化,我们就完全没必要重新运行测试。
And so again, we can just guarantee that for all those tests, if none of the implementation, if none of the dependencies changed, don't we need to bother rerunning the tests.
希望这两种测试结合起来后,那些必须每次都重新运行的测试范围,就只剩下真正需要实际进行 IO 操作的集成测试了。
The hope is that with those two types of tests combined, this total surface area of tests that always need to get rerun is just limited to basically integration tests where you really want to do the actual IO for real and like IO.
其他所有测试基本上都可以缓存起来,根本不用重跑,除非相关的代码路径发生了变化——那时候才需要重新运行。
And all the other ones will just be essentially cached and just like don't need to rerun at all unless of course the relevant code paths change in which case you want them to rerun.
我们还没做这些,不过说实话……
We haven't done any of that yet, but I don't know.
这看起来是个很棒的优势,既然成本只是在做 IO 的时候多加一两个参数,甚至在有环境配置的情况下连参数都不用加,为什么不鼓励大家这么用呢?
It seems like a really nice benefit and like why wouldn't we encourage people to, like, have a pit of success there when the only cost is, like, an extra parameter here and there in the, you know, cases where you're doing IO and not even that if you've got an environment.
我觉得这简直太好了。
Seems seems great to me.
是的,听起来确实不错。
Yeah, that sounds really nice.
我们团队里现在有个人搭了个 RISC-V Jupiter 开发板,想在上面跑 Zig 的 CI,结果慢得要命。
Yeah, we have a core team member right now who set up a RISC V Jupiter board or something and is trying to run the Zig CI on it, and it's real slow.
所以这种减少不必要的测试,确实能帮上大忙。
So that kind of reduction of unnecessary testing would be helpful.
是的
Yeah.
对
Yeah.
我刚入行的时候,听到各种设计模式,比如依赖注入、里氏替换原则什么的,那时候真觉得挺玄乎的。
I mean, in general, like, I remember when I was earlier on in my career hearing about all these patterns, dependency injection and, like, Liskov substitution principle and and this and that.
但随着经验积累,这些术语确实有用,至少能帮我们更清楚地讨论问题。
But as I've spent more time in my career, like, I mean, those are helpful terms to know, I guess, to be able to talk about these things.
但归根结底,这件事就是难。
But at the end of the day, it really kinda comes down to, like, this is hard.
如果你用这种方式来做,就会轻松不少。
It'll be less hard if you do it this way.
举个特别典型的例子,就是我们的 REPL。
And and and a great example of this is, our REPL.
最早的 Rock REPL 是这么写的:先处理输入输出,读取用户输入,然后执行求值。
So the original rock REPL was written in kind of a like, okay, get the IO, like read what the user typed in, then go like do the evaluation thing.
事情变得相当棘手。
And it got pretty hard.
难点在于测试,尤其是网页上的 WebAssembly REPL,它和原来的版本在很多方面都完全不同。
Like the thing that was hard, to test this, especially because the WebAssembly REPL, like the one that goes on the website is like totally different in a lot of ways.
它没有标准输入,也没有完整的 TTY 功能,比如上下箭头、行读取这些都不支持。
It doesn't have like standard in, it doesn't have the whole like TTY, you know, like read line up arrow, down arrow stuff.
完全是两码事。
It's just completely different.
所以我们想了个办法:干脆把 REPL 做成一个状态机,不管从哪儿收到输入,先处理了再说。
And so the solution was like, okay, let's just make the REPL a state machine where it's like, okay, we get some input somehow from, from whatever source.
处理完之后,下一步该做什么?
And then once that's done, what do we want to do next?
这样就能把所有逻辑都测试一遍了。
And then we can test all of that.
至于输入输出的部分,最后再加上去就行。
And then you just have to sort of staple the IO on top of that.
WebAssembly 和命令行之间是不一样的。
And it's different for WebAssembly versus the command line.
但这一部分其实非常简单,几乎不值一提,反正我们都知道核心逻辑是一样的。
But that part is just so thin and relatively trivial that it's like, well, we know that all the logic's going to work the same way.
你可以说这涉及到依赖注入、抽象,或者类似的东西。
So you could call that like, well, now we're talking about like dependency injection or abstraction or this or that.
但说到底,核心想法就是把输入输出和实际逻辑分开处理。
But it's like the general idea is just like, yeah, just make it so that the IO happens separately from the actual logic.
这样我就能单独测试那些核心逻辑了。
And then I can test that actual logic separately.
这样一来,我的工作就轻松多了。
And then my life is a lot better.
确实有很多这样的情况,作为程序员,我们容易纠结于分类,比如这算不算抽象、边界在哪里之类的。
Yeah, there's a lot of things like that where I think we can get wrapped up in as programmers in like classifying things as like, well, is technically an abstraction or like an abstraction boundary or this or that.
但归根结底,关键就一个问题:做起来难不难?
But at the end of the day, it's really just about like, how hard is it to do it?
那你能换成别的做法,具体有哪些特点呢?
Like, what are the characteristics of what you could do instead?
对。
Yeah.
这又回到了我们刚才聊的函数颜色那些话题,对我来说,这根本不是什么哲学家坐在扶手椅上讨论的抽象问题。
And that goes back to the thing we were just talking about with function colors and stuff, because to me, it's not a philosophical armchair category theorist question.
这纯粹是个工程问题。
It's an engineering question.
你到底需不需要一个异步版本的标准库?
Do you need an async copy of the standard library or not?
这才是我关心的问题。
That's the question for me.
没错。
Right.
你要么对这个问题回答是,要么回答否。
And if you you either answer yes to that question or no to that question.
这正是关键所在。
That's that's the point.
对。
Yeah.
我们计划在新编译器里做这么一件事——虽然还没实现,但这就是我们想采用的设计:简化但更巧妙一些,更贴近 Go 的风格。简单说,每次你执行一个操作,具体是同步还是异步,就交给平台去决定,比如协程或者其他机制。
So something that we are planning on doing in the new compiler, this is not implemented yet, but it's it's the design that we wanna go with is is basically, do something simpler but kind of fancier and a little bit more go like in the sense of, like, essentially, whenever you do an effect, we're leaving it up to the platform, whether that's synchronous or asynchronous, like coroutines versus whatever.
你只需要说:我这段代码里的这些操作,是要按顺序执行,还是并行执行。如果是并行,你就用回调,比如一个 Lambda 表达式,明确告诉系统:这两个 Lambda 一起跑。
Essentially, you're just saying, like, look, I am saying when I run this code that these things are gonna be run-in this sequence, or if they're gonna be run concurrently, I'll say that with like a callback, you know, like a Lambda or something saying like, run these two Lambdas concurrently.
但你不用管底层 CPU 到底是怎么实现的。
But I'm not defining like what's actually going on behind the scenes in terms of the CPU.
你只管说:先执行这个,再执行这个,然后这个,接着这个。
I'm just saying like, run this and then run this and then run this and then run this.
不同的平台可以选择不同的方式来处理这些操作。
The different platforms might decide to do those in different ways.
有的平台可能用异步,有的用绿色线程,有的用别的办法,都行。
Some of them might do async, some of them might do you know, green threads, some of them might do whatever.
关键是,我们把 Rock 定义成一种语言:当你写下‘我要做这个,再做那个’,我们保证它们会按这个顺序执行,前提是平台实现没问题。
The point is just that we're defining Rock as a language where like, when you say I want to do this and do that, well, we're gonna guarantee that it's gonna happen in that order, at least as long as the platform implementation is correct.
但我们不保证它是同步的还是异步的。
But we're not gonna guarantee like, is, is this synchronous versus not?
说实话,我也不是很确定。
And I'm not really sure.
如果你在写底层代码,确实会关心这个,比如中断来了怎么办?
I mean, I guess if you're doing really low level stuff, it totally makes sense why you would care about that because you care about things like, okay, what if an interrupt happens?
在不同情况下,这差别可就大了。
Like now, you know, that's wildly different in, in some scenarios versus other.
但我们不是,这毕竟是个高级语言。
But we're not, you know, this is a high level language.
我们不打算让你去掌控这些细节。
We're not trying to like give you control over that.
Rock 里没有中断处理这类东西。
There's no like interrupt handling and rock, that sort of thing.
所以啊,我也想不明白,为什么有人会在意这个。
So yeah, I don't know why, I have not yet thought of a reason why someone might care about that.
这让我想起洛里斯了,他写了你之前提到的那篇关于异步的博客,今天他又发给我一份新草稿,想给我们计算机领域的术语里加个新词。
Yeah, that makes me think of Loris, who wrote the Asynchrony blog post that you were mentioning before, he shared with me another draft today where he's trying to introduce another vocabulary word to our computer science vernacular.
你懂吧,大家总说并发不等于并行?
You know how people say concurrency is not parallelism?
对。
Yeah.
意思就是,并发可以是单线程的。
Idea there being concurrency is potentially single threaded.
你做一会儿这个,切换一下,再做点别的,这就叫并发。
You can do something for a little bit, switch tasks, do something else.
这叫并发,但不是并行。
That's concurrent currency, but that's not parallelism.
他现在又提出第三个词:异步,或者叫异步性,看你更喜欢哪种说法。
Well, introduces a third word, which is asynchrony, or asynchronicity, depending on your preferred English.
意思是,如果你表达异步,那就是说这些操作可以乱序执行,但也可以按顺序执行。
The idea is that if you express asynchrony, you're saying that these operations are allowed to happen out of order, but they're also allowed to happen in order.
这就是异步。
That's asynchrony.
挺有意思的。
Interesting.
这和并发有什么区别呢?
How is that different from concurrency?
我可以举个例子,说明异步是适用的。
Because if you I can give you an example of something where asynchrony is valid.
明白。
Okay.
我再举个不同的例子,说明什么时候异步行不通,必须用并发。
But where, I can give you a different example where it's invalid and you need concurrency.
明白。
Okay.
举个简单的例子,比如同时写两个文件到磁盘。
Example, a simple example is just like writing two files to disk.
你有两个文件,需要分别写进去,顺序无所谓。
You just have this one and you have this one, and you need to write them to different files, and it doesn't matter.
先写哪个都行,或者同时写也没问题。
You can do one before the other, you can do it but the same time, it's fine.
所以,这段代码里可以用异步来表达。
So, you can express asynchrony in this code.
用 Zig 提出的语义,就是对这两个操作都用 async/await。
With Zig's proposed semantics, it's like async await on both of these things.
这样一来,你既可以把它放在单线程阻塞程序里运行,完全合法;也可以放在并发程序里,比如事件循环之类的,同样没问题。
And then now you're free to run those in a single threaded blocking program that's legal, but you're also free to run them in a concurrent program, a devnet loop or whatever.
这也是完全合法的。
It's also legal.
明白了。
I see.
明白了。
Okay.
所以它们仍然是顺序执行的,这跟并发不一样。
So they're still sequential, which is different from concurrent.
比如并发会说:等等,这是真的吗?
Like concurrent would say, wait, is that true?
还是说它们不一定如此?
Or they're potentially not?
不是的。
No,
它们可以被乱序执行。
they're allowed to be executed out of order.
好的。
Okay.
但它们也可以按顺序执行。
But they're also allowed to be executed in order.
但并发不也是这样吗?
But isn't that also true with concurrency?
如果我说这两件事是并发的,它们在同时运行,意思不就是说顺序无所谓吗?
Like if I say these two things are concurrent, they're running concurrently, I mean, I'm kind of saying that either order is fine, right?
我不是说顺序错有什么问题,
Like I'm not saying it's a problem if
只要它们没有按相同顺序执行就行。
they haven't executed in the same order.
但如果它们没有真正并发运行,那可能就有问题了。
But it could be a problem if they don't run concurrently.
我可以给你举个例子。
And I can give you an example.
好。
Okay.
例子是这样的:你有一个单元测试,需要创建一个服务器和一个客户端。
So the example's gonna be, you have a unit test that wants to create a server and a client.
好的。
Okay.
两个都要。
Both.
你的服务器需要监听,客户端需要连接。
So your server's gonna need to accept, and your client's gonna need to connect.
好的。
Okay.
你可以用单线程、非阻塞的方式实现。
And you can do this in a single threaded, non blocking manner.
你可以写出这个单元测试。
You can write this unit test.
但如果你只用异步来实现这个例子,程序会死锁,因为异步并不排除按顺序执行。
But if you only express asynchrony for this example, it will deadlock, because asynchrony means that you also can run them in order.
比如,如果我试着先启动服务器,
So for example, if I try to do If I try to do, yeah, server.
接受连接时,它在等客户端连上来,对吧?
Accept, it's waiting for the client to connect, right?
但下一行是客户端的连接操作。
But then the next line is like client.
连接。
Connect.
可这一步根本执行不到。
Well, it's never gonna get there.
这个单元测试要避免死锁,就得用并发顺序。
Need concurrency order for this unit test to not deadlock.
所以这在某种程度上是阻塞和非阻塞的区别。
Okay, so this is about blocking versus non blocking to some extent.
对,没错,就是这样。
Yeah, yeah, exactly.
明白了。
Okay, gotcha.
有意思。
Interesting.
对,很难说这个能不能行得通。
Yeah, hard to see if that'll stick.
这个区别非常微妙。
That's a very subtle distinction.
没错,确实很微妙,但关系到正确性,对吧?
Yeah, it's subtle, but it's like a correctness issue, right?
因为在另一个例子中,异步就够了。
Because in the other example, asynchrony was enough.
不管是阻塞还是非阻塞,都没问题。
They could run blocking, they could run non blocking, it's all fine.
但在这个例子中,要求更严格。
In this example, you have a stronger requirement.
你得在代码里明确表达出不同的要求,才能保证正确。
You have to express a different requirement in your code for it to be correct.
是啊,没错。
Yeah, right.
是啊,我在想,会不会有个术语叫‘阻塞性’之类的,我不确定。
Yeah, I wonder if, I don't know, if like the terminology blockingicity might, I don't know.
不如设计一个能解决实际问题的 API。
Well, have an API that like solves a problem.
你只需要明确要求并发即可。
So you just have to ask for concurrency.
所以我们有 async/await,还多了个 async concurrent await。
So it's like, we have async await, and then we also have async concurrent await.
你是在向系统、向执行引擎表明:这段代码不能阻塞,绝对不能阻塞。
And you're expressing to the system, to the execution engine, this can't be running, this cannot run blocking.
它必须和调用上下文并发执行。
It needs to be run concurrently with the calling context.
如果做不到,那就直接报错,说明这个执行引擎根本没法运行这段代码。
And if it can't, then like, you have to just panic and say like, this execution engine is not capable of like running this code.
对。
Right.
所以我想,这其实正好印证了我之前说的——我还没想到在RockCode里有什么地方会用到这种区别。
So I guess that's actually an example of what I was saying earlier of, I haven't thought of any examples of where this distinction might matter in RockCode.
我们目前的RockCode里根本没有这种底层操作,比如等连接之类的。
We don't have any RockCode that does that sort of thing, like that low level, like, you know, wait for a connection type thing.
但现在我得好好想想,将来会不会出现类似的情况,然后被这个问题卡住?
But now I need to go think through, like, could we have something like that in the future that might have a problem with this?
我的意思是,如果真有,那我们大不了加个类似的机制,也不一定非得做成语言原生功能。
I mean, I guess if we do, then we can just introduce a similar primitive or wouldn't be a language primitive case.
可能就是得把这段代码扔进一个Lambda里。
It would just be like a, you gotta throw this in a Lambda.
这其实算是解决这类问题的通用办法了。
It's like kind of the solution to all these problems.
不过,确实如此
But yeah, that's
简单来说,就是一种单元测试,需要在同一线程里同时处理客户端和服务器端。
what gonna put Put succinctly, it's a unit test in which you need to do both client and server in the same thread.
是的。
Yeah.
明白了。
Okay.
对。
Yes.
没错。
Right.
这种情况大概只会在测试时出现吧,我觉得。
Which only would really come up in testing, I think.
可能就像
Maybe like
我想到的另一个情况是
The only other case I can think
就是客户端和服务器之间并不连接到自己这种情况。
of where your client and server, you're not connecting to yourself.
对。
Yes.
没错。
Yeah.
那这种叫什么来着?
Is, what's it called?
OAuth。
OAuth.
如果你有个命令行工具用 OAuth,通常的做法是启动一个本地服务器,然后让用户像你说的那样,让系统打开浏览器。
If you, if you have like a command line, that does OAuth, the way that that usually works is you spin up a little local host server and then you have the user, like you said, tell the OS to open a browser.
用户可以在浏览器里用他们的 Cookie 之类的登录。
The user can log into the browser using their cookie or whatever.
然后浏览器会尝试访问本地主机。
Then the browser tries to hit local host.
因为你正好在运行那个命令行服务器,所以没问题。
Because you happen to have that CLI server running, then yeah.
这是同一台机器,但不是同一个进程。
So that's the same machine, but it's not the same process.
那这里就不会有问题。
So it wouldn't be a problem there.
对,你只需要服务器,不需要客户端,对吧?
Yeah, there you just need the server, no client, right?
在你的进程里,没错。
In your process, right.
是的。
Yeah.
你并没有自己的客户端。
You, you don't have your own client.
我的意思是,客户端是有的,但它在浏览器的进程里。
I mean, there is a client, but it's in the browser's process.
这样就没问题了。
So that's fine.
这不关你的事。
Not your problem.
对。
Right.
没错。
Yeah.
说到IO,最近我们重写Rock编译器、从Go转到Zig的一个主要原因就是,如果我们想实现Zig那种出色的缓存机制,迟早得重写一遍。
Speaking of IO, so lately I've been doing like one of the motivations for the rewrite, in Rock's compiler to Zig, was that we were going to need to do a rewrite anyway, if we wanted to get the really amazing caching design that Zig has.
我觉得我们只是稍微往前推进了一点,但幅度非常小。
And I think we've taken it a slight step further, but a very small one.
我觉得我们本质上还是在做同样的事情。
I think we're still fundamentally doing the same kind of thing.
我来介绍一下我们现在的情况——我有个还在进行中的PR,还没合并进去。
So let me describe what we're, I have a work in progress PR that's not quite merged yet.
展开剩余字幕(还有 480 条)
等大家看到这段的时候,希望那时候已经完成了。
By the time people are seeing this, hopefully it will be.
简单来说,我们有个这样的机制,嗯。
But basically the idea here is that, so we have this thing, okay.
从高层次来看,我们每个模块都是独立的。
So high level motivation, we have individual modules for us.
这算是最细粒度的编译单元了,比如在 Rust 里叫 crate,而在 Rock 里就是一个 .rock 文件,就是一个模块。
That's kind of the like most granular compilation unit you can get like in Rust it's a crate, but in Rockets, like you have a dot rock file, that's a module.
我们希望尽可能多地缓存每个模块的计算结果。
We want to be able to cache as much work as we can on that module.
比如解析、规范化、名称解析、类型检查这些步骤,我们都想缓存起来,并且用一个键来管理,一旦依赖发生变化,这个缓存就失效。
So this is basically like parsing, canonicalizations, like name resolution, type checking, all of those things we would like to cache it and then cache it under a key where that key gets invalidated if like its dependencies change.
但实际上,我们可以用两个不同的缓存。
But actually we can have two different caches.
其中一个用来存所有独立无关的部分。
One of them is like all the independent stuff.
比如解析过程不依赖任何其他模块。
So like parsing doesn't depend on any other modules.
类型检查就不同了,因为你可能导入了其他内容。
Type checking does because you could have imported stuff.
所以这两类工作用不同的键来缓存,但最关键的是,我们已经把整个编译器重写过了,几乎从不使用指针。
So those are cached under two different keys, but the really critical thing is that, so we've already rewritten everything so that in the compiler, we really rarely use pointers.
几乎所有地方都用索引代替。
Almost always use indices for everything.
我们的所有树结构都不是由指针指向其他节点的。
So all of our trees are not like trees of pointers to other things.
树里的任何一个节点引用另一个节点,都是通过索引。
Anytime a node in the tree references another node in the tree, it's with an index.
所有这些索引都是相对于整棵树的起始地址来计算的。
And all of that is relative to the initial address of the whole tree.
这样做的好处是,当你看一个单独的模块时,实际的内存分配次数是固定且很少的。
What this gets us is that if you look at an individual module, we have a fixed and pretty small number of actual allocations.
大概就20个左右吧。
Like it's like 20 or something.
具体数字我记不太清了,但每个模块大概是这样,对吧?
I forget exactly what it is, but like per module, right?
所以像所有的解析节点、各种区域,还有那些东西,反正就是这些。
So it's like all of your like parse nodes, all of your regions, of your, this, that, whatever.
这些东西的总量其实挺小的。
There's a pretty small total number of these things.
所以这让我们能做一件事,我相信 Zig 里也在做类似的操作,因为当初跟你聊的时候,我就是从那儿了解到这个想法的。基本上就是,既然内存里已经存了一堆数据。
So this enables us to do what, I believe you're doing something almost exactly like this in Zig, because this is where I heard about the idea from when was talking to you about it, is basically, okay, well, we just got a bunch of stuff in memory.
我们能做的是,至少在 Unix 系统上可以这么做,Windows unfortunately 没有好用的替代方案,但你可以直接用 preadv 系统调用。简单来说,就是构建一个指向这些数据的指针数组,然后告诉系统:第一个数据在这儿,长度是多少。
What we can do is at least on Unix systems, windows doesn't have a usable alternative to this, unfortunately, but you can just do a P write V, which is basically where you say, okay, I would like to just go grab every, create a array of pointers to all these things and say like, okay, at first here's, here's this one and here's how long it is.
第二个数据在这儿,长度又是多少。
And then here's this one, here's how long it is.
你只需要一次系统调用,把整个指针数组和对应的长度信息一次性传过去就行。
And you just, with one syscall, you say, here's, here's all these, like an array of pointers and their lengths.
操作系统会直接把这些内容连续写入磁盘,聚成一大块字节数据,挺厉害的。
And the operating system will just go write all those to disc continue to contiguously in one giant blob of bytes, which is pretty cool.
对。
Right.
一方面,相比逐个序列化这些数据,这种方式已经省了不少资源。
On the one hand, like that's, that's already a pretty significant savings compared to going through and like serializing it all.
我举个极端的例子。
So I'll use a pathological example.
假如你非要写 JSON 到磁盘,那跟我们说的这种做法比起来,简直贵得离谱。
Like imagine if you wanted to write JSON to disc instead of this, like, that's just hilariously more like expensive than what we're describing here.
只需要一次系统调用,而且准备这次调用的工作也特别简单。
It's like one syscall and preparing the, the prep work for that one syscall is also trivial.
你只需要建一个最多二十个条目的数组就行,不管你的模块里有多少数据。
You're talking about, like, make an array of, like, 20 entries total, like, no matter how much stuff is in your module.
那为什么只做到这一步呢?
Why stop there?
咱们可以把这个用 Base64 编码后通过 HTTP 传输。
You know, let's, let's wrap that in base 64 HTTP
你说得对。
and Oh, you're right.
是的。
Yeah.
我给忘了。
Forgot.
没错。
Right.
我们确实可以这么做。
We, we, we could make it Yeah.
肯定比 JSON 强多了。
Than JSON for sure.
XML 还得在每个节点上加属性。
XML with, attributes on every node.
是的。
Yeah.
所以,这样一来,数据就保存到磁盘上了。
So, So then, so that's that's got it on disk.
但真正厉害的部分是,从磁盘读取时用到了一种技术。
But then the really cool part is that reading it from disk, involves a technique.
我觉得我们这儿有点儿过度复杂了,比Zig搞得还繁琐。
So this is where I think we're being a little bit, more, I don't know, a little bit more ridiculous than necessary, than Zig is.
所以当我们从磁盘读取时,打算这么处理:先拿到一大块字节数据。
So when we read it from disk, the way that we are gonna attempt to do this is basically so we got this big blob of bytes.
直接把整块数据读进一段内存分配里。
We just read all that into an allocation.
我们就说,行了。
We're like, okay.
现在这些字节全都在内存里了。
Now all those bytes are in memory.
如果我们能直接说,好了,搞定,那就太好了。
And it would be nice if we could just say, okay, great.
只要把这块内存的开头强制转成那个主结构体,一切就都解决了。
We'll just cast the beginning of that to the main struct that's got all these things and we're done.
但显然这行不通,因为如果你直接把结构体转成模块环境,问题又回来了。
But of course that won't quite work because if you just cast the struct to like, okay, now we have the module environment, it's back.
那些地方还留着一些指针。
Well, were still some pointers there.
大概有二十个之多。
There were like 20 of them.
它们全都指向我写入时内存里随机的地址。
And so those are all pointing to addresses of like whatever happened to be in memory when I did the write.
所以这方法行不通。
So that's not gonna work.
因此我们现在改用两端同时做修复的方式。
So what we're doing instead is we're doing a fix up on both sides.
所以当我们写入的时候,实际上是把每个地址都换算回原始地址。
So basically when we do the write, what we're doing is we are normalizing each of those addresses to the original address.
换句话说,我们把地址转换成了偏移量。
So in other words, we're kind of converting the addresses to indices.
比如说,我在偏移量10的位置,我在偏移量30的位置,我在偏移量50的位置。
So it's like, oh, I'm at like offset 10, I'm at offset 30, I'm at offset 50.
这些并不是真正的指针地址,但也不需要是。
Those aren't real pointer addresses, but they're not supposed to be.
它们只需要保持一致,方便使用就行。
They're just supposed to be consistent and like helpful.
等你反序列化的时候,再反过来把这些值还原成真正的地址。
So then after you deserialize them, you basically go through and fix them up in the other direction.
比如说,某个指针在序列化时记录的是30,但我知道这一整块内存的实际基地址是7920左右。
So you say, okay, well, this pointer was written down as 30 when it was serialized, but I know that my actual base memory address for all of this is like, you know, 7,920 or whatever.
那只要把30加上去,就是真正的地址了。
So just like add 30 to that and that's the address.
所以现在我们又把这些全都恢复了,就靠遍历这二十个指针,做点简单的算术运算,就这么简单。
And so now we've rehydrated all of those again, just by going through these like 20 pointers and just doing some arithmetic and that's it.
现在好了,所有东西都重新加载到内存里了。
And now poop, we have everything back in memory.
我这里省略了一个小细节:写入的时候,其实得考虑对齐问题。
There's a little bit like a detail I'm omitting there where like, when you do the right, you have to like account for alignment.
不过 apparently 也许不用,Daniel Lemire 刚刚写了一篇博客,说在现代系统上,非对齐的读写根本没问题。
Although apparently maybe not, The Daniel Lemire just wrote, a blog post about how like apparently like on modern systems, being like misaligned reads and writes are like fine.
居然没有性能惩罚,这让我挺惊讶的,不过算了。
Like don't actually have a per penalty, which is wild to me, but, whatever.
我们还是这么做,主要是因为开销几乎可以忽略,而且应该也没问题。
We're doing it just to be, you know, because the cost is minimal and it's probably Okay,
等一下。
hold on.
我这边有几个问题想问一下。
I got a, I got a bunch of questions about this.
好的,没问题。
Okay, sure.
说吧。
Go for it.
你是不是用了一个专门基于索引而不是指针来管理底层内存的哈希表?
Are you, are you using Do you have like a specialized hash map that operates on like indices instead of pointers for its backing memory?
还没有。
Not yet.
哦,你是说想用这个?
So that's like Oh, you want
不过有这个打算吗?
to though?
那是什么?
What's that?
明白了。
Okay.
嗯,听起来
Well, it sounds
你好像有计划。
like you're planning to.
我们目前只用哈希表来处理字符串驻留之类的事情。
We're only using hash maps for things like string interns and stuff like that.
其实我们对哈希表的使用非常少,而且我打算为字符串驻留搞点更高级的方案。
We actually have very minimal use of hash maps and I actually wanna do something way fancier for string interns.
那咱们先稍微岔开话题,聊聊这个。
So let's just go on a micro tangent about that.
我有个想法,觉得可以用 SIMD 技术让字符串驻留的速度提升好几个数量级。具体来说,就是针对标识符的驻留来做优化。
I have this hypothesis that we could use SIMD to make string interning just like astronomically faster by basically being like, okay, so that we're interning identifiers, right?
比如变量名,像 X、AB,或者 Foo 之类的。
So you have variable names like X or AB or like Foo or whatever.
我的假设是,如果你去观察这些标识符的分布情况,你会发现其中有很多是重复的。
My hypothesis is that if you look at the distribution of those, you have a lot of them.
好的。
Okay.
可能一个字母的没那么多,但这类字母确实挺常见的,尤其是在类型变量名里。
Maybe not that many that are like one letter, but that one letter does come up decently often still, especially in like type variable names.
不过在Rock里,说不定情况没这么明显。
Maybe that won't end up being true in rock, whatever.
然后是两个字母、三个字母、四个字母,以此类推。
But then like two letter and then three letter and four letter, etcetera.
尤其是那些短的,其实Rock里本来就习惯用缩写来命名。
Well, if you look at, especially the short ones and we do actually have a culture in rock of like trying to name things using abbreviations.
比如,我们不写string,直接写STR——这话题又跑偏了。
So for example, instead of string, we say STR, micro tangent off the micro tangent.
我觉得这样挺好的。
I think this is good.
有些人一看就摇头,说不喜欢这样。
Like, some people look at that and they're like, oh, I don't like that.
我觉得你还是把整个单词写出来比较好。
I think you should just like write out the whole word.
像 string 这种常用词,根本没必要缩写,首先,这完全是没事找事。
I think you should not bother writing out the whole word for things that are really commonly used like string because A, like it's just a solution in search of a problem.
你有没有见过有人真用这种系统?
It's like, have you ever seen people use a system like this?
实际使用中根本没人觉得有问题。
Nobody has a problem with this in practice.
这根本就不是个问题。
It's just a total non issue.
你天天用,大脑自然就适应了。
You're just, your brain just adjusts to it very quickly when you're using it all the time.
对于不常使用的词,写全称才更合理,因为你如果不常接触,根本不知道它代表什么。
It makes more sense to me to write out something longer just because you're not using it, if you're not using it frequently, then you just might not know what it is.
你根本不知道它展开后是什么。
Like you're not, you don't know what the expansion is.
但像 str、int、bool 这种特别常见的,其实吧,哦,又绕回那个小话题的小小分支了。
But for really common stuff like str, int, bool, you know, actually, okay, micro tangent off the micro tangent off the micro tangent.
bool,b-o-o-l,这得感谢 Brian Hicks 指点我,bool 是来自布尔逻辑,而布尔逻辑又是以 George Boole 命名的,原名是 b-o-o-l-e,所以我们其实是把 b-o-o-l-e 简化成了 b-o-o-l,就多一个 e 容不下似的,你懂吧?
Bool, b o o l is like this is shout out to Brian Hicks who pointed this out to me b o o l is named after boolean logic but boolean logic is named after George Boole b o o l e so really like we're abbreviating b o o l e to b o o l just like couldn't fit that e in there, you know?
对啊。
Right.
为了纪念 George Boole。
For George Boole.
总之。
Anyway.
太惊人了。
Incredible.
好吧。
Okay.
把这个小插曲打住。
Popping that microteam
他说,我差点就出名了。
He's like, I was almost famous.
就差一点。
So close.
是的。
Yeah.
你可以想象一下,要是乔治·布尔复活了,发现自动纠正总想把他名字末尾的那个 e 删掉。
You can imagine, like, if you're George Boole, like, you know, zombie George Boole comes back to life and like auto correct is always trying to drop the e off the end of your name.
你肯定会说,
You're like,
别闹了。
come on.
对啊。
Yeah.
真的假的?
Like really?
我的名字就不能再多留一个字母吗?
You couldn't take one more letter from my name?
比如说,
Like,
好吧。
all right.
没错。
Right.
总之,因为这个原因,我觉得我们以后会冒出好多三四个字母的变量名。
Anyway, so because of that, I think, you know, we're gonna have a lot of like three and four letter variable names.
所以基本思路是,字符串驻留嘛,我们只需要查一下这个字符串对应的整数 ID 是多少。
So the idea is basically like, okay, string interning, we just need to go look up like, what's the, you know, the ID, like an integer ID associated with this thing.
当然可以用哈希表,但我猜如果直接按常见长度分组,用不同数组来存,速度会快很多。
You could put it in a hash map obviously, but my hypothesis is that it'll actually be a lot faster if we just have different arrays of, for the different bucket sizes, of, of really common, like lengths.
比如长度为一、二、三、八这些常见的。
So like one, two, three, eight, etcetera.
然后你用 SIMD 一次性快速处理这些,迅速找到索引。
And you just blast through those in SIMD and just check really fast and find the index.
这就是我的推测。
That's the hypothesis.
所以如果你有像——
So if you've got like-
等等,这些桶也是各自做成哈希表吗?
Wait, wait, so are, are these buckets also like hash maps each or?
不是,简单说,我举个例子吧。
No, it's just like, well, so, okay, let me give you an example.
比如,我们设一个桶专门存长度为四个字节的标识符,对吧?
So let's say we've got a bucket for, identifiers of length four bytes, right?
比如 '33' 就不太合适,因为 SIMD 只能按 2 的幂次对齐,得补零。
So like, no, three three is a worse example, but just because it's, you have to have like a zero padding because like SIMD only works in, you know, powers of two.
明白了。
Okay.
所以是四个字节的长度。
So length four.
我们这边有一些标识符,名字嘛,随便吧。
We, we, so we've got some identifier names of, I dunno.
我现在在想,常见的四个字母的标识符有哪些。
Now I'm trying to think of like, what's a common four letter identifier, form.
我也说不准。
I dunno.
比如有个标识符叫 form。
So you have an identifier named form.
旁边还有另一个。
And then next to that you have like another one.
再旁边又有一个。
And next to that you have another one.
然后还有一个叫 bool。
So let's say, then you have another one called, yeah, bool.
为什么不呢?
Why not?
当然可以。
Sure.
这个不错。
That's a good one.
这是个不错的四个字母的名称。
That's a good four letter one.
那我们来想想,bool 的字符串内部 ID 是多少?
So we're like, okay, what's the, what's the string intern ID of bool?
我们只需要去那个数组里,用 SIMD 来查找。
So all we do is we just go into that array and we're just like using SIMD.
我们就是想找找这个数组里哪个位置是 B O O L。
We're like, like, let me just find the index in this array of like, which one has B O O L in it.
用 SIMD,我们可以一次处理 16 个字节,对吧?
Using SIMD, we can do this 16 bytes at a time, right?
所以我们一次可以看四个这样的字符串。
So we can look at four of these at a time.
而且因为我们是按顺序遍历的,那个叫什么来着?
And because we're proceeding sequentially through it, the, what do you call it?
预测器,就是缓存预测器,对。
Predictor, the cache predictor Yeah.
这样缓存的表现应该会很不错。
Like, it'll the cache behavior should be pretty good.
而且这些字符串几乎都会常驻内存,因为我们一直在做字符串驻留。
Plus also, like, all these are gonna be really hot because we're gonna be interning all the time.
所以它们本来就很可能已经在缓存里了。
So they're have a decent chance of being in cache anyway.
这是每个函数都单独有一份吗?
Is this like per function?
这些数组一般有多大?
Like how big are these arrays?
这是按模块来的。
This is per module.
每个单独的文件都有自己的一套内部标识符。
So like an individual file would have its own intern, set of interns.
也就是说,这些就是该模块里的所有标识符。
So this would be all the identifiers in that module.
我们想把这些标识符压缩一下,把原本可能好几字节的长度,统一缩到四个字节。
We want to turn them, crush them down and from like, you know, however many bytes they are into like, four bytes.
说起来,我突然想到一件事。
Actually, you know what?
既然说到这了,我们还可以对特别短的标识符做点优化。
Now that I say this, like another optimization that we can do is for really small ones.
如果用的是四字节的标识符,其实可以直接把它们嵌进我们已有的32位空间里,就像字符串的小缓冲优化那样,直接存进去就行,不过这事儿先放一边。
If we're using four byte identifiers, we can actually just like inline those into the 32 bits that we've already got, like the small string optimization and just like store them right in there, But that's neither here nor there.
但对那些比较长的,比如八字节、六字节之类的,我们可以按长度分桶,这样就不需要去八字节的桶里找长度为15的标识符了。
But I guess for yeah, so for like longer ones, like, I don't know, eight bytes or six bytes or whatever, because we can put them into separate buckets based on their length, like we don't need to bother searching for something that is, for example, has the length of like 15 in the eight bucket.
这些可以直接分到不同的桶里,没必要在同一个桶里浪费时间查找。
Those can just be in separate buckets because there's no point in like searching the same bucket for that.
这一切都基于我们在旧版 Rust 编译器里观察到的现象——当初我们到处都用哈希表,后来试了叫 VecMap 的东西,其实就是把所有东西塞进一个线性数组,每次直接遍历查找。
All of this is based on the observation that we saw in the old Rust compiler where we originally were using hash maps all over the place And then we tried out using what we were calling VecMap at the time, which was basically just like put everything in like a linear array and just search every time.
结果发现,这比反复哈希快太多了,真让人没想到。
And it was just like way faster than doing all the hashing, which was really surprising.
这确实挺意外的。
Oh, that's surprising.
是啊。
Yeah.
我当时也特别惊讶。
I was very surprised.
当时那些测试数据是 Folker De Vries 做的,他做的 benchmark。
It was like, and the numbers that, this this is a Folker Devries was, was doing these benchmarks.
他太厉害了。
He's awesome.
他特别惊讶,因为得要几十万条数据之后,HashMap 才会快起来。
He was shocked by like, it was like, you needed to get like hundreds of thousands of entries before like the HashMap one.
这完全违背了所有人的预期。
It was like a totally, really defied all like expectations.
不过话说回来,如果这个结论在当前这个编译器里也成立,那可能之前那个老编译器里有什么别的因素影响了内存使用,这种可能性也不是没有。
But anyway, so, so if that holds true in this compiler as well, you know, I don't know, maybe there was some like weird mitigating factor of like all the other memory usage that we were having in that old compiler, which is always possible.
但我的猜测是,我们这个新版本的编译器,可能根本不需要用真正的哈希表。
But the hypothesis is that actually we won't need to use actual literal hash maps, I don't know, maybe at all in this version the compiler.
到时候就知道了。
We'll see.
对了,Zig 的数组哈希表有个线性阈值,
By the way, Zig's array hash map has a linear threshold,
我不太清楚,
I don't know
你要是看过的话,就会发现,当数据量低于那个阈值时,它就直接用数组,完全不哈希。
if you saw that, where below that amount, it's literally just an array with no hashing.
我还不知道呢。
Didn't know that.
这挺有意思的。
That's cool.
它就是这么做的。
It just does that.
不过我觉得我只设成了一条缓存行的大小。
But I think I only set it to be one cache line size.
也许根据你的发现,我们应该考虑调大默认值。
Maybe we should look into a bigger default, I guess, based on your findings.
不妨试试相关的实验。
Might as well try some experiments of that.
你也可以这么做。
You can also do that.
如果你在用这个类型,可以试着在标准库里把那个数值调高一点,看看效果会不会更好。
So if you're using that type, could just try bumping that number in the standard library and see if you get better results.
没错。
True.
对,我们可以拿这个做点实验。
Yeah, we could experiment with that.
有结果了告诉我。
Let me know.
是的,我相信我们会的。
Yeah, I'm sure we will.
不过话说回来,核心想法其实就是,如果你想想看,一个大型的 Rock 项目在构建上会是什么样子,也许你会觉得,这种优化根本没必要,毕竟现在根本没人有这么大的 Rock 项目。
But anyway, so the basic idea is just that like, if you think about what is a larger rock project gonna look like in terms of like builds and, you know, this might seem like a silly thing to optimize for when we're like, nobody has a large rock project.
就我所知,目前最大的生产环境 Rock 项目也就几千行代码左右。
Like the biggest like production rock projects that I'm aware of are like thousands of lines of code.
说实话,我都不确定这个项目现在有没有达到一万行代码。
Like, I don't even, I don't even think this one's like ten, ten thousand lines as far as I know yet.
但我们还处在 0.1 版本之前呢。
But also we're like pre O dot one.
所以呢,这就是我们的救命稻草。
So, you know, that's our that's our get out of jail free card.
我们计划在Zig编译器基本搞定之后,就发布0.1版本。
We are planning on launching O dot one, like once we get the Zig compiler, like basically, you know, there.
估计不会到2025年底,但我希望能在2026年上半年,最好是第一季度,具体还得看情况。
That probably will not be end of twenty twenty five, but I'm hoping it will be like 2026, like first half of ideally first quarter of, we'll see.
在我看来,一门编程语言的责任,就是给使用者良好的体验。
But basically like the way that I look at, a programming languages responsibility is to give people a good user experience.
随着项目越来越大,依我的经验,一切都集中在编译速度上。
As your project gets bigger, in my experience, like the UX, just everything becomes about compile times.
如果编译太慢,其他什么都白搭。
If your compile times are slow, it doesn't matter.
因为每次构建你都像在受罪,整个过程痛苦不堪。
Like nothing else about the language matters because you're just in total agony every time you do a build and like everything's miserable.
所以我们得提前考虑好,绝不能让这种情况发生,不然其他所有努力都没意义。
So I'm like, we need to anticipate that and make sure that never happens to us or else all the other work that we do didn't matter.
人们现在还是过得特别糟。
Like people are still having a terrible experience.
所以我特别关注怎么确保我们不仅能增量缓存,而且从缓存加载的时候速度够快。
So I focus a lot on like, how do we make sure that like, not just are we able to cache things incrementally and whatnot, but also when you are loading from cache, how fast is that?
比如你有个超大的项目,有上百万行ROC代码,但你大部分时候只是改了这个文件或那个文件再重新编译,这时候99.9%的时间其实都在从磁盘读缓存,复用之前的东西。
Like if you have this gigantic project with like, you know, a million plus lines of ROC code, but almost all of what you're doing is like, you're making a little change to this file or that file before you rebuild, then 99.9 of what you're doing is loading cache files from disc, like, you know, reusing that stuff.
所以当项目大到你开始在意性能、能明显感觉到差别时,这个环节的性能就至关重要了。
So like this, that performance is extremely critical as your project gets big enough where you care about that performance and can notice it.
所以,
So,
缓存命中时的性能。
um- The performance of cache hits.
对,就是这样。
Yeah, exactly.
到那时候,编译器主要干的就是这件事。
That's like, that's like mostly what the compiler is doing at that point.
至少在当前讨论的这些阶段,有些部分你每次都得重新做。
At least for the stages in question, like some parts of it, you, you have to redo every time.
据我所知,这些没法做到增量处理,比如链接这一步。
There's no way to make them incremental that I'm aware of, like linking for example.
不过,好吧。
But well, okay.
明白了。
Okay.
这其实牵扯到另一个话题——我们目前还没实现,但将来可能会很惊艳。
That does, that does get into a separate topic of like something that we are, this doesn't work yet, but, it's gonna be kind of wild.
而我刚才说的这些,正是实现它的前提。
And actually the thing that I just explained is a prerequisite for it.
所以,好吧。
So, well, okay.
在聊这个之前,我也不太确定。
Before we get into that, well, I don't know.
你对这些怎么看?
What do you think of all that?
这和 Zig 在实际生产环境中已经做的事情有多契合?
Like how does that line up with what, what Zig is already doing in production for real?
你抛出了太多让人想接话的话题了。
You you created so many conversational doorknobs.
我都搞不清楚该从哪个开始聊了。
I don't even know which one I wanna go in.
我简直像《瑞克和莫蒂》里那个试所有门把手的人。
I'm gonna be like that scene in Rick and Morty with the guy trying all the doorknobs.
那我们先从链接说起吧。
Well, let's start with linking.
我觉得这个话题挺有意思的,因为过去五年左右,大家又开始特别关注链接器的性能了。
So I think that that's quite an interesting topic because we've seen this resurgence of focus on linker performance in the last, I don't know, five years.
我们有来自 Rui 的 Mould。
We had Mould from Rui.
我好一阵子没关注他了。
I haven't checked in on him in a while.
他最近怎么样?
How's he doing?
苹果好像就是针对这个,直接自己搞了一套,现在他们的链接器更快了。
Had Apple seemingly, like, in response to that just did their own thing, and now their linker is faster.
但这些项目都基于一个前提:链接器就像个函数,输入目标文件,输出可执行文件。
But all these projects, they rely on the premise that a linker is like a function that takes objects as an input and then an executable as an output.
但非得这么搞不可吗?
But it doesn't have to be that way, does it?
如果你从整个工具链的角度来看,比如你输入一个 Zig build 命令,这本身就包含了编译和链接,那我根本不用等所有目标文件都生成完就能开始链接了,对吧?
Because if you are thinking about the tool chain together, if you give me like a Zig build exe command, that's compilation and linking, which means that I can start linking immediately before I have I don't even Why do I need objects?
目标文件其实只是在把链接器当成独立函数来用的时候才需要的东西。
Objects are something that you only need if you're doing this, like, the linker is a function way of modeling it.
所以,当你从一开始就考虑链接的时候……
So, like, if this, when you start linking.
我澄清一下,你说的‘对象’,是指目标文件吧?
To clarify, when you say objects, you mean like, object files.
就是那种二进制文件,存放了一堆可执行代码和相关数据的文件,你知道的。
Like, the the binary, like, representation of, like, a bunch of executable stuff, you know, and and and related data on disk.
是的。
Yeah.
对。
Yeah.
ELF目标文件、COFF目标文件、Mach-O目标文件,还有Wasm。
Elf objects, cough objects, macho objects, Wasm.
既然编译器和链接器都是Zig自己做的,那就可以直接开始链接。
So if you're the compiler and the linker, which in Zig's case we are, then you can just start linking immediately.
我一看见main函数,就马上生成它的机器码,然后直接写到输出文件里。
I look at the main function, I generate machine code for the main function, I can just write that to disk in the output somewhere.
只要后面在函数表里正确指向它,就没问题。
And then as long as I later point to that correctly in the table of functions or whatever, it's going to be good.
这样肯定能行。
It's going to work.
所以如果你不假设那些目标文件已经提前准备好了,整个思路就完全不一样了。
So the equation's totally different if you don't assume that your objects are already pre baked for you.
对,这真的很棒。
Yeah, which is really cool.
我们当初在编译器的原始版本里尝试过自己做链接,现在反而用了一个更简单、但在某些方面也更激进的方法,这个以后再聊。
I mean, we ran into, we tried to do our own linking in the original version of the compiler and now we're doing something that's actually a lot simpler, but also a lot wilder in some sense, which we can talk about later.
不过你说得对,关键就在于你得完全掌控整个流程,因为我们之前搞那个精准链接时特别难,就是因为要处理别人提供的第三方目标文件,它们很多都已经完成了重定位。
But yeah, like that that last part that you mentioned about, like, as long as you're in control of it, right, is is really critical because the pro the thing that made what we were trying to do with the surgical linker so hard is that we're taking arbitrary third party objects that already have their relocations done in a lot of cases.
所以我们得去调整这些文件,重新处理重定位,那简直太复杂了。
And so we had to go like, try to tweak all those and relocate the relocations and that got really hard.
可以做手术。
Could do surgery.
对。
Yeah.
说实话,我们确实在 Linux 上把这东西搞定了。
Mean, and actually, I mean, to be fair, we did, you know, get it working in, in Linux.
得感谢布伦丹·汉斯金克把这功能实现出来,后来沃尔克又把它移植到了 Windows 上,也成功跑起来了。
Shout out to Brendan Hanskinek for getting that working and then Volker, ported that to Windows and got that working.
但一到 Mako 上,就完全搞不定了,我们根本没能把这事做成,因为 Mako 的做法实在太另类了。
But then as soon as we tried to do it for Mako, it was just like a total I mean, we we never got it across the finish line because Mako was just so totally out there with how they do everything.
天啊,苹果干嘛非要把自家平台的开发搞得这么麻烦?
Man, why has Apple gotta make development so unnecessarily difficult on their platform?
我觉得这可能是某种历史遗留问题,我还没去查过具体背景,但感觉像是从 NextStep 时代或者刚过渡那会儿留下来的规矩。
You know, I suspect that's one of those things that probably I guess I haven't looked up the history of this, but seems like the type of thing that was probably from, like, the next step era or, like, the transition out of that or something like that.
这玩意儿可太老了,得有好几十年了吧。
It's, like, some really, really old, like, you know, multiple decades.
那时候,大家其实都搞不清楚自己在干嘛。
Like, at the time, it seemed like, like, nobody really knew what they were doing.
所以嘛,也就只能接受了。
So it's like, sure.
咱们不如就这么办?
Why don't we do it this way?
结果呢,咱们又回到原点了。
And then it's like, oh, here we are.
向后兼容性的问题。
Backwards compatibility.
我们只能认了。
We're stuck with it.
是的。
Yeah.
我觉得肯定开过会讨论过。
I feel like there had to be a meeting.
就是那个梗,有人提了个合理建议,结果被扔出大楼了?
You know, that that that meme with the guy being, like, thrown out of the building for suggesting a reasonable idea?
肯定有个人当时提议过:咱们就用 ELF 吧,行不行?
There had to be that person who suggested, Can we just use elf, please?
当时就是那样,
It was like,
把他直接扔出去了。
Toss him out of Yeah.
不过算了,好吧。
The But anyway yeah.
所以,行吧。
So okay.
所以那会儿是链接的问题。
So that was linking.
对。
Right.
你对链接还有什么别的想法吗?
Did you have any other thoughts on linking?
还是想从之前那堆选项里换个别的?
Or do you wanna pull out a different doorknob from the earlier menu of them?
嗯,我确实还有几点关于链接的想法。
Well, yeah, do have a couple more thoughts on linking.
其中一个有趣的观察是,如果你采用我之前说的这种增量链接方式,也就是一边写新函数,
So one interesting observation is that if you start to do this incremental linking approach that I outlined, where you're trying to You want to add functions.
一边同时做编译和链接。
You want to do compiling at the same time as linking.
当你完成一个函数的机器码生成后,就可以立刻把它传给链接器,直接写入可执行文件。
So as you finish generating the machine code for a function, you can then send it to the linker, and you can put it in the binary.
还有另一个角度:编译完成后,如果你修改了某个函数,想重新编译,
There's another angle to this too, which is after that compilation completes, now I make a change and I want to recompile.
那如果你只改了一个函数,就只需要重新编译这一个,生成新的机器码,然后直接替换二进制文件里对应的那部分。
Well, if I only change one function, then what you should be able to do is recompile that one function, generate the new machine code, and then just go edit that one function in the binary.
如果新代码变长了,需要挪位置也没关系,换个地方放,再更新所有调用它的地方就行。
And if you have to put it somewhere else because it got longer, that's fine, put it somewhere else and then update the calls to it.
这完全是
This is And totally
关键是,你知道这些位置都在哪儿。
crucially, you know where all of those are.
你没必要把整个东西翻一遍去找它们,因为你可以把这些信息直接记录在内存里的某个元数据里。
You don't have to go scan the whole thing looking for them because you can just have those written down in some metadata in memory somewhere.
对,你要跟踪这些数据。
Yeah, you track that data.
对,你要跟踪这些数据。
Yeah, you track that data.
所以最终你得到的,其实就像一种内存分配机制,对吧?
And so what you end up with is this kind of like memory allocation, isn't it?
这其实就是函数的内存分配。
It's just function memory allocation.
这和内存分配器面临的问题完全一样。
It's the same problem as a memory allocator.
有意思的是,这和热代码替换的问题有99%是相同的。
What's interesting about that is that it's 99% the same problem as hot code swapping.
没错。
Right.
因为你只需要像在磁盘上那样操作,但在内存里用虚拟内存来实现,通过一个系统调用就能直接修改任何进程的内存。
Because all you have to do is do the same thing you did on disk, but also in memory with VM, like a syscall you can do that just lets you arbitrarily edit the memory of any process.
没错。
Right.
所以你可以暂停一个正在运行的程序,然后在内存里直接做和磁盘上一模一样的链接操作。
So you can just pause a running executable and then just go do, like, literally the same linking that you did on disk, but in memory.
接着再恢复程序运行,瞬间你的新代码就生效了。
And then unpause the executable and then, like, boom, your new code.
下次调用这个函数时,就会自动执行新版本了。
Next time that function's called, it'll call the new version of that function.
是的。
Yeah.
我看过相关说法,做这种操作时得特别小心杀毒软件,因为这种行为看起来和恶意攻击几乎一模一样,尽管你只是想给开发者提供更顺畅的开发体验。
I have read that you have to be really careful about antivirus when you do that because apparently doing that sort of looks a lot like you're trying to do something malicious even though you're just trying to give people a nice software development experience.
是的。
Yeah.
但据说——这让人
But apparently- That makes
我特别生气。
me so angry.
对。
Yeah.
我就想问,我连点个按钮都不行吗?
It's like, man, can't I press things?
状态就是这样。
The state.
我知道。
I know.
对。
Yeah.
是的。
Yeah.
不过话说回来,我觉得还是有一些解决办法的,比如只要确保内存页面在任何时刻都没有同时被标记为可执行和可写,那就没问题。
But having said that, I do believe there are some workarounds you can do such as, there was something about like, if you can make sure that the, the page of memory is not marked as even in any, like at any point is not marked as both executable and writable, then you're okay.
你可以先暂停它,取消它的可执行权限。
So you can do like, pause it, make it not executable anymore.
然后再把它设为可写。
Now make it writable.
明白了。
Okay.
往里面写数据。
Write to it.
好的。
Okay.
再把它恢复原状。
Put it back.
你知道,它是可执行的。
You know, it's executable.
我不太清楚。
I don't know.
我觉得可能是杀毒软件
Like, I guess the antivirus
就是这么做的。
just do that.
对。
Right.
我真是搞不懂。
I I don't get it.
不过呢,这也就是我之前看到的说法。
But again, this is this is just something I read.
但我自己还没实际操作过。
I haven't actually gotten this, yet.
我明白了。
I see.
对。
Yeah.
我不清楚。
I don't know.
我的看法是,如果你的杀毒软件干扰了你的开发工作,那说明你自己的能力有问题。
My philosophy is, like, if your antivirus software is, interfering with your software development, that is a skill issue.
把它关了。
Turn it off.
你在干什么?
What are you doing?
你打算下载病毒吗?
Are you going to download a virus?
别这么干。
Don't do that.
等到杀毒软件发现病毒的时候,已经晚了。
Catch If a virus with antivirus software, it is too late.
这根本不是解决这个问题的正确方法。
That's the wrong solution to the job.
是的,有道理。
Yeah, that's fair.
说实话,我从小孩时候之后就没再用过杀毒软件了,可能吧。
I mean, I haven't used antivirus software since I was like a kid, maybe?
我不确定。
I don't know.
估计都二十年了。
It's been like twenty years probably.
我电脑里肯定有不少病毒,但据我所知,还没出过什么大问题。
And I'm riddled with viruses, but I don't think I have been as far as I know.
不过谁知道呢。
But who knows?
我的意思是,我也在想,杀毒软件这个概念是不是已经过时了?毕竟早期互联网时代,JavaScript其实起了很大作用,因为它成功实现了沙盒隔离。
I mean, that also I I do kinda wonder to what extent antivirus software as a concept is just kind of outmoded in the sense that in the early days of the Internet before I actually think JavaScript gets a lot of credit for this because JavaScript is actually successfully sandboxed.
比如你访问一个网页,运行里面的JavaScript代码,我从来不会担心会中病毒。
Like when you visit a webpage and you run JavaScript code on it, I don't have any fear that I'm gonna get a virus.
到目前为止,我也确实没因此出过问题。
And so far I've never been burned by that.
理论上当然可能存在零日漏洞,但我没听说过哪个造成了真正重大的影响。
In theory, there could be zero day exploits, but I am not aware of any that have had any significant impact on the world.
现在这么多软件都在浏览器里运行,而不在浏览器里运行的,通常都是你特意下载的,而且几乎都来自可信来源。
So that just means that because so much software runs in the browser in practice these days, and when you're not running in the browser, it tends to be something that's you're very specifically downloading and almost always from some reputable source.
所以,感染病毒的风险面比以前小太多了。
Then, I don't know, the risk of getting a virus is the surface area is just so much smaller than it used to be.
我不禁想,那些非程序员的人,现在还真的有那么大风险中病毒吗?
I wonder why, you know, even people who are not programmers, are they at serious risk of getting viruses still?
也许有,只是我还不了解而已,不过……
Maybe they are, and I'm just ignorant of that, but
我的意思是,杀毒软件从诞生起就注定失败,因为一旦中了病毒,唯一靠谱的解决办法就是重装系统。
I mean, the software Antivirus software was dead on arrival because if you have a virus, the only, like, security acceptable solution is to reformat your computer.
你不可能简单地删掉一个病毒就完事了。
Like, you can't you you one does not simply delete a virus and then be done with the virus.
你明白我的意思吧?
You you see what I mean?
你已经彻底被攻陷了。
Like, you're compromised.
你得删掉所有私钥,重新生成新的。
Like, you have to delete your private keys and make new ones.
你得重装系统。
You have to reformat your computer.
你得把所有账号的密码全部换掉。
You have to change all your passwords to everything.
你已经没救了。
Like, you're done.
你完蛋了。
You're cooked.
你不可能光靠删个病毒就完事了。
Like, you can't just delete a virus.
而且我都不确定重装系统就够不够,不是有些病毒能侵入固件吗?
Well, and I I don't even know if reformatting is enough because aren't there some viruses that can infiltrate the firmware?
天哪。
And so you Oh, jeez.
是啊。
Yeah.
对吧?
Right?
我听说过这种事。
I've heard of this.
我没亲身体验过,但我知道有这种说法。
I have not, I don't have firsthand experience
这不太常见。
That's unusual.
据我所知是这样。
That I'm aware of.
这就好比是硬件或者操作系统层面的逃逸,就像突破今天的 JavaScript 沙箱那种感觉。
I mean, that'd be like the hardware or operating system equivalent of, like, escaping, like, the today's JavaScript sandbox or something.
是的。
Yeah.
我觉得这在理论上是可能的,但非常罕见。
But I, and I think it's Theoretically possible, but very rare.
对。
Right.
我想这就是为什么现代系统会有安全启动这类机制,就是为了防止这种事情发生。
I guess this is why modern systems have like secure boot and stuff like that is to try to prevent things like that from happening, I think.
这确实超出了我自信能完全理解的范围,不过
Definitely getting out of my comfort zone of things I feel confident that I actually understand, but
是的
Yeah.
那是他们的借口,但其实这也是数字版权管理的一部分。
That's their excuse, but then it's also a DRM thing.
主要是数字版权管理的问题。
It's mainly a DRM thing.
有道理。
Fair.
我想这背后有很多因素
I guess there's a lot
需要考虑。
of things to
关于这一点。
about that.
这和沙盒机制不一样。
It's different than sandboxing.
是的。
Yeah.
对。
Yeah.
好的。
Okay.
不过我们大概可以把这个从栈里移除。
Well, we could probably pop that off the stack though.
好的。
Okay.
行。
All right.
所以这跟链接
So that's linking
和杀毒软件有关。
and antivirus.
链接这个事可深了。
Linking goes deep.
是的。
Yeah.
对。
Yeah.
好。
Okay.
那还有别的什么事来着?
So what was one of the other things?
我们之前聊过 P 写 V 这类东西,还有读回来做修复的操作。
So we talked about the like P write V and stuff like that and then reading back in and doing fix ups.
其实就是重定位,比如调整一下你的重定位信息,嗯,在
Just kind of relocations in a sense, like tweaking your relocations Well, in a
我问你个更宏观的问题。
let me ask you a more high level question.
你决定用 Zig 重写 Rock 已经有几个月了吧?
It's been like a few months now since you decided to do some rewriting in Zig Yes.
是 Rock。
Of Rock.
进展怎么样?
How's that going?
现在做到哪一步了?
What what's the progress?
有没有遇到什么棘手的问题想聊聊?
And, is there any pain points that you wanna share with?
当然有。
Sure.
进展非常顺利。
It's been going great.
对这个决定特别满意。
Very happy with the decision.
对目前的进展非常满意。
Very happy with how everything's been going.
反馈循环快了很多,这也是我们最看重的点之一。
The feedback loop is a lot faster, which is one of the main things that we were hoping for.
虽然大家都在 ARM 64 Max 上开发,连最新的、像 x64 用户那样超快的特性都用不上。
Even though we're all doing development on ARM 64 max, so we're not even getting the latest, like the really fast stuff that the x64 Zig users already get to use.
说实话,我最喜欢 Zig 的一点是,它会明确告诉你编译器在做什么。
Honestly, like the main thing, and I love that Zig does, tell you what the compiler is doing.
所以基本上,每当我跑那个构建和测试的循环时,你懂的,我看到的总是 LLVM 对象生成这一步。
So basically whenever I'm running my loop of like, you know, rebuild and retest, always what I see is emitting LLVM object.
对吧?
Right?
然后就像,好吧,
And then like, okay,
行吧。
all right.
我们现在在等LLVM。
We're waiting on LLVM.
这个情况我们早就知道了。
We're familiar with this.
我注意到了。
I noticed that.
等那一步完成后,我就只能等着测试真正运行了。
And then after that's done, then I'm just waiting for the test to actually execute.
这部分是我们自己的事。
And that's our stuff.
我们就是希望这些测试能正常执行它们该做的事。
And like, want those tests to do what they're doing.
所以,这方面我们没什么意见。
So, you know, we're fine with that.
我们还一直在用,嗯,
We've also been using, okay.
我觉得最大的痛点是,等Zig原生支持模糊测试的时候,现在虽然也能做一堆模糊测试,而且能跑起来,但体验真不怎么样,这点你应该也清楚。
So pain point, I would say we're excited about when Zig has first class fuzzing integration because doing a bunch of fuzzing, and it works, but it's not the smoothest experience, as I'm sure you're aware.
比如和AFL集成这种,嗯,能用,但用户体验真不算好。
Like the integration with like AFL fuzz is like, yeah, like it works, but it's not the best user experience.
对。
Yeah.
其实那个内置的模糊测试根本跑不通。
Well, the integrated fuzzing doesn't doesn't work.
我觉得可以这么形容。
That's how I'd categorize it.
明白。
Sure.
不过呢,咱们也都知道,就是……
Like, but like, you know, we we Yeah.
算了。
Whatever.
用 Zig 做模糊测试是可行的,我们确实已经在用了。
Like, using using fuzzing in Zig, like, is is a possible We are doing it.
所以从某种意义上说,它确实是能跑通的。
So we know it's it it like, it works in that sense.
但说实话,这跟 Zig 一贯的体验差远了,比如标准库里的东西,你一用就顺手。
But like, yeah, it's not like the normal Zig experience of just like, oh, this is like in standard library.
直接拿过来就能用,一切都特别流畅。
We just like pull it off the shelf and everything just works great.
对吧?
Right?
我们迟早会做到的。
Know you We'll get there.
我们一定会做到的。
We'll get there.
是的。
Yeah.
就像我之前说的,我们很期待。
So like I said, we're excited.
这是个好话题,因为模糊测试这个概念本身就特别酷。
That's a good topic because man, fuzzing is such a cool concept, first of all.
这个播客的听众主要是哪些人?
How's the audience of this podcast?
需要解释一下吗,还是说我们直接跳过?
Is it worth an explanation or should we kind of
当然,简单讲一下吧。
Sure, can briefly, yeah.
你来说吧。
Go for it.
好的。
Okay.
让我先了解一下模糊测试到底有多厉害。
Let me learn about how cool fuzzing for a second.
当然。
Sure.
所以呢,你知道最烦的是什么吗?
So the idea is, you know what sucks?
写单元测试。
Coming up with unit tests.
真的特别头疼。
It's so annoying.
你得动脑筋,还得想出一大堆东西。
You have to think, and you have to figure out all this stuff.
那要是电脑能帮你把所有单元测试都自动生成出来呢?
Okay, what if instead the computer could figure out all your unit tests for you?
我们先从随机性开始。
So we start with just randomness.
开始模糊测试,测试工具会直接生成一串完全随机的字节。
We start fuzzing, and the fuzzer gives us just a buffer of just literally random bytes.
你的任务就是想办法把这些数据喂给你的软件,也就是你要测试的那个程序。
And your task is to feed that somehow to your software that you want to test.
你可以想象一下,有些东西测试起来比另一些容易。比如你有个分词器,你直接喂它一堆随机字节,然后检查它会不会崩溃。
Now, you can imagine some stuff being easier to test than others, like if you have a tokenizer, you can literally feed it random bytes, and you can check that it doesn't crash.
你还可以检查某些属性是否符合它本该有的工作约束。
You can check that certain properties are within the constraints of how it's supposed to work.
我们真的
We literally
你已经做到了这一点,好吧,确实如此。但如果你想做点更有趣的事,比如编译器的下一个阶段——解析,随机字符串根本帮不上忙。
do You're that already kind of Okay, yeah, you literally do But that if you're trying to do something more interesting, let's say the next phase of the compiler, like parsing, random strings are not going help you at all.
那只是
That's just
会一遍又一遍地遇到同样的无效令牌错误。
going to hit the same bad token error over and over and over again.
就算有一百万只猴子永远不停地敲键盘,它们也写不出莎士比亚的作品。
A million monkeys typing keyboards forever, they're not gonna create the works of Shakespeare.
他们就是不停地敲 ASDF ASDF,一遍又一遍。
They're just gonna type, like, ASDF ASDF, like, over and over again for return.
对。
Right.
不过话说回来,我倒不觉得这完全没用,因为我们到现在还没处理完所有暴露出的 bug。
Now now in in in defense of that, I would I wouldn't say it doesn't help you at all because we actually just by doing that, we we still have not gotten through the backlog of all the bugs that has turned up.
比如你可能会遇到这种情况:有人输入了一个像问号加右花括号、再加方括号这样的乱七八糟的程序,结果编译器处理得特别差,根本没按该有的方式应对。
Just because, like, you'll get this thing where it's like, you know, oh, it turns out if someone types in a rock program of like question mark, close curly brace, you know, like square bracket, that, like, doesn't do like the compiler doesn't handle it as that nonsense as gracefully as it should.
这种问题挺多的,没错。
There's a bunch Right.
有一方面呢,我知道你意思,但我也想说,谁在乎这些呢?
Of stuff like that And, and, and, like on the one hand, you know, you know, not, not to, not to derail what you're saying, but like, on the one hand, you know, you can make the case of like, who cares?
普通人根本遇不到这种问题。
People aren't gonna run into that.
但另一方面,把所有这些漏洞都彻底修掉,确保我们没有任何已知的模糊测试错误,好处是当你给编译器加新功能时,一旦再出现模糊测试报错,你马上就能意识到:坏了,这问题我知道怎么回事了。
The, on the other hand, what's really nice about burning down all of those and actually fixing them all such that we're like, yeah, we don't have any known fuzzing errors, is that when you add a new feature to the compiler, if you start seeing fuzzing errors, suddenly you're like, uh-oh, I know, I know what caused this.
这种情况已经发生好几次了。
And this has already happened multiple times.
我们还说呢,都快一周没见到模糊测试出错了。
We're like, we're like, hey, I hadn't seen a fuzzing error in like a week.
结果刚合并了这个 PR,现在又开始冒出模糊测试错误了。
And then we just landed this PR and now I'm starting to see fuzzing errors again.
所以我不太清楚到底出了什么问题。
So like, I don't know what happened.
但我清楚是什么引起的。
I know I know what caused it.
对吧?
Right?
是的。
Yeah.
对。
Yeah.
关于 Bayt 播客
Bayt 提供中文+原文双语音频和字幕,帮助你打破语言障碍,轻松听懂全球优质播客。