用 curl 下载 OnePlus 的 ROM

刷机,轻而易举啊! 坏了,坏了坏了坏了 这是一篇很可能过期了的文章,但截止 2025.09.10 可以使用 发现没办法通过浏览器直接下载 OnePlus 的 OTA ROM,于是在 XDA上翻资源找到了这个邪门的方法 https://xdaforums.com/t/download-oxygenos-rollback-package.4712819/格式大概是curl -C - -LO --resolve oxygenos.oneplus.net:443:23.48.224.239 <url> 以下是举例 NAcurl -C - -LO --resolve oxygenos.oneplus.net:443:23.48.224.239 https://oxygenos.oneplus.net/4188_sign_CPH2417_11_A_OTA_0090_all_5f7153_10100001.zip GLOcurl -C - -LO --resolve oxygenos.oneplus.net:443:23.48.224.239 https://oxygenos.oneplus.net/4248_sign_CPH2415_11_A_OTA_0080_all_44864f_10100111.zip EUcurl -C - -LO --resolve oxygenos.oneplus.net:443:23.48.224.239 https://oxygenos.oneplus.net/4189_sign_CPH2415_11_A_OTA_0080_all_44864f_01000100.zip INcurl -C - -LO --resolve oxygenos.oneplus.net:443:23.48.224.239 https://oxygenos.oneplus.net/4190_sign_CPH2413_11_A_OTA_0080_all_e63867_00011011.zip

2026/1/13
articleCard.readMore

实用命令切片

记录一下我常用的比较奇技淫巧的实用命令切片 XD 使用管道通过 SSH 传输目录 在常用的情况下一般是使用 tar 命令打包目录然后scp到远程主机再解压,通过这个操作复制需要三个步骤:打包,登录,解压 我们注意到 tar命令有管道能力,于是我们可以利用管道能力来把这几个操作压缩成为一个操作: 1 tar -cJf - [目录名] | ssh [主机信息] "mkdir -p [目标目录] && xz -d | tar -C [目标目录] -xpf -" 检查管道中传输数据的进度 有的时候可能会想在管道操作中查看数据的进度,毕竟大多数的时候管道是没有输出进度的,我们可以使用pv 命令来解决这个问题: 1 tar -cJf - [目录名] | pv | ssh [主机信息] "mkdir -p [目标目录] && xz -d | tar -C [目标目录] -xpf -" 使用管道通过 SSH 传输Podman/Docker 镜像 注意到 podman 和 docker 的save子命令是会输出到标准输入输出的,于是我们可以将其直接管道到远程主机直接加载: 1 podman save [镜像信息] | ssh [主机信息] 'podman load' 在这个时候是没有压缩的(可能是有但是太小了),我们可以引入xz 命令来压缩,这样就可以节约传输的带宽: 1 podman save [镜像信息] | xz -z -T0 | ssh [主机信息] 'xz -d | podman load' 我们引入上面的 pv 命令用法,可以看到数据确实是有压缩 1 2 3 4 5 ➜ forks podman save ... | pv | ssh ... 'podman load' 92.1MiB 0:00:06 [15.1MiB/s] [ <=> ] ➜ forks podman save ... | xz -z -T0 | pv | ssh ... 'xz -d | podman load' 20.9MiB 0:00:06 [3.14MiB/s] [ <=> ]

2026/1/11
articleCard.readMore

再见,Oh My Zsh。

Oh My Zsh 陪伴了我一整个学生时代,自从使用 zsh 后就没有离开过 Oh MyZsh。 不过终有离别的时候,上一次改革自己的工作条件是因为 VSCode工作速度太慢不跟手导致体验非常不好,现在这把砍刀终于砍到 shell上了。 挥别 很高兴的是我的 shell配置习惯非常好,所有的自己定义的配置文件都集中在了.zprofile 中,因此只需要保留这个文件就好。 我需要什么? 在替换掉 Oh My Zsh 之前,我要想下我需要什么东西? 需要一个还不错的 prompt?来保证我知道我现在的目录里有什么信息。 比如 git,nodejs 版本等 需要补全系统。默认的 zsh是没有启用补全的。 zsh还可以在输入内容可以被判定为目录的时候自动切换到目录中,这个我也需要。 还需要历史记录,这也是默认情况下 zsh 没有的 还有...对历史记录的模糊搜索! 现在我们知道需要什么了,开始超级拼装吧! 超级拼装 这些方案都有现成的,所以拼起来非常简单。 自动补全 & 历史功能 & 自动cd zsh 自带补全模块和自动 cd 模块,我们简单设置一下就好 不过我把历史记录大小设置得非常大,建议按照自己的需求修改 1 2 3 4 5 6 7 export HISTFILE=~/.zsh_history export HISTSIZE=1000000000 export SAVEHIST=$HISTSIZE setopt EXTENDED_HISTORY setopt autocd autoload -U compinit; compinit 历史搜索 我使用 fzf 作为我的模糊搜索器,他非常出名而且几乎是系统源中必然就有的 fzf 已经做了相关支持,我们只需要简单的应用一下就好 1 source <(fzf --zsh) 更多的快捷键配置 在我还在使用 Oh My Zsh 的时候,可以使用Ctrl+<Left> 和 Ctrl+<Right>跳转命令行中的单词。 到后来我才发现这个并不是 zsh 自身的设定,因此需要自己做一个绑定 1 2 bindkey '^[[1;5D' emacs-backward-word bindkey '^[[1;5C' emacs-forward-word 快捷键本质上是 CSI序列,如果不知道组合键的做法,你可以使用如下命令然后键入组合键来获取CSI 序列 1 cat > /dev/null 原先的环境配置 我自己的环境配置都是在 .zprofile 中,因此在.zshrc 中 source 一下就行了 更好的提示符 我采用 starship作为我的提示符,同时我希望他可以负担一部分主题的功能,虽然主题确实显得很不重要了:) 不过starship默认会开启几乎所有的显示功能,有些我是不太需要的,因此我也需要关掉,这些一般是一些云服务。 我使用的是 Oh My Zsh 的默认主题robbyrussell,就只是个简单的加粗绿色箭头符号,因此我打算继承下来。 1 2 3 4 5 6 7 8 9 "$schema" = 'https://starship.rs/config-schema.json' add_newline = true [package] disabled = true [aws] disabled = true 然后在我们的 .zshrc 中启用我们的starship 候选高亮 在以上的配置中我们发现没有候选高亮,在搜索之下我们添加如下的 zsh配置即可 1 zstyle ':completion:*' menu select 更多? 更多的就不需要我介绍了,可以自己装插件管理器来实现更多的功能。 最终答案 在最后,我们得到了一个十分简单易用的 zsh 配置! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ################################################# # zsh # ################################################# export HISTFILE=~/.zsh_history export HISTSIZE=1000000000 export SAVEHIST=$HISTSIZE setopt EXTENDED_HISTORY setopt autocd autoload -U compinit compinit zstyle ':completion:*' menu select ################################################# # keybinds # ################################################# bindkey '^[[1;5D' emacs-backward-word bindkey '^[[1;5C' emacs-forward-word ################################################# # environment # ################################################# source ~/.zprofile eval "$(starship init zsh)" source <(fzf --zsh) 晚安,Oh My Zsh。

2026/1/7
articleCard.readMore

你不应该复用 strings.Builder

在编写 Go 程序的时候在程序所占用的堆足够大后经常会遇到 GC缓慢的问题,在这个时候第一个入手的地方就是利用对象池来处理对象复用问题减轻GC 压力。 特别是对于 strings.Builder 类型,更应该利用sync.Pool 来复用它对...吧? 网上是怎么说的? 截止 2025 年 11 月 22 日,在 Bing上搜索关键字strings.Builder 复用会得到如下几个排在前面的结果 高并发 Go服务如何避免 GC 压力?strings.Builder 复用技术详解 Golang  中 strings.builder  的 7 个要点 - 知乎 Golang字符串优化:strings.Builder 使用技巧-Golang 学习网 毫不意外地,三篇看起来高质量中文来源的文档都提到了你应该复用strings.Builder。事实是这样吗? 为什么你不应该复用strings.Builder? 要继续了解这一点,我们需要知道一个基础知识: string类型是不可变的,一旦创建不可修改(正常情况下) 如果要进行修改需要使用 unsafe黑魔法,而大多数情况下应该保持其不可变原则 strings.Builder 的源码 简略起见,这里摘抄本文涉及到的代码即可 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 // A Builder is used to efficiently build a string using [Builder.Write] methods. // It minimizes memory copying. The zero value is ready to use. // Do not copy a non-zero Builder. type Builder struct { addr *Builder // of receiver, to detect copies by value // External users should never get direct access to this buffer, since // the slice at some point will be converted to a string using unsafe, also // data between len(buf) and cap(buf) might be uninitialized. buf []byte } // String returns the accumulated string. func (b *Builder) String() string [](){ return unsafe.String(unsafe.SliceData(b.buf), len(b.buf)) } // Reset resets the [Builder] to be empty. func (b *Builder) Reset() { b.addr = nil b.buf = nil } // copyCheck implements a dynamic check to prevent modification after // copying a non-zero Builder, which would be unsafe (see #25907, #47276). // // We cannot add a noCopy field to Builder, to cause vet's copylocks // check to report copying, because copylocks cannot reliably // discriminate the zero and nonzero cases. func (b *Builder) copyCheck() { if b.addr == nil { // This hack works around a failing of Go's escape analysis // that was causing b to escape and be heap allocated. // See issue 23382. // TODO: once issue 7921 is fixed, this should be reverted to // just "b.addr = b". b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b))) } else if b.addr != b { panic("strings: illegal use of non-zero Builder copied by value") } } 特殊的地方? 我摘抄了三个函数,他们分别是 func (b *Builder) Reset():重置状态函数 func (b *Builder) String() string:转换成 string类型的最终函数 func (b *Builder) copyCheck():掌管Builder 不能被复制的神 从这三个函数中就能看到端倪:copyCheck 的存在实际上不允许Builder 被复制,当出现复制的时候会直接panic copyCheck implements a dynamic check to prevent modification aftercopying a non-zero Builder, which would be unsafe (see #25907,#47276). We cannot add a noCopy field to Builder, to cause vet's copylockscheck to report copying, because copylocks cannot reliably discriminatethe zero and nonzero cases. copyCheck 实现了一种运行时检查,用于防止在复制了一个非零的 Builder之后再对其进行修改,因为那样做是不安全的(见 issue 25907, 47276)。 我们不能给 Builder 增加一个 noCopy 字段,从而让 go vet 的 copylocks检查器报告复制行为,因为 copylocks无法可靠地区分“零值”和“非零值”这两种情况。 这几段代码中提到了几个 issues,也顺便记录在这里 strings: copyingbut not using a Builder after first use leads to runtime crash(generates heap -> stack pointer) #47276 proposal: embed"noCopy" for bytes.Buffer and strings.Builder #25907 strings: Buildercopy check causes a dynamic memory allocation #23382 看起来这里还涉及到了逃逸分析的事情,我们日后再探 :) 黑魔法? 可以很简单的注意到代码切片中有一些 unsafe 用法: 1 2 3 func (b *Builder) String() string [](){ return unsafe.String(unsafe.SliceData(b.buf), len(b.buf)) } 这个写法其实是后来 Go 官方引入的 unsafe工具,在此之前写法比较像是: 1 *(*string)(unsafe.Pointer(&b)) 其中 b 的类型是 []byte。 unsafe.String 于https://github.com/golang/go/issues/53003 引入 这个 unsafe 用法非常典型,可以避免一次因为从切片类型转换到 string类型导致的开销。 但事实上黑魔法总是有代价的,不然为什么叫作黑魔法? 因黑魔法付出的代价 还记得吗?在 Go 中默认情况下 string类型是不可变的:创建后就不可以修改,如果要修改那么需要付出一次复制内存的代价。 这句话的另外一个意思是:正常创建的 string对象是不会被修改的。 我们使用以上提到的 unsafe黑魔法时本质上是对指针进行强制转换,可以这样做的前提是对象的内存布局相同 这里引出了一个问题:如果因为某种情况导致内存布局不同,那么黑魔法就会反噬我们。 在上文提到的 issues https://github.com/golang/go/issues/53003中也有相关提及: The second use case is commonly seen as([]byte)(unsafe.Pointer(&string)), which is by-defaultbroken because the Cap field can be past the end of a page boundary(example here, in widely used code) -- this violates unsafe rule(1). 在 Go 中,切片类型是可变的,而 string类型是不可变的——那我们就得到了一个结论, strings.Builder在完成构造 string的时候最终需要构造出一个不可变的类型的指针 string指向这块内存。 在 strings.Builder这个语境上,我们有两个选择: 不复制内部的 []byte,在使用 String()方法的时候强制转换指向其的指针类型成为 string作为返回值。 复制内部的 []byte,并且重置内部的 []byte指针为 nil,将指向原先 []byte 的指针强制转换为string 作为返回值。 而 Go 选择了第一个方法。为什么呢? 考虑到开销和常见的用法,确实第一种会更合理——因为拼出来的 string可能会非常大,如果在最后一步还要付出一次复制开销显然是不值得的。 我们继续深入考虑这个选择:当你把指向一块内存的指针从可变类型转换成为了不可变类型,如果你不做额外措施保护这一块内存的话那么他就会受到修改,进而使得产生数据竞争破坏内存。 所以复用 strings.Builder起码不能减轻因为内存分配而产生的 GC压力——因为分配的内存块并不能被成功复用。 我们这个时候来看看 Reset()这个方法,可以清楚的看见这个方法的功能是置为 nil而不是清空状态,调用 Reset()后会完全清空其中的内容,这样也就避免了内存块在转换成为string 受到破坏的可能。 换句话来讲,Reset() 这个方法其实相当于把strings.Builder 恢复到刚实例化的时候。 所以使用sync.Pool 复用 strings.Builder 有提升吗? 基于这个想法,我们可以构造多个情况来检测是否有提升。 名字场景描述实现方式 BenchmarkWithoutReset大写入量,串行每次都新建 BenchmarkWithReset大写入量,串行Pool + Reset 复用 BenchmarkWithoutResetTiny极小写入量,串行每次都新建 BenchmarkWithResetTiny极小写入量,串行Pool + Reset 复用 BenchmarkWithoutResetParallel极小写入量,32 并发 goroutine每次都新建 BenchmarkWithResetParallel极小写入量,32 并发 goroutinePool + Reset 复用 我编写的代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 package bench_test import ( "strings" "sync" "testing" ) // 全局的 Pool,避免每次 benchmark 都重新创建 var builderPool = sync.Pool{ New: func() any { return new(strings.Builder) }, } // 为了公平,我们让两个 benchmark 都做同样多的写入工作 const writeSize = 64 // 每次写 64 字节 // 每次新建 Builder func BenchmarkWithoutReset(b *testing.B) { b.ReportAllocs() for b.Loop() { var buf strings.Builder for range 100 { buf.WriteString("0123456789ABCDEF") buf.WriteString("0123456789ABCDEF") buf.WriteString("0123456789ABCDEF") buf.WriteString("0123456789ABCDEF") } _ = buf.String() } } // 复用 Builder 并 Reset func BenchmarkWithReset(b *testing.B) { b.ReportAllocs() for b.Loop() { buf := builderPool.Get().(*strings.Builder) buf.Reset() // 清零 for range 100 { buf.WriteString("0123456789ABCDEF") buf.WriteString("0123456789ABCDEF") buf.WriteString("0123456789ABCDEF") buf.WriteString("0123456789ABCDEF") } _ = buf.String() builderPool.Put(buf) // 归还 } } // 测试非常小分配的情况下不调用 Reset 的情况 func BenchmarkWithoutResetTiny(b *testing.B) { const tiny = "x" // 1 字节 b.ReportAllocs() for b.Loop() { var buf strings.Builder for range 100 { buf.WriteString(tiny) } _ = buf.String() } } // 测试非常小分配的情况下调用 Reset 的情况 func BenchmarkWithResetTiny(b *testing.B) { const tiny = "x" b.ReportAllocs() for b.Loop() { buf := builderPool.Get().(*strings.Builder) buf.Reset() for range 100 { buf.WriteString(tiny) } _ = buf.String() builderPool.Put(buf) } } // 测试非常小分配的情况下不调用 Reset 的情况(并行) func BenchmarkWithoutResetParallel(b *testing.B) { const tiny = "x" b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { var buf strings.Builder for range 100 { buf.WriteString(tiny) } _ = buf.String() } }) } // 测试非常小分配的情况下调用 Reset 的情况(并行) func BenchmarkWithResetParallel(b *testing.B) { const tiny = "x" b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { buf := builderPool.Get().(*strings.Builder) buf.Reset() for range 100 { buf.WriteString(tiny) } _ = buf.String() builderPool.Put(buf) } }) } 运行这个测试,在我的电脑上得出了如下结果: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ➜ bench go version go version go1.24.9 linux/amd64 ➜ bench go test -bench=. -benchmem goos: linux goarch: amd64 pkg: bench cpu: AMD Ryzen 9 7945HX with Radeon Graphics BenchmarkWithoutReset-32 357000 3283 ns/op 24816 B/op 13 allocs/op BenchmarkWithReset-32 406946 3279 ns/op 24854 B/op 13 allocs/op BenchmarkWithoutResetTiny-32 2897300 404.1 ns/op 248 B/op 5 allocs/op BenchmarkWithResetTiny-32 3016304 400.0 ns/op 248 B/op 5 allocs/op BenchmarkWithoutResetParallel-32 17941327 75.50 ns/op 248 B/op 5 allocs/op BenchmarkWithResetParallel-32 13661240 90.87 ns/op 248 B/op 5 allocs/op PASS ok bench 7.642s 最终的结论 另外我需要提到的是,在部分情况下 string 到[]byte 是可以不付出转换开销的,可以看这个 issue

2026/1/2
articleCard.readMore

博客的明日

这段时间因为一些原因需要查找一些可能并不是那么好找的资料,于是我逛尽了Reddit,StackOverflow。 在这个路程中我经常逛到一些博客地址,给我了一些以前从未有过的体验,让我觉得博客这种信息载体(或许可以这样叫?)的存在依然有独特的意义。 我和博客的三二事 高中的时候就听到过一个言论:博客已死,RSS 已死。 在高二的时候我还会经营我的微博,那个时候喜欢往微博上面发自己的画。还记得当年尚存的腾讯微博,在那时候已经彻底死掉了。他们都被称之为博客的终结者,所有的人都能轻松地注册账号,在上面轻松地搜索所有信息,并且也可以轻松地发微博:不需要自己搭建什么,不需要自己编写代码,只需要输入文章,就能让所有的关注者都能看到。一切都是如此的欣欣向荣,一切都是那么的简单。 但总是有人不信邪。我在高三的疫情期间建立了第一个属于我自己的博客,那个时候还是使用的WordPress——毕竟那个时候我只知道 WordPress,后来才知道的 Typecho 和Hexo。 在那个初代博客上,我写下了当时的一些日记:可能是骂谁的,可能是自己的感想,也可能是某年的年终总结,毕竟当时想的是没有人会看——被我骂的人也不可能看到,他们都不知道我有这个博客:) 那个时候的感觉非常有趣。我作为一个十分内向的人,居然在互联网上有了自有地。我可以在上面撒欢,自由自在地说想说的事情,没有人会管我,他们甚至不会看到这里,就算说看到了也不知道我是谁。 再到后来就高考了,我进入了大学。 它已经随风去了,随着注册商和域名的消失,消失在了互联网里。可它没有化作齑粉,而是化作了未来更为稳定的博客的基石。 互联网的自有地 作为零零后,虽然我接触计算机很晚,但也有幸受到某种黑客精神的熏陶——即使已经是自由互联网的余晖——我可以自由的在互联网上做些事情。 我是个很笨的人,学习速度非常慢,但唯独计算机除外:它总是能精确而快速地给我结果,我对这件事非常兴奋。在疫情期间了解了很多比如自由软件运动之类的左翼运动,也在那段时间坚信知识是自由的。 在第二个博客里,我依然会写以上提到的那些内容,不过基本上没有了情绪化的输出,开始尝试将博客作为笔记本和日记本使用。在那段时间也开始写一些技术上的学习笔记,因为我开始正式编程正是那段时间,有了很多不同的收获。 现在回望那个时候,感觉也不算是什么自留地:充其量也只能说是个记事本。 再后来我忘了因为什么原因,或许是不希望透露太多个人的情感?也可能是打算认真经营博客?总之是删掉了那个博客的所有文件,它也消失在了互联网中。 过了一段时间,我还是重新搭建起来了博客,也就是使用到了现在的博客,到现在已经四年啦。买了新的域名,并且决定以后就使用Kryszta Huang 这个名字了——这就是另外的故事了。 还记得第一篇文章是写的我解决了一个我未能从互联网上找到解决方案的问题,那种成就感是真的很棒。希望它能帮到某人吧! 说了这么多我的博客的事情,稍微有点偏题了。 在 Reddit 和 StackOverflow上闲逛的时候遇到过一些博客,而自己该死的好奇心驱使自己去多看看这些博客上写了什么——万一又是一个新的宝藏博客呢?如果是的话以后有更多时不时可以逛逛的博客了不是吗。 于是在对一些博客的"发掘"中发现了很多有趣的事情。 博客已死? 我还记得我的导师 Sylvestre 的博客,他是就职于 Mozilla的工程师——但他的博客大部分内容都是他的旅行日记。在发现这点后,我意识到博客并非只能与我的工作有关:它可以记录任何事情,我感兴趣的,我的生活,以及更多,想写什么写什么。这是我第一次意识到博客可以如此立体,我的导师写了很多旅行中遇到的事情,虽然基本上是法语,但翻译过来后我依然能感受到他在旅行过程中的感受和情绪。 同时我也意识到并非所有博客文章都需要详细记录,或许也可以只是很短的一篇呢,毕竟对于灵感而言,一瞬间的感受也是很重要的。 在前段时间的游览中进一步感受到博客的独特之处:博客有着很好的记录属性,他可以几乎不受限制地记录博客主人的所思所想与所写,而诞生于SaaS潮流的微博受制于平台本身并不能做到这一点,他们受到平台的限制,并且没有目录功能很难说可以整理成一个笔记本一样的东西——在上面几乎只能发表很小很短的文章,这也是"微"的原因。 回到我最初的目的,我在寻找解决方案之时有幸访问了一些博客,并且了解到了对应的博客主人的所思所想。 这就好比于,我在宽广无际的沙漠之中探寻,在我即将万策皆尽之时竟然看到了一抹绿:那是一篇伫立在沙漠之中的绿洲,十分突兀但又十分喜人,吸引着我前去。而在我正在担忧绿洲的主人是否会拒绝我的时候,其正捧着一碗水带着微笑向我走来——并且邀请我前去绿洲之中修整。 绿洲的主人大方地向我分享了我所求之物,并且还告诉我:若需要的话还请再来,有所需皆尽其所能。甚至还和我彻夜畅谈(没错,我曾因为读一个博客熬夜熬穿了)。 但也有一些特别的情况。有些博客会因为一些原因停止了更新,我会去看作者的关于页面,寻找其其他的互联网公开联系方式——或许是Steam,可能是推特,也可能是其他的,想了解他们发生了什么事情、最近在做什么。有两个博客给了我很深的印象。 其一博客的主人已于 2024年因癌症去世,其二博客的主人因车祸而亡。 前者的博客主人写了一系列的关于其生平的文章,介绍了其一生的经历:从8086时代到现代,到他给他儿子配的一台很棒的计算机——并且着重地声明了"是用来打游戏的!我儿子非常喜欢",再到他罹患癌症,到生命的终点。并且将这一系列的文章取了个很俏皮的名字:死灵的回响。他已不存在于世间,但他乐意与我分享他的过往。 后者的主人去世地很突然,是其妻子写的最后一篇文章。他的妻子并不擅长计算机,甚至可以说一窍不通——但她依然写了博客上的最后一篇文章:告知大家博客主的情况,家庭情况,表达了对大家的感谢。很多受其益的程序员都来这里表达了哀悼和感谢。最让我动容的是,这篇文章是其妻子发邮件向其开源社区里的"同事"寻求帮助之下写作的。 如果还是以绿洲作为比喻的话,那他们的博客是依然良好打理的绿洲,不过其主人已不在居住于此——虽然可能不礼貌,稍后会解释为什么我会这样说——但其依然备了取之不尽用之不竭的果盘在绿洲的庭院之中的凉亭等候着旅者的到来,而微风吹动的庭院窗帘正阐述着这片绿洲的惬意。而绿洲的主人在果盘之下压了一封信,向你述说着这个绿洲的故事,独属于绿洲主人的故事。并且在末尾之处温柔地提醒你,请你享受这片绿洲和微风,享受这片他昔日为自己搭建的歇脚之处——他愿意分享于你。 为什么我会觉得他们没有死亡呢?我觉得死亡有三个阶段:生理死亡,物理死亡,社会死亡。只有到最后的社会性死亡才算真正的死亡——已没有人记得你的存在。不过这是个相当复杂的话题,我想我在这个方面是远远不够格的。 在当今充满戾气的互联网之中显得格外耀眼,这些博客体现了人类的最耀眼的优良品质。 博客未死,绿洲永存。 From those who've fallen to those who arise,活下来的人啊,恳求你们, A prayer to keep us ever by your side,请将我们已经逝去的生命永远带在身边, An undying promise that we just might, 也许这不灭的承诺, Carry on in a song, 将会在歌中传承下来, Pray don't forget us your bygone kin,请不要忘记我们,你逝去的同胞, With one world's end does a new begin,一个世界的终结,意味着另一个世界的诞生, And should our souls scatter unto the wind,我们的灵魂定会在风中消散, Still we shall live on, 但我们仍将继续存在。 《Tomorrow and Tomorrow》——祖堅正慶

2025/12/22
articleCard.readMore

被 AppArmor 击杀的 Dockge

Debian 自从 Debian10 后开始默认启用了 AppArmor,这是另外一个和SELinux 类似的 MAC(Mandatory Access Control)实现,但他有更容易被人类所接受的配置颗粒度,并且由社区开发,而且很容易关掉(不推荐你关掉) 在默认情况下 Debian 的 AppArmor运行情况是不会干扰用户的正常操作的,但是在有些时候他会突然给你一拳告诉你你不能这样做,这样违反了约束。 然而这完全是 Debian 的原因吗? 好吧,我遇到了。在 Debian13 上如果不去新增 AppArmor 配置就会无法运行Dockge 这个还挺不错的 Compose 管理器。 除此之外,还有 lscr.io/linuxserver/qbittorrent这个容器也受到了影响——可能更多,但我没有继续测试。 初探 我们通过命令 dmesg | rg apparmor | rg denied得到了输出 1 2 3 4 5 6 [ 2661.149627] audit: type=1400 audit(1759760495.520:84): apparmor="DENIED" operation="create" class="net" info="failed protocol match" error=-13 profile="docker-default" pid=15527 comm="node" family="unix" sock_type="stream" protocol=0 requested="create" denied="create" addr=none [ 2674.109757] audit: type=1400 audit(1759760508.479:85): apparmor="DENIED" operation="create" class="net" info="failed protocol match" error=-13 profile="docker-default" pid=15712 comm="node" family="unix" sock_type="stream" protocol=0 requested="create" denied="create" addr=none [ 2699.872327] audit: type=1400 audit(1759760534.241:86): apparmor="DENIED" operation="create" class="net" info="failed protocol match" error=-13 profile="docker-default" pid=15897 comm="node" family="unix" sock_type="stream" protocol=0 requested="create" denied="create" addr=none [ 2751.237225] audit: type=1400 audit(1759760585.607:87): apparmor="DENIED" operation="create" class="net" info="failed protocol match" error=-13 profile="docker-default" pid=16081 comm="node" family="unix" sock_type="stream" protocol=0 requested="create" denied="create" addr=none [ 2811.399220] audit: type=1400 audit(1759760645.767:88): apparmor="DENIED" operation="create" class="net" info="failed protocol match" error=-13 profile="docker-default" pid=16269 comm="node" family="unix" sock_type="stream" protocol=0 requested="create" denied="create" addr=none [ 2871.560585] audit: type=1400 audit(1759760705.928:89): apparmor="DENIED" operation="create" class="net" info="failed protocol match" error=-13 profile="docker-default" pid=16472 comm="node" family="unix" sock_type="stream" protocol=0 requested="create" denied="create" addr=none 接下来诡异的事情来了:我没有在任何地方发现任何一个叫作docker-default 的配置文件,完全不存在于标准的/etc/apparmor.d/ 中: 1 2 ➜ dockge ls /etc/apparmor.d/ | rg docker ➜ dockge 怎么办? 再探 我觉得这个问题的关键词应该是 Docker,AppArmor。于是我的第一反应是像 GPT求助——很可惜它没有给我预期响应。 于是我直接使用搜索引擎进行搜索,第一项便是如下链接: Docker的 AppArmor 安全配置文件 很好,这就是我想要的。但也不完全是,因为这里面没有说解决方案,相反给了个巨复杂的文件 再做搜寻,我看到了这个 如何在Debian 上禁用 docker-default 他的解决方案是想办法让 Docker 不再动态生成docker-default这个配置。我觉得这个已经很接近我想要的答案了,但我觉得不够完美 终曲 我们现在得到了两个方案: 调整 Docker 的安全配置 关闭 AppArmor 对于 Docker 的管控 调整 Docker 的安全配置 我这里列出 compose.yml (也就是 docker-compose.yml的高版本别名)中如何调整相关设定: 在修改之前: 1 2 3 4 5 6 7 8 9 10 11 12 13 services: dockge: image: louislam/dockge:nightly restart: unless-stopped ports: - 5001:5001 volumes: - ./data:/app/data - /pool/striping/dockge/stacks:/pool/striping/dockge/stacks - /var/run/docker.sock:/var/run/docker.sock environment: - DOCKGE_ENABLE_CONSOLE=true - DOCKGE_STACKS_DIR=/pool/striping/dockge/stacks 在修改之后,请注意新添加的 security_opt 字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 services: dockge: image: louislam/dockge:nightly restart: unless-stopped ports: - 5001:5001 volumes: - ./data:/app/data - /pool/striping/dockge/stacks:/pool/striping/dockge/stacks - /var/run/docker.sock:/var/run/docker.sock environment: - DOCKGE_ENABLE_CONSOLE=true - DOCKGE_STACKS_DIR=/pool/striping/dockge/stacks security_opt: - apparmor:unconfined 关闭 AppArmor 对于 Docker的管控 我不建议你这样做,别看了。 如果你真的要这样做的话,请参照这篇文章根据你的需求调整: 如何在Debian 上禁用 docker-default 以及另外一个方式,让 Docker 彻底不使用 AppArmor 1 systemctl edit docker 随后键入 1 2 [Service] Environment=container="disable apparmor" 本源 在不断的探索中我发现一个很奇怪的现象:为什么我的工作用系统同为Debian,但我的 NAS 却表现出了几乎完全不同的行为? 难道是 Debian 在某个版本产生了Breakings?这并不太现实,因为两个设备都是 Debian Testing,一旦产生Breakings 则会导致大家都出现错误。 于是我在不断的对比之下终于发现了不同:莫非是 NAS 上的 Debian的内核的区别导致的?我记得我的 NAS 为了使用 ZFS 安装了 Proxmox 的 Linux内核。 Proxmox的手册是如何说这件事的? Proxmox 的手册里有这样一段话: While it can be convenient to run “Application Containers” directlyas Proxmox Containers, doing so is currently a tech preview. For usecases requiring container orchestration or live migration, it is stillrecommended to run them inside a Proxmox QEMU virtual machine. 尽管直接将“应用容器”作为 Proxmox容器运行可能颇为便捷,但目前这仍属于技术预览阶段。对于需要容器编排或实时迁移的用例,我们仍然建议将其运行在Proxmox QEMU 虚拟机内部。 也就是说这部分功能实际上是由 PVE接管了的,因此会造成非常多的问题——但我好像还是没有搞懂为什么,论坛上指出可能是Proxmox 的网络规则导致,并且有其独特的 LXC 规则因此不支持 Docker: Howdifficult are Dockers to setup on Proxmox? The isolation on both on proxmox leaves a bit to be desired. NFSshares and mounts continue to cause issues. Particularly in CTs. If adocker stack gets a reference to a filesystem which then goes away itcan become kernel locked and will never, ever let go of that filesystemuntil you do a complete reboot of the PVE node. Proxmox 上的隔离机制还有些不尽如人意。NFS共享和挂载持续引发各种问题,尤其是在 CT 容器中尤为突出。如果某个 Docker堆栈引用了一个文件系统,而该文件系统随后又消失了,那么它就可能彻底卡死内核,而且无论如何都不会再释放这个文件系统,除非你彻底重启整个PVE 节点。 Dockerand Proxmox side-by-side Running Docker in LXC is not recommended or supported on Proxmox VE(you will run in many issues). 在 Proxmox VE 上,在 LXC 中运行 Docker是不被推荐且不受支持的(你会遇到很多问题)。 我猜测其实可能并不是内核导致的,而是安装 Proxmox 是某些包引入的iptables 规则导致的——毕竟我是从 Debian 安装的 Proxmox 而不是直接使用 ISO安装的 Proxmox 深度探寻 在根据手册安装 Proxmox 的时候需要安装如下软件包: proxmox-ve postfix open-iscsi chrony 首先瞄准的目标必然是 proxmox-ve这个包:里面大概率装了一大堆 Proxmox 的规则 TODO

2025/10/13
articleCard.readMore

AI 时代的自我

自从 GPT3 腾空出世以来已经过了快三年了,大家对 GPT3 这些 LLM的态度也产生了变化:它什么都做不了、它好像能做好一些事情、它要取代我了、它不是什么都能做好。 这十分符合技术成熟度曲线,我想我们,或者说我,已经到达了第四个阶段,对于LLM 的态度和看法也发生了些微的变化。 作为一个程序员、工程师,我的作品和工程对我而言意义丰厚:它是我的经验结晶,我的心智模型的具象化,是我对这个任务抽象后又具象化的结果。而LLM正在尝试取代我:它有着人类几乎所有电子化记录下来的经验、知识,它的水平远超于我,而我还依然坚持着我自己写代码,只是利用LLM 作为帮手、知识库。这和当前的潮流背道而驰:我没有将 LLM纳入我的工作流,它只是个旁观者。 但它没有取代我,并且在长远的未来也取代不了我。 在这里我无意讨论技术问题,但要知道一个基础概念:运算资源不是无限的,而计算机的运作很大程度是基于很多个"假如"来运行: 假如这个东西会在多久后被替代:UNIX 时间戳,新千年虫问题 假如进程不会超过多少个:Linux 内核进程管理从位图更换为基树 我作为一个工程师需要长期学习,不断的学习建模方式,强化我对工程的理解,最终我对工程的理解、对世界的构建会成为我的常识,成为我无意识就能做到的事情。但LLM 做不到,我们传递给 LLM的心智模型只会是一些碎片。莫拉维克悖论告诉我们,计算机模拟人类所独有的高阶智慧不需要很多的资源即可完成计算,但模拟人类无意识做出的行为反而需要巨大的算力。 这正好说明了一个现象:我在创造新模式的时候只需要花费大量的时间,因为需要总结我之前的经验,但LLM在这方面并不需要太多时间,他们有十足丰厚的知识来做到这点;但在维护已有模式时LLM 反而表现得不如人类,即使 LLM的知识水平可以说远超过人类。不如人类不是说 LLM的质量不如人类,而是说在同一个质量的情况下和 LLM合作完成一件事所花费的时间是大于熟练的人类工程师,因为熟练的人类工程师在无意识的情况下完成了诸多的工作,这是非常快速且无意识的 这和我在开源社区积累下来的感受也相符合:优质的 PR往往小而完整,他运作在已经构建好的框架之上,符合我们当初建立的心智模型。我的导师经常提示我我应该将一个PR 拆分成为多个 PR,这样每次提交 PR导师们都能轻轻松松的审查这些新增的代码,使其符合项目的心智模型。并且在这个过程中我也会加深对于项目的理解,将实际构建出来的项目转换成为我对于工程的理解,并且建立属于我自己的心智模型。 但 Vibe Coding 的出现打乱了这个良好的循环:我可以说大多数通过Vibe Coding 得到的 PR 并不符合这个良好的循环模式。为什么? 为什么Vibe Coding 打破了建立心智模型的良好的循环模式? 在提交 PR之前需要对项目有一定程度的了解,即使这个了解是错误的。这和培养人才、工程师的线路是一致的,需要他们对于项目有一定的了解。但Vibe Coding,或者说依赖 LLM 来做这件事很显然是一种"偷懒"。 当有开发能力的用户萌发出一个想法,或者他觉得他有能力解决一个开源软件的缺陷、有能力贡献出一个新功能,那么他就必须阅读相关的代码。当今LLM 可以代替他做到这一点,替他完成阅读并且修补缺陷、实现新功能 然后压力就来到我们维护者身上了:我们需要知道这些代码到底是不是符合我们的、项目的心智模型。想象一下,一个PR出色的完成了他所述的任务,但你仔细看代码,你发现了不少代码晦涩难懂,于是你作为维护者需要询问贡献者这些代码为什么、能不能有更好的写法,但贡献者贴上来一段LLM 生成的解释,此时你是什么想法? 好的结果,就是你这些代码你理解了并且有能力改写出更简单的代码,这些代码符合了工程的模型。 坏结果,这些代码依然晦涩难懂,并且提交代码的贡献者再也没有了回信,PR进入了 miss-assignment 状态。 很遗憾的是,在当前 LLM 或者说 Vibe Coding的水平之下,后者是常态。 在阅读他人博客的时候,有幸读到一篇十分有价值的论文,Programming as TheoryBuilding programming properly should be regarded as an activity by which theprogrammers form or achieve a certain kind of insight, a theory, of thematters at hand 正确的编程应当被视为一种活动,通过它,程序员形成或获得对所处理问题的某种洞察、一种理论。 我想这是大多数程序员、学生都应该要有的一种基础意识。Vibe Coding的出现打破了这个健康的循环模式,甚至使得不少程序员也失去了这个基础意识。 但事情并不是绝对的:开源项目可以看作是我自己的一亩三分地,我愿意花费巨量的心血去维护;但我还有工作,而工作只在乎我的产出,因此需要我提高效率。 回到我刚刚提到的一点,工程师在接手一个项目的时候很难快速建立正确的完整的心智模型,但是在LLM 的帮助下却能快速建立一个简单的、大概正确的心智模型:通过询问LLM的方式了解这一部分代码的工作,在单元测试和回归测试中验证对这部分代码的修改是否正确。 在互联网上,Vibe Coding 只是 LLM开始大规模应用的一种表现,而有趣的是,还有另外一种表现。 "@grok, check fact." 在推特的一些消息类型、历史讲解的一些帖子之下我们经常能看到有人向 Grok查询事实,我觉得这是一个很有趣的现象:这样和 Vibe Coding本质是一件事,只是不 coding 而已。 让 Grok 来搜索数据,整合数据,然后告诉用户结论,这何尝不也是一种vibe? LLM 有提高我的效率吗? 毋庸置疑地讲,要分开来讨论: 是否是我熟悉的领域? LLM 是否在这方面有十分丰厚的知识积累? LLM 的知识是否过时 很多时候我是在我熟悉的领域工作,这样的时候在编写代码和设计时几乎是无意识的行为,我会利用我熟悉地再熟悉不过的方式来进行建模和编写代码。 但如果我在这个时候插入 Vibe Coding 环节,我需要花费大量的时间去查看LLM写出的代码是否正确;但我对我自己亲手编写的代码有充分自信他几乎是正确的——只要能通过单元测试和回归测试。 我现在还在做系统方面的开发,维护着 uutils 之下的procps,在这个领域 LLM没有帮助我太多忙,即使是我不熟悉的领域。 为什么?因为 LLM 在这方面的知识积累并不多,即使 procps的命令基本上已经存在了二十年左右了,但 LLM对这些命令还是知之甚少,而且还会因为幻觉问题开始考虑奇奇怪怪的边角情况,这些情况很难说能遇到,而我们向来是将这些情况等测试集不通过或者用户汇报有问题的时候再开始调查——这也不符合我们建立心智模型的路径。 换句话来讲,我接触的领域是: LLM 知识过时 LLM 并没有积累太多知识 的领域,所以很难说得上 LLM 在这些事情上有提高我的效率。 所以应该不使用 LLM? 不,这不是我的结论。实际上,我也会利用 LLM 在编程方面帮助我: 帮我编写 FFmpeg 命令行 帮我理解函数的作用 帮我确认注释是否易懂 在这些小方面,LLM 做得十分出色。甚至于在编程之外,我也会利用 LLM来帮我一些忙: 一篇论文给 LLM,使其帮我提炼大纲 看不懂的算法让他解释 但如果我真的想完全理解一个东西的时候,我会选择关闭 LLM。 一个很形象的例子:如果我要去往一个地方,我骑着电瓶车肯定比我骑着自行车更加快速。但如果我其实只想通过骑自行车锻炼自己,那么我骑电瓶车对我有什么好处吗?当然没有。 所以我认为,LLM对于快速探索某个话题,并且更快完成任务方面,他们绝对是无二的好帮手。而如果你想真正的掌握一项技能,你更需要的是去积累丰厚的经验,而不是去找一个捷径——相信我,在长期依赖LLM 之后一旦你失去了 LLM ,你会变得一无所知。 那如果利用 LLM 抹平学习路上的所有困难呢?我觉得利用 LLM抹平路上的一些边角上的困难是可以的,因为在解决这些困难的路上很难说得上学到对我们有使用价值的知识。但如果完全抹平我认为这或许不是一件好事。 人类在解决困难的路途中会不断积累经验,这些经验能构筑独一无二的你我,成为我们成长路上最宝贵的部分。但...如果让LLM替代我们做这件事,或许我们会感到大脑光滑十分爽快,在结束后我们如果回顾路途会感到一无所知和空虚:因为困难全部被LLM 解决了,我们几乎没有参与这件事本身。 但 LLM还有个优点也十分明显,毕竟它叫作大语言模型。 如果你和我一样,将 LLM 作为一个帮手、教科书,你会发现 LLM的一个巨大的优点:几乎所有的 LLM都是有无限的耐心,他们会一直陪着我们,也几乎不会有掉线一说。我能在深夜情绪很差的时候寻找LLM 聊天,说出我的想法;我能在周围没有人可以回答我的问题的时候向 LLM咨询一些事情;完全不擅长社交的我在和 LLM聊天的时候几乎感觉不到任何压力(即使我几乎很少和 LLM 闲聊) 这就是赛博生命(如果你能接受这种叫法的话,或者叫其他的类似的)的优点,我和他不在一个维度,我对他没有心理负担。 我在其他博客中看到个类似的观点: LLM 的出现和当初的 Google 出现有十分的相似之处,在 Google这种搜索引擎出现之前人们只能通过在图书馆中不断的查找书籍和不断的询问教师教授等方式获取资料,但Google出现后改变了这一点:现在人们可以在深夜在互联网上毫无顾虑地查找资料,不需要担心打扰到谁,也能节约大量的时间不再需要去图书馆。 或许一年后我对 LLM 的看法又会变了呢,毕竟离 GPT3横空出世,到现在也不过 2 年。 谨以此文告慰此时于 LLM 时代找到"自我"的我。

2025/8/23
articleCard.readMore

支持删除的布隆过滤器

一般情况下布隆过滤器只能填入不能删除,有些特别的需求比如支持读写删的系统就会需要支持删除的布隆过滤器 原始布隆过滤器 我们先看看原始的布隆过滤器是个怎么回事 前提假设 原始的布隆过滤器有几个很重要的点,其实是个非常简单的东西 一个哈希函数:\(hash(T)\) 注意,该哈希函数最好稳定不需要种子,摘要哈希最好。 一个可以被位操作的数据类型 \(T\)(也可以叫做向量?毕竟实际上是由一个十分长的位组成的东西) 一个统计有多少位相同的算法:\(clac(T,T)\) 原始的布隆过滤器使用多个位来作为其内部数据表示,我们这里使用u8 作为 \(T\)的实际类型 1 0000_0000 我们使用上文的 \(hash()\)函数来对输入 \(input\) 做如下处理 \[ result=hash(input) \] 则我们同样得到了一个类型为u8 的 \(result\) ,其内容为0000_0001 判断是否存在 在得到哈希结果后,我们利用这个结果来进行判断 1 2 I: 0000_0001 B: 0000_0000 我们使用的数据类型总共有 8 位,其中相等的位数为7 位,并不完全相等,那么我们可以认为这个元素肯定不存在 写入数据 我们将这个数组写入到这个我们的过滤器里,这样我们的过滤器里的内容就变成了如下内容 1 0000_0001 假如有新的数据,我们就继续按照判断是否存在来判断是否存在于过滤器中 如果需要写入数据,我们就按照上文来写入数据 假阳性(FalsePositive)与误判率 假阳性(误判) 考虑到一种情况,我们的布隆过滤器满了 1 1111_1111 这个时候无论对什么数据进行哈希,得到的结果都会被布隆过滤器判定为存在。那么这里就产生了很重要的问题:误判 误判率 我们做如下假设 布隆过滤器的总位数为 \(m\) 函数 \(hash\) 产生的结果为 \(result\) \(result\) 的位数为 \(k\) 则在插入时布隆过滤器中的某一个特定的位没有被操作的概率是 \[ 1-\frac{1}{m} \] 在完全插入\(result\) 后某一位仍然为0 的概率为 \[ (1-\frac{1}{m})^{k} \] 在执行了 \(n\)次插入后,某一位仍然为 0 的概率就为 \[(1-\frac{1}{m})^{kn}\] 那么很容易得出,某一位为 1 的概率是 \[1-(1-\frac{1}{m})^{kn}\] 我们假设过滤器中所有位均被设置为1,那么认为某一个原本不应该在集合中的元素却被误判的概率则为 \[P=(1-[1-\frac{1}{m}]^{kn})^k\]则我们得到的误判率计算公式为 \(P\)。对于该公式,我们假设 \(m\)趋近于无限,则我们可以进行进一步的处理:令 \(L=(1-\frac{1}{m})^{kn}\),我们对\(L\)进行极限,根据极限的定义我们可以得到如下的结果 \[\lim_{m\to\infty}(L)\approx{e^{-1}}^{\frac{kn}{m}}\] 什么?忘了指数函数的极限了? 简单回忆一下一般形式的指数函数的极限: \[\lim_{m\to\infty}(1-\frac{x}{m})^m=e^{-x}\] 那么我们可以得到其误判率大概为 \[P\approx(1-e^{-x})^k\]

2025/7/29
articleCard.readMore

基于栈的虚拟机与基于寄存器的虚拟机

经常听到 LuaJIT 和 JVM分别是基于寄存器的虚拟机和基于栈的虚拟机,那么这两种虚拟机究竟有什么区别? 最主要的区别 既然都叫作虚拟机了,那么实际上这些 Runtime的最终目的是模仿实体机提供一个可以跨平台运行的环境。 基于寄存器的操作 读过计组和 CSAPP 的朋友们都知道,我们的 x86 机器指令存在一个东西叫作N 地址指令,他看起来像是这样 N=3: ADD a, b, c N=2: ADD a, b ADD 指令后的地址就是目标的地址,并且 ADD命令在完成后会在某个寄存器/内存里放入这次运算的值。经典的 x86采用的是二地址指令,会在某个寄存器里放入这个值 基于栈的操作 那么如果我们的 N 地址 N=0会发生什么?看起来我们没有任何地址可以存放读取。 但如果我们引入一个数据结构来储存值,操作数仅仅能操作当前最新的值会发生什么?基于栈的虚拟机就产生了。 求值栈 我们引入一个栈式结构叫作求值栈,并且假定有如下的命令 1 2 3 4 iconst_1   iconst_2   iadd   istore_0 在 iconst_1 和 iconst_2中我们压入了两个整型值:1,2 然后我们使用了 iadd 命令,iadd命令将会弹出栈顶的两个整型值,并且将其相加后压入栈顶,这样我们的求值栈就只有一个值2 了 在完成以上求值后,我们使用了 istore命令将求值栈最终的值放到局部变量区的 0 号位 istore, istore_<n>: Anistore instruction with operand Index is type safeand yields an outgoing type state NextStackFrame, if astore instruction with operand Index and typeint is type safe and yields an outgoing type stateNextStackFrame. iadd: An iadd instruction is type safe iff onecan validly replace types matching int and inton the incoming operand stack with int yielding theoutgoing type state. 这样看起来就非常的跨平台,因为到目前为止几乎完全不用和平台的寄存器交互,这几乎完全是模拟出来的行为 如果要用 x86 汇编的话,两条指令就可以了: 1 2 mov  eax, 1   add  eax, 2 请注意,求值栈(EvaluateStack)不是一个固定的名字,除了求值栈,还可以叫作表达式栈(ExpressionStack) 各有优劣? 简单列个表会更直观一些 请注意,设计模式并不是固定的,例如 JVM 的 JIT也会面向平台进行特化,这一步实际上也是走向基于寄存器的虚拟机的设计模式,因为这样很明显会更快 基于栈的虚拟机基于寄存器的虚拟机理由 可移植性高低基于寄存器的虚拟机由于需要考虑和本地机器的映射关系,因此需要花很大力气去整理映射关系,但基于栈的虚拟机不需要这层映射关系 实现难易度简单难同上 性能较慢快速基于寄存器的虚拟机由于存在和本地机器的寄存器映射关系,因此运行起来非常的高效,基于栈的虚拟机有额外开销 储存资源占用较低较高由于基于栈的虚拟机使用的是零地址指令,它的占用比其他任何指令都小并且更加紧凑。所以基于栈的虚拟机对于硬盘占用会更小 参照 虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 JavaVirtual Machine Specification: 4.10.1.9. Type CheckingInstructions

2025/7/20
articleCard.readMore

RVA23 包含了什么

Ubuntu计划在 25.10 的 RISC-V 架构中只支持 RVA23 配置文件,于是打算大概看下RVA23 有什么是比较重大的指令集成为了必选 For Ubuntu 25.10 release we plan to raise the required RISC-V ISAprofile family to RVA23. The ubuntu-release-upgrader should stop upgrades beyond Ubuntu 24.04on hardware that does not support the RVA23U64 profile. RVA23U64 [1] isthe profile relevant for user space. As there is no upgrade path from Ubuntu 25.04 Plucky for RVA20systems, we should also stop upgrading these RISC-V systems from Nobleto Plucky. Probably a warning is adequate here. RVA23 配置文件旨在规范 RISC-V 64位应用处理器的实现,使二进制软件生态系统能够依赖大量有保障的扩展功能和少量可发现的粗粒度选项。RVA23明确不以支持最小功能集和大量细粒度扩展来实现更大的硬件实现灵活性为目标。 指令集 该系列仅规定了用户模式(RVA23U64)和管理模式(RVA23S64)两种配置方案。 用户模式强制性基础 拓展解释备注 RV64IThe mandatory base ISA for RVA23U64 and islittle-endian.As per the unprivileged architecturespecification, the ECALL instruction causes a requested trap to theexecution environment. 用户模式强制性拓展 拓展解释备注 A原子指令 B位操作指令 C压缩指令 D双精度浮点指令 F单精度浮点指令 M整数乘除法指令 Zkt数据无关执行延迟控制 ZicsrCSR 控制状态寄存器指令(F扩展隐含此功能) Zihpm硬件性能计数器 Za64rs保留集必须为 64 字节连续自然对齐空间 Zfhmin半精度浮点运算 Zic64b缓存块必须为 64字节大小且在地址空间中自然对齐 Zicbom缓存块管理指令 Zicbop缓存块预取指令 Zicboz缓存块清零指令 Ziccif具有可缓存性和一致性的主存区域必须支持指令获取,且对自然对齐的2 的幂大小(最大 min(ILEN,XLEN),RVA23 为 32位)的指令获取必须是原子的 Zicclsm必须支持对具有可缓存性和一致性的主存区域进行非对齐加载/存储操作 Ziccrse具有可缓存性和一致性的主存区域必须支持RsrvEventual 特性 Zicntr基础计数器和定时器 Ziccamoa具有可缓存性和一致性的主存区域必须支持 A扩展中的所有原子操作 Zihintpause暂停提示指令 RVA23U64 新增了以下强制性扩展: 拓展解释备注 V向量扩展注:V 在 RVA22U64 中是可选扩展 Zcb额外压缩指令集 Zfa额外浮点指令集 Supm指针掩码功能,执行环境至少需支持 PMLEN=0和 PMLEN=7 的设置 Zawrs等待保留集指令 Zcmop压缩型可能操作指令 Zimop可能操作指令 Zvbb向量基础位操作指令 Zvkt向量数据无关执行延迟控制 Zicond整数条件操作指令 Zvfhmin向量最小半精度浮点运算 Zihintntl非临时局部性提示指令 用户模式可选拓展 拓展解释备注 Zbc标量无进位乘法 Zfh标量半精度浮点运算包含在 RVA22U64 标准中 Zvbc向量无进位乘法 Zvfh向量半精度浮点运算 Zabha字节与半字原子内存操作 Zacas比较并交换指令 Zvkng带 GCM 的向量密码学 NIST 算法标量密码扩展 Zkn/Zks已移除;向量密码变为强制要求且性能显著提升;仅包含 GCM版本以优化性能 Zvksg带 GCM 的向量密码学商密算法同 Zvkng Zama16b未跨越 16字节边界的非对齐加载/存储/原子内存操作保持原子性实现非对齐原子粒度(Sm1p13);将加入特权架构的PMA 章节 Zfbfmin标量 BF16 格式转换 Ziccamoc具有可缓存性/一致性 PMA的主存区域必须支持AMOCASQ级别的 PMA确保支持 CAS 指令;将加入特权架构的 PMA章节 Zicfilp着陆垫机制 Zicfiss影子栈 Zvfbfmin向量 BF16 格式转换 Zvfbfwma向量 BF16 扩展乘加运算 管理模式强制性拓展 RVA23S64 除了包含所有 RVA23U64的所有强制性拓展外还包含了如下的强制性拓展 拓展解释备注 Ss1p13监管者架构版本 1.13Ss1p13 取代了 Ss1p12。 Ssccptr具有可缓存性和一致性 PMA 的主存区域必须支持硬件页表读取 Sscofpmf计数器溢出和基于模式的过滤 Sscounterenw对于任何非只读零的hpmcounter,scounteren中的相应位必须可写 Ssnpm指针掩码,senvcfg.PMM和henvcfg.PMM至少支持PMLEN=0 和 PMLEN=7 的设置 Sstc监管者模式定时器中断Sstc 在 RVA22 中是可选的 Sstvala对于加载、存储和指令页面错误、访问错误及不对齐异常,以及由非EBREAK或C.EBREAK指令执行引起的断点异常,stval必须写入引发错误的虚拟地址。对于虚拟指令和非法指令异常,stval必须写入引发错误的指令 Sstvecdstvec.MODE必须能够保存值0(直接模式)。当stvec.MODE=Direct时,stvec.BASE必须能够保存任何有效的四字节对齐地址 Ssu64xlsstatus.UXL必须能够保存值 2(即必须支持 UXLEN=64)Ssu64xl 在 RVA22 中是可选的 Sv39基于页的 39 位虚拟内存系统 Svade当 A 位清零时访问页面或 D 位清零时写入页面会引发页面错误异常 Svbare必须支持satp模式的 Bare Svinval细粒度地址转换缓存无效化 SvnapotNAPOT 转换连续性Svnapot 在 RVA22 中是可选的 Svpbmt基于页的内存类型 Zifencei指令获取屏障Zifencei 是 RVA23应用处理器中支持指令缓存一致性的唯一标准方式,因此被强制要求。新的指令缓存一致性机制正在开发中(暂定名为Zjid),未来可能作为可选功能加入。 其中 Sha 拓展包含了如下拓展 拓展解释备注 H虚拟机监控程序扩展 Shcounterenw对于任何非只读零的hpmcounter,hcounteren中的相应位必须可写 Shgatpa对于satp中支持的每个虚拟内存方案 SvNN,必须支持相应的hgatp SvNNx4 模式。同时必须支持hgatp的 Bare 模式增强型虚拟机监控程序扩展(完全等同于 Sha)在 RVA22 中是可选的 Shtvala在 ISA允许的所有情况下,htval必须写入引发错误的客户机物理地址 Shvsatpasatp中支持的所有转换模式都必须在vsatp中支持 Shvstvala在上述所有stval描述的情况下,都必须写入vstval Shvstvecdvstvec.MODE必须能够保存值0(直接模式)。当vstvec.MODE=Direct时,vstvec.BASE必须能够保存任何有效的四字节对齐地址 Ssstateen状态使能扩展的监管者模式视图。必须提供监管者模式(sstateen0-3)和虚拟机监控程序模式(hstateen0-3)的状态使能寄存器 管理模式可选拓展 拓展解释备注 Sdtrig调试触发器 Sspm监管者模式指针掩码,执行环境至少需支持 PMLEN=0 和 PMLEN=7的设置 Ssstrict不存在非标准扩展。尝试执行未实现的指令或访问标准/保留编码空间中未实现的CSR 将触发非法指令异常Ssstrict 是新的配置文件定义扩展,用于限制保留编码空间的行为。注1:Ssstrict 不规定自定义编码空间或 CSR 的行为。注 2:Ssstrict定义适用于声称兼容 RVA23的执行环境,该环境必须包含虚拟机监控程序扩展。 Sv48基于页的 48 位虚拟内存系统 Sv57基于页的 57 位虚拟内存系统 Svadu硬件自动更新 A/D 位 Svvptc从无效 PTE 到有效 PTE的转换将在有限时间内可见,无需显式内存管理屏障 Zkr熵 CSR 观测 对于 RVA23 来讲,最显著的是之前 RVA22 的 V拓展现在是强制性的,我想这也是面向 AI 时代的一个必然。如果一个 CPU没有向量加速相关的拓展那么这个 CPU 的效率是无法达到现代的标准的。 除此之外,Sha 现在也成为了必选,这是在 RISC-V上运行虚拟机的基础 这也是 RVA23 的主打卖点: Vector Extension: The Vector extension accelerates math-intensiveworkloads, including AI/ML, cryptography, and compression /decompression. Vector extensions yield better performance in mobile andcomputing applications with RVA23 as the baseline requirement for theAndroid RISC-V ABI. Hypervisor Extension: The Hypervisor extension will enablevirtualization for enterprise workloads in both on-premises server andcloud computing applications. This will accelerate the development ofRISC-V-based enterprise hardware, operating systems, and softwareworkloads. The Hypervisor extension will also provide better securityfor mobile applications by separating secure and non-securecomponents. 和 x86_64v4 的区别 由于我用的是 Zen4 系列的 Ryzen9 7945HX,所以以我的 CPU 为标准 包含如下指令集 fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov patpse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gbrdtscp lm constant_tsc rep_good amd_lbr_v2 nopl xtopology nonstop_tsccpuid extd_apicid aperfmperf rapl pni pclmulqdq monitor ssse3 fma cx16sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand lahf_lmcmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetchosvw ibs skinit wdt tce topoext perfctr_core perfctr_nb bpextperfctr_llc mwaitx cpb cat_l3 cdp_l3 hw_pstate ssbd mba perfmon_v2 ibrsibpb stibp ibrs_enhanced vmmcall fsgsbase bmi1 avx2 smep bmi2 ermsinvpcid cqm rdt_a avx512f avx512dq rdseed adx smap avx512ifma clflushoptclwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsavescqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local user_shstk avx512_bf16clzero irperf xsaveerptr rdpru wbnoinvd cppc arat npt lbrv svm_locknrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilterpfthreshold avic vgif x2avic v_spec_ctrl vnmi avx512vbmi umip pku ospkeavx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalgavx512_vpopcntdq rdpid overflow_recov succor smca fsrm flush_l1damd_lbr_pmc_freeze 可以注意到,x86_64v4上甚至有能效控制相关的指令集:apic, cppc,rapl, monitor。但是 RVA23 是没有的 以上只是非常主观的观测,指令集的指令还有更细致的区别但是因为太多了(而且我不太懂)就算了 不足之处? 指令集只是硬件层面的一部分,在硬件之上的软件支持也十分重要。这一点很依赖编译器和上游软件的实现,很可惜的是截至当前编译器和上游软件对RVA23 的支持并不明朗,请查看 Yangyu Chen 的研究,原因讲的非常详细

2025/7/12
articleCard.readMore

Rust 的边界检查是否已经有很大的进步

最近又听到有人说 Rust做不了高性能程序的原因是内插的边界检查会降低程序运行速度,我想这么多年了应该Rust开发团队不会不知道这个问题,因此做了个简单的实验来测试一下是不是这方面已经有了长足的进步 源码 我们有如下代码,该代码为N 皇后问题的算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 use std::time::{SystemTime, UNIX_EPOCH}; const N: i32 = 13; fn clock_realtime() -> i64 { let start = SystemTime::now(); let since_the_epoch = start .duration_since(UNIX_EPOCH) .expect("Time went backwards"); since_the_epoch.as_millis() as i64 } fn array_check(array: &[i32], row: i32) -> bool { if row == 0 { true } else { let x0 = array[row as usize]; for y in 0..row { let x = array[y as usize]; if x == x0 { return false; } else if x - x0 == row - y { return false; } else if x0 - x == row - y { return false; } } true } } fn queen() -> i32 { let mut array = [0; N as usize]; let mut found = 0; let mut row = 0; let mut done = false; while !done { if array_check(&array, row) { if row == N - 1 { found += 1; } else { row += 1; array[row as usize] = 0; continue; } } array[row as usize] += 1; while array[row as usize] >= N { row -= 1; if row >= 0 { array[row as usize] += 1; } else { done = true; break; } } } found } fn main() { queen(); let ts = clock_realtime(); let found = queen(); let dt = clock_realtime() - ts; println!("found={} time={} ms", found, dt); } 我们需要查看汇编,看看 panic 相关的汇编是不是变少 从汇编角度分析 在 Rust 1.88 得到的涉及到边界检查的汇编如下 1 2 3 4 .LBB6_40: leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.7(%rip), %rdx movl$13, %esi callq*_ZN4core9panicking18panic_bounds_check17hda0827d94e974e71E@GOTPCREL(%rip) 在 Rust 1.42 得到的涉及到边界检查的汇编如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 .LBB4_19: .cfi_def_cfa_offset 64 leaq.L__unnamed_2(%rip), %rdi movl$13, %esi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 .LBB4_25: leaq.L__unnamed_3(%rip), %rdi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 .LBB4_17: leaq.L__unnamed_4(%rip), %rdi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 .LBB4_15: leaq.L__unnamed_5(%rip), %rdi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 .LBB4_9: leaq.L__unnamed_6(%rip), %rdi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 可以看到很明显是从五个减少到了一个,说明 Rust 本身也在不断进步 附录 1.完整的 Rust 1.88.0 输出汇编 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 .file"boundtest.375cfcca86286996-cgu.0" .section.text._ZN3std2rt10lang_start17h3c102020d822bd07E,"ax",@progbits .hidden_ZN3std2rt10lang_start17h3c102020d822bd07E .globl_ZN3std2rt10lang_start17h3c102020d822bd07E .p2align4 .type_ZN3std2rt10lang_start17h3c102020d822bd07E,@function _ZN3std2rt10lang_start17h3c102020d822bd07E: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 movl%ecx, %r8d movq%rdx, %rcx movq%rsi, %rdx movq%rdi, (%rsp) leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.0(%rip), %rsi movq%rsp, %rdi callq*_ZN3std2rt19lang_start_internal17ha8ef919ae4984948E@GOTPCREL(%rip) popq%rcx .cfi_def_cfa_offset 8 retq .Lfunc_end0: .size_ZN3std2rt10lang_start17h3c102020d822bd07E, .Lfunc_end0-_ZN3std2rt10lang_start17h3c102020d822bd07E .cfi_endproc .section".text._ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h93d859901d9a3bf1E","ax",@progbits .p2align4 .type_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h93d859901d9a3bf1E,@function _ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h93d859901d9a3bf1E: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 movq(%rdi), %rdi callq_ZN3std3sys9backtrace28__rust_begin_short_backtrace17h8132116926a6755eE xorl%eax, %eax popq%rcx .cfi_def_cfa_offset 8 retq .Lfunc_end1: .size_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h93d859901d9a3bf1E, .Lfunc_end1-_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h93d859901d9a3bf1E .cfi_endproc .section.text._ZN3std3sys9backtrace28__rust_begin_short_backtrace17h8132116926a6755eE,"ax",@progbits .p2align4 .type_ZN3std3sys9backtrace28__rust_begin_short_backtrace17h8132116926a6755eE,@function _ZN3std3sys9backtrace28__rust_begin_short_backtrace17h8132116926a6755eE: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 callq*%rdi #APP #NO_APP popq%rax .cfi_def_cfa_offset 8 retq .Lfunc_end2: .size_ZN3std3sys9backtrace28__rust_begin_short_backtrace17h8132116926a6755eE, .Lfunc_end2-_ZN3std3sys9backtrace28__rust_begin_short_backtrace17h8132116926a6755eE .cfi_endproc .section".text._ZN42_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$3fmt17haf17d8060b4e03d4E","ax",@progbits .p2align4 .type_ZN42_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$3fmt17haf17d8060b4e03d4E,@function _ZN42_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$3fmt17haf17d8060b4e03d4E: .cfi_startproc movq(%rdi), %rdi jmpq*_ZN57_$LT$core..time..Duration$u20$as$u20$core..fmt..Debug$GT$3fmt17h7281b74b0c14c846E@GOTPCREL(%rip) .Lfunc_end3: .size_ZN42_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$3fmt17haf17d8060b4e03d4E, .Lfunc_end3-_ZN42_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$3fmt17haf17d8060b4e03d4E .cfi_endproc .section".text._ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17hfb43ae5ef4974b72E","ax",@progbits .p2align4 .type_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17hfb43ae5ef4974b72E,@function _ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17hfb43ae5ef4974b72E: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 movq(%rdi), %rdi callq_ZN3std3sys9backtrace28__rust_begin_short_backtrace17h8132116926a6755eE xorl%eax, %eax popq%rcx .cfi_def_cfa_offset 8 retq .Lfunc_end4: .size_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17hfb43ae5ef4974b72E, .Lfunc_end4-_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17hfb43ae5ef4974b72E .cfi_endproc .section".text._ZN63_$LT$std..time..SystemTimeError$u20$as$u20$core..fmt..Debug$GT$3fmt17hdc81834098293439E","ax",@progbits .p2align4 .type_ZN63_$LT$std..time..SystemTimeError$u20$as$u20$core..fmt..Debug$GT$3fmt17hdc81834098293439E,@function _ZN63_$LT$std..time..SystemTimeError$u20$as$u20$core..fmt..Debug$GT$3fmt17hdc81834098293439E: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 movq%rsi, %rax movq%rdi, (%rsp) leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.3(%rip), %rsi leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.2(%rip), %r8 movq%rsp, %rcx movl$15, %edx movq%rax, %rdi callq*_ZN4core3fmt9Formatter25debug_tuple_field1_finish17h06aa1c014801bdacE@GOTPCREL(%rip) popq%rcx .cfi_def_cfa_offset 8 retq .Lfunc_end5: .size_ZN63_$LT$std..time..SystemTimeError$u20$as$u20$core..fmt..Debug$GT$3fmt17hdc81834098293439E, .Lfunc_end5-_ZN63_$LT$std..time..SystemTimeError$u20$as$u20$core..fmt..Debug$GT$3fmt17hdc81834098293439E .cfi_endproc .section.text._ZN9boundtest4main17ha95ec5995628f7dfE,"ax",@progbits .p2align4 .type_ZN9boundtest4main17ha95ec5995628f7dfE,@function _ZN9boundtest4main17ha95ec5995628f7dfE: .cfi_startproc pushq%rbp .cfi_def_cfa_offset 16 pushq%r14 .cfi_def_cfa_offset 24 pushq%rbx .cfi_def_cfa_offset 32 subq$112, %rsp .cfi_def_cfa_offset 144 .cfi_offset %rbx, -32 .cfi_offset %r14, -24 .cfi_offset %rbp, -16 xorps%xmm0, %xmm0 movaps%xmm0, 32(%rsp) movaps%xmm0, 16(%rsp) movaps%xmm0, (%rsp) movl$0, 48(%rsp) xorl%edx, %edx movq%rsp, %rax jmp.LBB6_1 .p2align4 .LBB6_3: movl$1, %edx movl$1, %ecx movl$0, (%rsp,%rcx,4) .LBB6_1: movslq%edx, %rdi movl%edi, %ecx jmp.LBB6_2 .p2align4 .LBB6_10: incl%esi movl%esi, (%rsp,%rcx,4) cmpl$13, %esi jge.LBB6_14 .LBB6_2: testl%edx, %edx je.LBB6_3 cmpl$12, %ecx ja.LBB6_40 movl(%rsp,%rdi,4), %esi movq%rax, %r8 movq%rcx, %r10 movq%rcx, %r9 .p2align4 .LBB6_6: subq$1, %r9 jb.LBB6_11 movl(%r8), %r11d movl%r11d, %ebx subl%esi, %ebx je.LBB6_10 cmpl%ebx, %r10d je.LBB6_10 movl%esi, %ebx subl%r11d, %ebx addq$4, %r8 cmpl%ebx, %r10d movq%r9, %r10 jne.LBB6_6 jmp.LBB6_10 .p2align4 .LBB6_11: cmpl$12, %edx jne.LBB6_17 incl48(%rsp) movl(%rsp,%rcx,4), %esi cmpl$13, %esi jl.LBB6_2 .p2align4 .LBB6_14: subq$1, %rcx jb.LBB6_19 movl(%rsp,%rcx,4), %edx incl%edx movl%edx, (%rsp,%rcx,4) cmpl$12, %edx jg.LBB6_14 movl%ecx, %edx jmp.LBB6_1 .LBB6_17: incl%ecx movl%ecx, %edx movl$0, (%rsp,%rcx,4) jmp.LBB6_1 .LBB6_19: callq*_ZN3std4time10SystemTime3now17h1481857dbd8e3482E@GOTPCREL(%rip) movq%rax, 64(%rsp) movl%edx, 72(%rsp) xorl%ebx, %ebx movq%rsp, %rdi leaq64(%rsp), %rsi xorl%edx, %edx xorl%ecx, %ecx callq*_ZN3std4time10SystemTime14duration_since17hf6dc8eb19471745bE@GOTPCREL(%rip) cmpl$1, (%rsp) je.LBB6_39 movq8(%rsp), %r14 movl16(%rsp), %ebp xorps%xmm0, %xmm0 movaps%xmm0, 32(%rsp) movaps%xmm0, 16(%rsp) movaps%xmm0, (%rsp) movl$0, 48(%rsp) xorl%eax, %eax jmp.LBB6_21 .p2align4 .LBB6_22: movl$1, %ecx .LBB6_37: movl$0, (%rsp,%rcx,4) incl%eax .LBB6_21: testl%eax, %eax je.LBB6_22 movslq%eax, %rdi cmpl$12, %eax ja.LBB6_40 movl%edi, %ecx movl(%rsp,%rdi,4), %edx movl%eax, %edi xorl%esi, %esi .p2align4 .LBB6_25: cmpq%rsi, %rcx je.LBB6_30 movl(%rsp,%rsi,4), %r8d movl%r8d, %r9d subl%edx, %r9d je.LBB6_29 cmpl%r9d, %edi je.LBB6_29 incq%rsi movl%edx, %r9d subl%r8d, %r9d leal-1(%rdi), %r8d cmpl%r9d, %edi movl%r8d, %edi jne.LBB6_25 .LBB6_29: incl%edx movl%edx, (%rsp,%rcx,4) cmpl$13, %edx jl.LBB6_21 .p2align4 .LBB6_33: subq$1, %rcx jb.LBB6_38 movl(%rsp,%rcx,4), %eax incl%eax movl%eax, (%rsp,%rcx,4) cmpl$12, %eax jg.LBB6_33 movl%ecx, %eax jmp.LBB6_21 .p2align4 .LBB6_30: cmpl$12, %eax je.LBB6_31 leal1(%rax), %ecx jmp.LBB6_37 .LBB6_31: incl%ebx incl48(%rsp) movl(%rsp,%rcx,4), %edx cmpl$13, %edx jl.LBB6_21 jmp.LBB6_33 .LBB6_38: movl%ebx, 60(%rsp) callq*_ZN3std4time10SystemTime3now17h1481857dbd8e3482E@GOTPCREL(%rip) movq%rax, 64(%rsp) movl%edx, 72(%rsp) movq%rsp, %rdi leaq64(%rsp), %rbx movq%rbx, %rsi xorl%edx, %edx xorl%ecx, %ecx callq*_ZN3std4time10SystemTime14duration_since17hf6dc8eb19471745bE@GOTPCREL(%rip) cmpl$1, (%rsp) je.LBB6_39 movl%ebp, %eax imulq$1125899907, %rax, %rax shrq$50, %rax movq8(%rsp), %rcx subq%r14, %rcx movl16(%rsp), %edx imulq$1125899907, %rdx, %rdx shrq$50, %rdx subq%rax, %rdx imulq$1000, %rcx, %rax addq%rdx, %rax movq%rax, 64(%rsp) leaq60(%rsp), %rax movq%rax, 80(%rsp) movq_ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i32$GT$3fmt17h863d77ac4b43588eE@GOTPCREL(%rip), %rax movq%rax, 88(%rsp) movq%rbx, 96(%rsp) movq_ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i64$GT$3fmt17hde631ae64c57a835E@GOTPCREL(%rip), %rax movq%rax, 104(%rsp) leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.11(%rip), %rax movq%rax, (%rsp) movq$3, 8(%rsp) movq$0, 32(%rsp) leaq80(%rsp), %rax movq%rax, 16(%rsp) movq$2, 24(%rsp) movq%rsp, %rdi callq*_ZN3std2io5stdio6_print17h915f3273edec6464E@GOTPCREL(%rip) addq$112, %rsp .cfi_def_cfa_offset 32 popq%rbx .cfi_def_cfa_offset 24 popq%r14 .cfi_def_cfa_offset 16 popq%rbp .cfi_def_cfa_offset 8 retq .LBB6_39: .cfi_def_cfa_offset 144 movq8(%rsp), %rax movl16(%rsp), %ecx movq%rax, 80(%rsp) movl%ecx, 88(%rsp) leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.4(%rip), %rdi leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.1(%rip), %rcx leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.6(%rip), %r8 leaq80(%rsp), %rdx movl$19, %esi callq*_ZN4core6result13unwrap_failed17h727108008d9f4c9bE@GOTPCREL(%rip) .LBB6_40: leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.7(%rip), %rdx movl$13, %esi callq*_ZN4core9panicking18panic_bounds_check17hda0827d94e974e71E@GOTPCREL(%rip) .Lfunc_end6: .size_ZN9boundtest4main17ha95ec5995628f7dfE, .Lfunc_end6-_ZN9boundtest4main17ha95ec5995628f7dfE .cfi_endproc .section.text.main,"ax",@progbits .globlmain .p2align4 .typemain,@function main: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 movq%rsi, %rcx movslq%edi, %rdx leaq_ZN9boundtest4main17ha95ec5995628f7dfE(%rip), %rax movq%rax, (%rsp) leaq.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.0(%rip), %rsi movq%rsp, %rdi xorl%r8d, %r8d callq*_ZN3std2rt19lang_start_internal17ha8ef919ae4984948E@GOTPCREL(%rip) popq%rcx .cfi_def_cfa_offset 8 retq .Lfunc_end7: .sizemain, .Lfunc_end7-main .cfi_endproc .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.0,@object .section.data.rel.ro..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.0,"aw",@progbits .p2align3, 0x0 .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.0: .asciz"\000\000\000\000\000\000\000\000\b\000\000\000\000\000\000\000\b\000\000\000\000\000\000" .quad_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17hfb43ae5ef4974b72E .quad_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h93d859901d9a3bf1E .quad_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h93d859901d9a3bf1E .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.0, 48 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.1,@object .section.data.rel.ro..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.1,"aw",@progbits .p2align3, 0x0 .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.1: .asciz"\000\000\000\000\000\000\000\000\020\000\000\000\000\000\000\000\b\000\000\000\000\000\000" .quad_ZN63_$LT$std..time..SystemTimeError$u20$as$u20$core..fmt..Debug$GT$3fmt17hdc81834098293439E .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.1, 32 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.2,@object .section.data.rel.ro..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.2,"aw",@progbits .p2align3, 0x0 .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.2: .asciz"\000\000\000\000\000\000\000\000\b\000\000\000\000\000\000\000\b\000\000\000\000\000\000" .quad_ZN42_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$3fmt17haf17d8060b4e03d4E .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.2, 32 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.3,@object .section.rodata..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.3,"a",@progbits .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.3: .ascii"SystemTimeError" .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.3, 15 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.4,@object .section.rodata..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.4,"a",@progbits .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.4: .ascii"Time went backwards" .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.4, 19 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.5,@object .section.rodata..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.5,"a",@progbits .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.5: .ascii"src/main.rs" .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.5, 11 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.6,@object .section.data.rel.ro..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.6,"aw",@progbits .p2align3, 0x0 .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.6: .quad.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.5 .asciz"\013\000\000\000\000\000\000\000\t\000\000\000\n\000\000" .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.6, 24 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.7,@object .section.data.rel.ro..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.7,"aw",@progbits .p2align3, 0x0 .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.7: .quad.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.5 .asciz"\013\000\000\000\000\000\000\000\021\000\000\000\022\000\000" .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.7, 24 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.8,@object .section.rodata..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.8,"a",@progbits .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.8: .ascii"found=" .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.8, 6 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.9,@object .section.rodata..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.9,"a",@progbits .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.9: .ascii" time=" .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.9, 6 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.10,@object .section.rodata.cst4,"aM",@progbits,4 .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.10: .ascii" ms\n" .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.10, 4 .type.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.11,@object .section.data.rel.ro..Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.11,"aw",@progbits .p2align3, 0x0 .Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.11: .quad.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.8 .asciz"\006\000\000\000\000\000\000" .quad.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.9 .asciz"\006\000\000\000\000\000\000" .quad.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.10 .asciz"\004\000\000\000\000\000\000" .size.Lanon.577bbe129332a5e2b73bb2ccd2cfb5e2.11, 48 .ident"rustc version 1.88.0 (6b00bc388 2025-06-23)" .section".note.GNU-stack","",@progbits 2.完整的 Rust 1.42.0 输出汇编 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 .text .file"boundtest.6clktnjp-cgu.0" .section.text._ZN3std2rt10lang_start17h9aebbcf43540bf0fE,"ax",@progbits .hidden_ZN3std2rt10lang_start17h9aebbcf43540bf0fE .globl_ZN3std2rt10lang_start17h9aebbcf43540bf0fE .p2align4, 0x90 .type_ZN3std2rt10lang_start17h9aebbcf43540bf0fE,@function _ZN3std2rt10lang_start17h9aebbcf43540bf0fE: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 movq%rdx, %rcx movq%rsi, %rdx movq%rdi, (%rsp) leaq.L__unnamed_1(%rip), %rsi movq%rsp, %rdi callq*_ZN3std2rt19lang_start_internal17h9cf8802361ad86c2E@GOTPCREL(%rip) popq%rcx .cfi_def_cfa_offset 8 retq .Lfunc_end0: .size_ZN3std2rt10lang_start17h9aebbcf43540bf0fE, .Lfunc_end0-_ZN3std2rt10lang_start17h9aebbcf43540bf0fE .cfi_endproc .section".text._ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17hfcc61cdeabb58ae0E","ax",@progbits .p2align4, 0x90 .type_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17hfcc61cdeabb58ae0E,@function _ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17hfcc61cdeabb58ae0E: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 callq*(%rdi) xorl%eax, %eax popq%rcx .cfi_def_cfa_offset 8 retq .Lfunc_end1: .size_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17hfcc61cdeabb58ae0E, .Lfunc_end1-_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17hfcc61cdeabb58ae0E .cfi_endproc .section".text._ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17h85cd4d27ce61189fE","ax",@progbits .p2align4, 0x90 .type_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17h85cd4d27ce61189fE,@function _ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17h85cd4d27ce61189fE: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 callq*(%rdi) xorl%eax, %eax popq%rcx .cfi_def_cfa_offset 8 retq .Lfunc_end2: .size_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17h85cd4d27ce61189fE, .Lfunc_end2-_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17h85cd4d27ce61189fE .cfi_endproc .section.text._ZN4core3ptr13drop_in_place17h8206b4e5dd5bef15E,"ax",@progbits .p2align4, 0x90 .type_ZN4core3ptr13drop_in_place17h8206b4e5dd5bef15E,@function _ZN4core3ptr13drop_in_place17h8206b4e5dd5bef15E: .cfi_startproc retq .Lfunc_end3: .size_ZN4core3ptr13drop_in_place17h8206b4e5dd5bef15E, .Lfunc_end3-_ZN4core3ptr13drop_in_place17h8206b4e5dd5bef15E .cfi_endproc .section.text._ZN9boundtest5queen17h2a40cedb5ec97973E,"ax",@progbits .p2align4, 0x90 .type_ZN9boundtest5queen17h2a40cedb5ec97973E,@function _ZN9boundtest5queen17h2a40cedb5ec97973E: .cfi_startproc subq$56, %rsp .cfi_def_cfa_offset 64 xorps%xmm0, %xmm0 movaps%xmm0, 32(%rsp) movaps%xmm0, 16(%rsp) movaps%xmm0, (%rsp) movl$0, 48(%rsp) xorl%eax, %eax xorl%r8d, %r8d testl%r8d, %r8d jne.LBB4_4 .p2align4, 0x90 .LBB4_2: movl$1, %r8d movl$1, %esi .LBB4_3: movl$0, (%rsp,%rsi,4) .LBB4_1: testl%r8d, %r8d je.LBB4_2 .LBB4_4: movslq%r8d, %rsi cmpl$12, %r8d ja.LBB4_17 movl(%rsp,%rsi,4), %r9d movl%r8d, %r10d xorl%edi, %edi .p2align4, 0x90 .LBB4_6: cmpq%rsi, %rdi jge.LBB4_7 cmpq$13, %rdi je.LBB4_19 movl(%rsp,%rdi,4), %ecx movl%ecx, %edx subl%r9d, %edx je.LBB4_14 cmpl%edx, %r10d je.LBB4_14 addq$1, %rdi movl%r9d, %edx subl%ecx, %edx leal-1(%r10), %ecx cmpl%edx, %r10d movl%ecx, %r10d jne.LBB4_6 .LBB4_14: movq%rsi, %rcx cmpl$12, %r8d ja.LBB4_15 addl$1, (%rsp,%rcx,4) cmpl$13, (%rsp,%rsi,4) jl.LBB4_1 .p2align4, 0x90 .LBB4_22: addl$-1, %r8d js.LBB4_16 movslq%r8d, %rsi cmpl$12, %esi ja.LBB4_25 movl(%rsp,%rsi,4), %ecx addl$1, %ecx movl%ecx, (%rsp,%rsi,4) cmpl$12, %ecx jg.LBB4_22 jmp.LBB4_1 .p2align4, 0x90 .LBB4_7: cmpl$12, %r8d je.LBB4_20 addl$1, %r8d movslq%r8d, %rsi cmpl$13, %esi jb.LBB4_3 jmp.LBB4_9 .LBB4_20: addl$1, %eax movl$12, %ecx addl$1, (%rsp,%rcx,4) cmpl$13, (%rsp,%rsi,4) jl.LBB4_1 jmp.LBB4_22 .LBB4_16: addq$56, %rsp .cfi_def_cfa_offset 8 retq .LBB4_19: .cfi_def_cfa_offset 64 leaq.L__unnamed_2(%rip), %rdi movl$13, %esi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 .LBB4_25: leaq.L__unnamed_3(%rip), %rdi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 .LBB4_17: leaq.L__unnamed_4(%rip), %rdi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 .LBB4_15: leaq.L__unnamed_5(%rip), %rdi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 .LBB4_9: leaq.L__unnamed_6(%rip), %rdi movl$13, %edx callq*_ZN4core9panicking18panic_bounds_check17h09b793daa6d169ffE@GOTPCREL(%rip) ud2 .Lfunc_end4: .size_ZN9boundtest5queen17h2a40cedb5ec97973E, .Lfunc_end4-_ZN9boundtest5queen17h2a40cedb5ec97973E .cfi_endproc .section.text._ZN9boundtest4main17h261b80ccf5b33e42E,"ax",@progbits .p2align4, 0x90 .type_ZN9boundtest4main17h261b80ccf5b33e42E,@function _ZN9boundtest4main17h261b80ccf5b33e42E: .cfi_startproc pushq%rbp .cfi_def_cfa_offset 16 pushq%r14 .cfi_def_cfa_offset 24 pushq%rbx .cfi_def_cfa_offset 32 subq$112, %rsp .cfi_def_cfa_offset 144 .cfi_offset %rbx, -32 .cfi_offset %r14, -24 .cfi_offset %rbp, -16 callq_ZN9boundtest5queen17h2a40cedb5ec97973E callq*_ZN3std4time10SystemTime3now17hb1b51de2cdb13891E@GOTPCREL(%rip) movq%rax, 16(%rsp) movq%rdx, 24(%rsp) leaq32(%rsp), %rdi leaq16(%rsp), %rsi xorl%edx, %edx xorl%ecx, %ecx callq*_ZN3std4time10SystemTime14duration_since17he01043b61d61d851E@GOTPCREL(%rip) cmpq$1, 32(%rsp) je.LBB5_3 movq40(%rsp), %rbx movl48(%rsp), %ebp callq_ZN9boundtest5queen17h2a40cedb5ec97973E movl%eax, 12(%rsp) callq*_ZN3std4time10SystemTime3now17hb1b51de2cdb13891E@GOTPCREL(%rip) movq%rax, 16(%rsp) movq%rdx, 24(%rsp) leaq32(%rsp), %rdi leaq16(%rsp), %r14 movq%r14, %rsi xorl%edx, %edx xorl%ecx, %ecx callq*_ZN3std4time10SystemTime14duration_since17he01043b61d61d851E@GOTPCREL(%rip) cmpq$1, 32(%rsp) je.LBB5_3 movl%ebp, %eax imulq$1125899907, %rax, %rax shrq$50, %rax movq40(%rsp), %rcx subq%rbx, %rcx movl48(%rsp), %edx imulq$1125899907, %rdx, %rdx shrq$50, %rdx subq%rax, %rdx imulq$1000, %rcx, %rax addq%rdx, %rax movq%rax, 16(%rsp) leaq12(%rsp), %rax movq%rax, 80(%rsp) movq_ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i32$GT$3fmt17h765415089818841eE@GOTPCREL(%rip), %rax movq%rax, 88(%rsp) movq%r14, 96(%rsp) movq_ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i64$GT$3fmt17h6c2dbda1d476d957E@GOTPCREL(%rip), %rax movq%rax, 104(%rsp) leaq.L__unnamed_7(%rip), %rax movq%rax, 32(%rsp) movq$3, 40(%rsp) movq$0, 48(%rsp) leaq80(%rsp), %rax movq%rax, 64(%rsp) movq$2, 72(%rsp) leaq32(%rsp), %rdi callq*_ZN3std2io5stdio6_print17h7e1d4022dd9ebaeaE@GOTPCREL(%rip) addq$112, %rsp .cfi_def_cfa_offset 32 popq%rbx .cfi_def_cfa_offset 24 popq%r14 .cfi_def_cfa_offset 16 popq%rbp .cfi_def_cfa_offset 8 retq .LBB5_3: .cfi_def_cfa_offset 144 movq40(%rsp), %rax movl48(%rsp), %ecx movq%rax, 80(%rsp) movl%ecx, 88(%rsp) leaq.L__unnamed_8(%rip), %rdi leaq.L__unnamed_9(%rip), %rcx leaq.L__unnamed_10(%rip), %r8 leaq80(%rsp), %rdx movl$19, %esi callq*_ZN4core6result13unwrap_failed17h44d0943ece29c280E@GOTPCREL(%rip) ud2 .Lfunc_end5: .size_ZN9boundtest4main17h261b80ccf5b33e42E, .Lfunc_end5-_ZN9boundtest4main17h261b80ccf5b33e42E .cfi_endproc .section.text.main,"ax",@progbits .globlmain .p2align4, 0x90 .typemain,@function main: .cfi_startproc pushq%rax .cfi_def_cfa_offset 16 movq%rsi, %rcx movslq%edi, %rdx leaq_ZN9boundtest4main17h261b80ccf5b33e42E(%rip), %rax movq%rax, (%rsp) leaq.L__unnamed_1(%rip), %rsi movq%rsp, %rdi callq*_ZN3std2rt19lang_start_internal17h9cf8802361ad86c2E@GOTPCREL(%rip) popq%rcx .cfi_def_cfa_offset 8 retq .Lfunc_end6: .sizemain, .Lfunc_end6-main .cfi_endproc .type.L__unnamed_1,@object .section.data.rel.ro..L__unnamed_1,"aw",@progbits .p2align3 .L__unnamed_1: .quad_ZN4core3ptr13drop_in_place17h8206b4e5dd5bef15E .quad8 .quad8 .quad_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17hfcc61cdeabb58ae0E .quad_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17hfcc61cdeabb58ae0E .quad_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17h85cd4d27ce61189fE .size.L__unnamed_1, 48 .type.L__unnamed_9,@object .section.data.rel.ro..L__unnamed_9,"aw",@progbits .p2align3 .L__unnamed_9: .quad_ZN4core3ptr13drop_in_place17h8206b4e5dd5bef15E .quad16 .quad8 .quad_ZN63_$LT$std..time..SystemTimeError$u20$as$u20$core..fmt..Debug$GT$3fmt17hc5da7e82d27f47abE .size.L__unnamed_9, 32 .type.L__unnamed_8,@object .section.rodata..L__unnamed_8,"a",@progbits .L__unnamed_8: .ascii"Time went backwards" .size.L__unnamed_8, 19 .type.L__unnamed_11,@object .section.rodata..L__unnamed_11,"a",@progbits .L__unnamed_11: .ascii"src/main.rs" .size.L__unnamed_11, 11 .type.L__unnamed_10,@object .section.data.rel.ro..L__unnamed_10,"aw",@progbits .p2align3 .L__unnamed_10: .quad.L__unnamed_11 .asciz"\013\000\000\000\000\000\000\000\007\000\000\000\033\000\000" .size.L__unnamed_10, 24 .type.L__unnamed_4,@object .section.data.rel.ro..L__unnamed_4,"aw",@progbits .p2align3 .L__unnamed_4: .quad.L__unnamed_11 .asciz"\013\000\000\000\000\000\000\000\021\000\000\000\022\000\000" .size.L__unnamed_4, 24 .type.L__unnamed_2,@object .section.data.rel.ro..L__unnamed_2,"aw",@progbits .p2align3 .L__unnamed_2: .quad.L__unnamed_11 .asciz"\013\000\000\000\000\000\000\000\023\000\000\000\025\000\000" .size.L__unnamed_2, 24 .type.L__unnamed_6,@object .section.data.rel.ro..L__unnamed_6,"aw",@progbits .p2align3 .L__unnamed_6: .quad.L__unnamed_11 .asciz"\013\000\000\000\000\000\000\000+\000\000\000\021\000\000" .size.L__unnamed_6, 24 .type.L__unnamed_5,@object .section.data.rel.ro..L__unnamed_5,"aw",@progbits .p2align3 .L__unnamed_5: .quad.L__unnamed_11 .asciz"\013\000\000\000\000\000\000\000/\000\000\000\t\000\000" .size.L__unnamed_5, 24 .type.L__unnamed_3,@object .section.data.rel.ro..L__unnamed_3,"aw",@progbits .p2align3 .L__unnamed_3: .quad.L__unnamed_11 .asciz"\013\000\000\000\000\000\000\0003\000\000\000\021\000\000" .size.L__unnamed_3, 24 .type.L__unnamed_12,@object .section.rodata..L__unnamed_12,"a",@progbits .L__unnamed_12: .ascii"found=" .size.L__unnamed_12, 6 .type.L__unnamed_13,@object .section.rodata..L__unnamed_13,"a",@progbits .L__unnamed_13: .ascii" time=" .size.L__unnamed_13, 6 .type.L__unnamed_14,@object .section.rodata.cst4,"aM",@progbits,4 .L__unnamed_14: .ascii" ms\n" .size.L__unnamed_14, 4 .type.L__unnamed_7,@object .section.data.rel.ro..L__unnamed_7,"aw",@progbits .p2align3 .L__unnamed_7: .quad.L__unnamed_12 .asciz"\006\000\000\000\000\000\000" .quad.L__unnamed_13 .asciz"\006\000\000\000\000\000\000" .quad.L__unnamed_14 .asciz"\004\000\000\000\000\000\000" .size.L__unnamed_7, 48 .section".note.GNU-stack","",@progbits

2025/7/8
articleCard.readMore

使用 Rust 编写操作系统:Barebones

这是 Philipp Oppermann的操作系统系列博客的第一大章节,主讲如何搭建起一个基本的操作系统框架。 独立的 Rust二进制文件 Rust 在 Rust 1.88.0 引入了一个叫作 bare-function的特性用于强化 no_std的开发体验,因此我们基于这个新特性来改进原文中的一些过时之处 panic_impl duplicate 在写完这部分的代码后你会发现一个很奇怪的问题:Rust Analyzer总是提示你你的 panic_impl 实现重复了,因为test 依赖于 std 并且它已经在 std中实现了 found duplicate lang item panic_impl the lang item is first defined in crate std (which test dependson) 这个 ERROR实际上不影响编译,只是会显得很碍眼,可以使用一个简单的方式把他关掉:不使用test 就好了 1 #![cfg(not(test))] Link args 在这一章节提到了编译时需要链接参数,我们可以在.cargo/config.toml 里写上传递给 rustc的参数 1 2 3 4 5 6 7 8 9 10 [target.'cfg(target_os="windows")'] rustflags = [ "-C", "link-args=/ENTRY:_start", "-C", "link-args=/SUBSYSTEM:console", ] [target.'cfg(target_os="linux")'] rustflags = ["-C", "link-arg=-nostartfiles"] 注意,由于该系列的文章是面向 x64 的,所以无法通过基于 Apple Silicon的 macOS 的编译,因此暂时关闭了 macOS 的编译也暂时不支持他。

2025/7/3
articleCard.readMore

使用 Rust 编写操作系统:引言

最近在互联网上冲浪的时候找到了这个博客(其实是朋友发的),感觉很有意思于是决定做一下。 Philipp Oppermann is a freelance Rust developer from Germany. He isworking on the Dora robotic framework and on various projects related tooperating system development in Rust. Philipp is the author of the"Writing an OS in Rust" blog and the main editor of the "This Month inRust OSDev" newsletter. 嗯,看起来挺权威的 XD 梗概 这一系列博客文章算是能覆盖最基础最基础的操作系统概念,根据他的归类可以做如下大纲: Barebones: 基础 独立的 Rust 二进制文件 最小化的 Rust 内核 VGA 字符模式 内核测试 中断 CPU 异常处理 Double Faults 硬件中断 内存管理 内存分页初探 分页实现 堆分配 内存分配器设计 多任务 Async/Await 可以看到这个系列的博客探讨的是一个操作系统的最最基础的几个要素,其他在此之上构建的一些模块并没有包含: 文件系统 IPC 程序链接与执行 它更像是一个跑在单片机上的应用程序。但,我们真实世界的操作系统从脱离单片机进入现代设计也不过三四十年,因此还是有不少可以参考的设计 打算做的一些改进 Philipp Oppermann 在编写博客的时候是 2018 年,那个时候最新也就 Rust2018 Edition,而现在已经是 2024 年了,所以我打算基于最新的 Rust 2024Edition 做些改进 TO BE CONTINUED

2025/7/1
articleCard.readMore

编译器笔记:rope

在实现 paradoxical的时候注意到很多语言服务器或者编辑器都会使用一个叫作 rope的东西来保存对文件的操作结果,因此简单记录下这个神奇的东西。 rope 这个概念最开始是在 1995 年由 Hans-J.Boehm 、 Russ Atkinson 和Michael Plass提出的[0]。如果你想简单了解的话,你可以直接去看xi-editor的相关文档[1],写得非常不错 十分感谢他们的贡献 :) 简单介绍 Rope(绳索字符串)就是把一整段文本切成许多小块,用一棵树把这些块串起来;这样随机插入、删除或定位位置时,只需改动或遍历对数级别的节点,而不是线性扫描整段文本。 请注意,而不是线性扫描整段文本这段话非常重要,这也是和使用String 储存文件内容的最大区别 操作? 对于一个编辑器或者文本解析,我们常常使用行列的概念来操作他。 答案是,对于 rope 我们也可以使用行列来操作他! 为什么 rope 可以实现高效编辑 请注意,rope的主要目的是实现高效的编辑,对于读取他的速度和线性的String 相比是略微慢了一点点的。 但就这个数量级是常数,非常不明显 从储存结构上优化 rope最重要的一点是,他不是线性储存文本数据,转而使用了一棵树来维护储存文本数据。 让我们来看看这棵树长什么样子 1 2 3 4 5 6 ┌─── weight = 23 ───┐ │ │ [左子树] [右子树] weight = 10 weight = 7 │ │ │ │ "片段A" "片段B..." "片段C" "片段D..." 简单解释就是: 左子树(LeftSubtree):按照逻辑顺序(即文本顺序)排在当前节点之前的全部内容 右子树(RightSubtree):排在当前节点(或其左子树)之后、紧接着的那部分内容 请注意,节点内储存的权重不仅仅只有字符数,也包含行数等信息 在接下来我们定义权重分为以下几种 lines: 行权重 chars: 字符权重 偏移 当我们需要快速定位光标的时候,权重就显得非常重要了:它保存了左子树累计长度,若要找第\(k\) 个字符,算法会看 \(k\)是否小于该权重,按照如下逻辑来搜索: 是 : 目标一定在左子树。 否 : 目标必定在右子树,并将 \(k\)减去左权重后继续向下搜索。 除此之外我们还有行列偏移的需求:我们在编辑时经常需要行列->偏移,或者偏移->行列,那么rope 是怎么做的? 行列->偏移 输入行号 \(L\);从根节点开始。 比较 \(L\) 与当前节点的左子树行数: 若 \(L\)小于该权重,进入左子树; 否则 \(L = L -left\_lines\),转入右子树。 重复直到到达叶节点(叶片约 4–8 KB大小)。在叶内部用已缓存的换行表(二分搜索)把剩余行数定位到字节/字符位置。 通过这样的设计,这棵树就把线性扫描的复杂度降低到了 \(O(logN)\) 偏移->行列 从偏移转换到行列也是个很常见的需求,不过这种转换相比行列转换为偏移更加简单 将目标字符索引 \(K\)与字符权重做同样的左/右子树递归 到叶后用换行表计算它之前的换行符数量,加上在路径中累积的 \(left\_lines\) 即得行号。 换行符数量也是可以作为缓存项的,所以这种操作依然非常快速 对 CPU 缓存的充分利用 上文提到,叶片的大小一般是 4-8KB,这样的大小有利于 CPU缓存。 基于 CPU的缓存机制,我们可以使用链表或者其他顺序结构将叶子之间链接起来,在遍历的时候就不需要重走整棵树。 关于为什么这样的大小可以更好的利用 CPU 缓存,本文不做叙述 简答地讲,对于遍历文件内容的本身的时候有如下优化手段 利用链表链接所有叶子节点,在遍历的时候通过链表对叶子节点进行遍历而不是通过爬树来执行访问 利用 CPU会预缓存内存内容的特性使得访问内存的频率减少,降低遍历时使用其他结构因为miss 的原因访问内存 这样既得到了廉价的编辑行为也获得了不俗的遍历性能,代价即为直接读取内容稍慢而已 插入删除等局部更新 在刚刚提到了读取操作,以上的解释注重于解释为什么 rope在读取方面依然性能十分出众但并没有解释为什么 rope在编辑方面的性能远大于使用 String[u8] 等线性数据结构 定位 对于定位,请看 偏移 操作 在到达指定位置后就可以执行对叶片的操作了。对于叶片的操作核心在于需要合适的时机分裂叶片使其成为新的叶片并且添加新的节点 先将光标内的内容插入到这个叶片的缓冲区里,然后做如下判断 如果叶片大于4-8KB:叶片将会分裂成为两个(或者多个)新的叶片 在完成分裂后对叶片添加新的父节点 如果叶片小于 4-8KB:正常插入即可 在完成插入操作后需要回溯所有祖先节点并且完成对祖先节点的权重变更,这样就完成了插入操作 在正常编辑下(只添加一个字符)最多只需要触摸 2 个叶片和 \(O(logN)\)个节点,在特殊情况下(例如剪切板粘贴)会有更复杂的操作,暂且不提(简单讲就是对粘贴内容生成rope 树并且将树挂载到指定位置上) xi-editor 和 ropey在这方面实现上有区别,不介绍两个实现上所使用的树的具体区别 0.Ropes:an Alternative to Strings↩︎ 1.xi-editor↩︎

2025/5/22
articleCard.readMore

编译器笔记:CST

AST(Abstract Syntax Tree)倒是想做编译器的人、不想做编译器的人都会知道一点,但是 CST(ConcreteSyntax Tree) 倒是很少提到,睡不着就简单记录一下吧 有了 AST 为啥还要 CST? AST侧重于表达内容核心结构,他会忽略基本上所有除了内容核心结构以外的所有内容包括: 空格 tag 类型 各种括号 各种引号 关键字 分号 只会保存核心内容,由于抛弃了以上内容,基于 AST可以做我们更耳熟能详并且高级的操作: 语义分析 优化 代码生成 等等等,还有更多。 那么他的缺陷是什么?由于忽略了空格,我们没有办法从AST 直接还原文本————我们失去了原先的对应上文本的信息。 那么基于我们需要还原文本这个前提条件,我们就需要引入完整保存信息了的新的结构,他既可以还原成为原文本,也可以降级(Lowering) 成为 AST便于后续处理 这个时候,CST 就出现了。 CST,为什么能还原文本? 为了简单,我们直接画两张图来说明区别就好 我们假定有如下的表达式 1 a = (b + c) * d; 那么我们的 AST 长这样: 1 2 3 4 5 6 Assign └── LHS: "a" └── RHS: Multiply ├── Add("b", "c") └── "d" 我们的 CST 就长这样: 1 2 3 4 5 6 7 8 9 10 11 Assign └── LHS: Identifier("a") └── Operator("=") └── RHS: │ Multiply │ ├── Parentheses │ │ └── Add │ │ ├── Identifier("b") │ │ └── Identifier("c") │ └── Identifier("d") └── Semicolon 可以清晰的看到,CST 多了很多东西,他是可以被Lowering 到 AST 的 不过为了画起来方便我没画空格,空格实际上也是包含在其中的 :)

2025/5/19
articleCard.readMore

Paradoxical 札记

最近把 neorg 作为笔记和规划系统有点上头,但苦于没一个好用的Language Server,自己又对编译器前端方面略有了解于是决定写下这篇大概会持续更新的札记 我们的最终目的是实现一个不错的 Language Server并且可以在 neovim/helix 上跑起来 为什么是 neovim/helix 呢?因为我暂时不会继续用 VSCode了 流水线设计 考虑到我希望 paradoxical 有如下两个功能 重新格式化 neorg 文档 良好的补全 因此我决定采用如下流水线设计: 1 2 Text → Lexer (RawTokenKind) → CST → Typed AST/HIR → Semantic Analysis → LSP Server → Editor | src | Lazy Lexer | CST | AST | 需要着重说明的是,我们使用 rope来保存和编辑源文件,这样我们所有的操作的时间复杂度都是 \(O(logN)\) 目标 阶段 0 该阶段起始于 2025-05-16 在阶段 0 需要实现最基本的功能:解析 但由于 neorg 的格式内容较多所以在这个阶段只考虑实现一个子集 标题 有序/无序列表 文本段 文本类型 Paragraph:这种类型允许使用 neorg 的修饰符例如{} // $||$等,这些修饰符可以正常执行其对应的功能 逐字类型 VerbatimParagrah: 这种类型的文本段不会处理neorg 的修饰符,所有输入的修饰符都应该原样输出 标签 Tag 请注意,由于 neorg设计上要求的非歧义解析,所有解析失败的内容都应该回归为文本类型 Paragraph Token 设计 基于 Token 的功能和现在的目标,我设计出如下的RawTokenKind 1 2 3 4 5 6 7 8 9 10 11 12 13 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum RawTokenKind { Punct(char), NormalChar(char), Tag, Linending(LinendingKind), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum LinendingKind { LF, CRLF, } 以上的代码省略了对于 Display trait 的实现 Token 解析规则 我们使用 nom这个库来实现解析,因为 neorg的文档风格非常适合使用组合子解析器的方式进行解析。 不自己编写递归下降解析器是因为 neorg 的规范实际上比较简单,而且 neorg的二义性情况很容易被判断,因此我们直接使用组合子解析器来实现解析就好 由于我们需要保留 Token 在文档里的位置信息,因此基于RawTokenKind 派生出 RawToken,添加span 字段保留当前 Token的在文档里的位置信息(以偏移表示) 1 2 3 4 5 #[derive(Debug, Clone)] pub struct RawToken { pub kind: RawTokenKind, pub span: Range<usize>, } 接下来我们开始编写对于几个类型 Token 的组合子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 pub const AVAILIABLE_PUNCT: &[char] = &['(', ')', '-', '~', '*', '/', '{', '}', '[', ']']; fn parse_punct(input: Span) -> IResult<Span, RawToken> { let (input, pos) = position(input)?; let (input, matched) = nom::character::complete::one_of(AVAILIABLE_PUNCT)(input)?; let token = RawToken { kind: RawTokenKind::Punct(matched), span: pos.location_offset()..(pos.location_offset() + matched.len_utf8()), }; Ok((input, token)) } fn parse_normal_char(input: Span) -> IResult<Span, RawToken> { let (input, pos) = position(input)?; let (input, matched) = nom::character::complete::satisfy(|c| { !AVAILIABLE_PUNCT.contains(&c) && c != '@' && c != '\n' && c != '\r' })(input)?; let token = RawToken { kind: RawTokenKind::NormalChar(matched), span: pos.location_offset()..(pos.location_offset() + matched.len_utf8()), }; Ok((input, token)) } fn parse_tag(input: Span) -> IResult<Span, RawToken> { let (input, pos) = position(input)?; let (input, matched) = nom::character::complete::char('@')(input)?; let token = RawToken { kind: RawTokenKind::Tag, span: pos.location_offset()..(pos.location_offset() + matched.len_utf8()), }; Ok((input, token)) } fn parse_linending(input: Span) -> IResult<Span, RawToken> { let (input, pos) = position(input)?; let (input, ending) = nom::branch::alt(( nom::combinator::map(nom::bytes::complete::tag("\n"), |_| LinendingKind::LF), nom::combinator::map(nom::bytes::complete::tag("\r\n"), |_| LinendingKind::CRLF), )) .parse(input)?; let token = RawToken { kind: RawTokenKind::Linending(ending), span: pos.location_offset()..(pos.location_offset() + ending.to_string().len()), }; Ok((input, token)) } 对于 RawTokenKind::Tag 我们直接提取 @字符就好 对于 RawTokenKind::Linending我们需要判断并且保留文档里的换行符类型 \n:UNIX 平台所使用的换行符 \r\n:Windows 平台所使用的换行符 对于其他的标点符号,我们引入一个常量 AVAILIABLE_PUNCT表示可以出现在 RawTokenKind::Punct内的字符(相当于白名单) 如果在以上情况之外,则归类为RawTokenKind::NormalChar 因此,我们可以总结出如下的 lexer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let rope = Rope::from_str(source); let mut tokens = Vec::new(); let mut input = Span::new(source); let mut parsers = nom::branch::alt(( Self::parse_tag, Self::parse_linending, Self::parse_punct, Self::parse_normal_char, )); while !input.fragment().is_empty() { let Ok((rest, token)) = parsers.parse(input) else { return Err(error::Error::Lexer); }; tokens.push(token); input = rest; } Ok(Self { text: rope, tokens }) 单元测试也是不可少的一部分,因为包括对于中文的解析是略微麻烦也需要验证的 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 #[cfg(test)] mod tests { use super::*; use crate::token::{LinendingKind, RawTokenKind}; #[test] fn test_raw_document_parsing() { let input = "@hello\n-world\r\n"; let doc = RawDocument::new(input).expect("Failed to parse document"); let expected = vec![ RawToken { kind: RawTokenKind::Tag, span: 0..1, }, RawToken { kind: RawTokenKind::NormalChar('h'), span: 1..2, }, RawToken { kind: RawTokenKind::NormalChar('e'), span: 2..3, }, RawToken { kind: RawTokenKind::NormalChar('l'), span: 3..4, }, RawToken { kind: RawTokenKind::NormalChar('l'), span: 4..5, }, RawToken { kind: RawTokenKind::NormalChar('o'), span: 5..6, }, RawToken { kind: RawTokenKind::Linending(LinendingKind::LF), span: 6..7, }, RawToken { kind: RawTokenKind::Punct('-'), span: 7..8, }, RawToken { kind: RawTokenKind::NormalChar('w'), span: 8..9, }, RawToken { kind: RawTokenKind::NormalChar('o'), span: 9..10, }, RawToken { kind: RawTokenKind::NormalChar('r'), span: 10..11, }, RawToken { kind: RawTokenKind::NormalChar('l'), span: 11..12, }, RawToken { kind: RawTokenKind::NormalChar('d'), span: 12..13, }, RawToken { kind: RawTokenKind::Linending(LinendingKind::CRLF), span: 13..15, }, ]; assert_eq!(doc.tokens, expected); } #[test] fn test_raw_document_parsing_with_chinese() { let input = "@你好\n-世界\r\n"; let doc = RawDocument::new(input).expect("Failed to parse document"); let expected = vec![ RawToken { kind: RawTokenKind::Tag, span: 0..1, }, RawToken { kind: RawTokenKind::NormalChar('你'), span: 1..4, }, RawToken { kind: RawTokenKind::NormalChar('好'), span: 4..7, }, RawToken { kind: RawTokenKind::Linending(LinendingKind::LF), span: 7..8, }, RawToken { kind: RawTokenKind::Punct('-'), span: 8..9, }, RawToken { kind: RawTokenKind::NormalChar('世'), span: 9..12, }, RawToken { kind: RawTokenKind::NormalChar('界'), span: 12..15, }, RawToken { kind: RawTokenKind::Linending(LinendingKind::CRLF), span: 15..17, }, ]; assert_eq!(doc.tokens, expected); } }

2025/5/13
articleCard.readMore

简单的相似去重算法(基于向量)

在工作的时候遇到一个对图片进行去重的需求,简单记录一下 算法 当前提供两种算法,两种算法都依赖于向量数据库。并且由于架构设计,现在两种算法在执行时几乎只需要向量数据库 层级递进 这种算法可以在每次产生新数据时对新数据进行处理,如果新数据里已经出现过之前就已经被查找到的相似图片那么该数据就会被剔除 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func (d *Deduplicator) _algorithmV0(trainedUuid string, fuzz float32) { if d.searchedUuids.Contains(trainedUuid) { return } if res, err := d.vdb.SearchByNearOjWithDistance(trainedUuid, fuzz); err != nil { logrus.Errorf("while try to fetching result of `%s`: %s", trainedUuid, err) } else if len(res) > 1 { currentResult := x.NewList[ProcessedID]() for _, file := range res { partial, exist := d.fileIdMapping[file.FileId] if exist && !d.searchedUuids.Contains(partial.UUID) { currentResult.Append(ProcessedID{ID: file.FileId, Distance: file.Additional.Distance, Time: partial.TakeTime}) } d.searchedUuids.Add(partial.UUID) } if currentResult.Len() > 1 { tmp := d.postproc(currentResult) d.result.Append(tmp) } } } 后处理 在完成结果获取后我们还需要一次过滤操作,我们令该操作为 filter filter 函数接受一个 float64类型的参数,根据这个参数过滤掉不需要的值只保留需要的值 全量关联合并 这种算法实现起来就非常简单,相当于把上一种算法的去重步骤给放到提取结果的时候。 但这个算法会对每组都进行查找,假设有三十万个向量数据则会查找三十万次 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func (d *Deduplicator) _algorithmV1(trainedUuid string, fuzz float32) { if res, err := d.vdb.SearchByNearOjWithDistance(trainedUuid, fuzz); err != nil { logrus.Errorf("while try to fetching result of `%s`: %s", trainedUuid, err) } else { currentResult := x.NewList[ProcessedID]() for _, file := range res { partial, exist := d.fileIdMapping[file.FileId] if exist { currentResult.Append(ProcessedID{ID: file.FileId, Distance: file.Additional.Distance, Time: partial.TakeTime}) } d.searchedUuids.Add(partial.UUID) } if currentResult.Len() > 1 { d.result.Append(d.postproc(currentResult)) } } } 合并 我们令以上算法得到的结果为 res ,并且新引入一个函数 merging 令我们最终得到的结果为 y,相比于层级递进算法我们可以更加动态的计算结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func merging(input [][]ProcessedID) [][]ProcessedID { walked := mapset.NewSet[uint]() result := make([][]ProcessedID, 0) for _, group := range input { tmp := slices.DeleteFunc(group, func(id ProcessedID) bool { return walked.Contains(id.ID) }) result = append(result, tmp) for _, t := range tmp { walked.Add(t.ID) } } return result } 这样做,和层级递进最大的区别就在于全量运算,并且在获取结果的时候进行关联合并 后处理 全量关联合并的后处理和层级递进的后处理步骤一致 算法缺陷 层级递进算法的问题 旧算法的复杂度为 \(O(n)\) 到 \(O(n^2)\),计算量比较小速度快但是缺点也非常明显 计算结果只能在向量空间里一次性使用,假如在二维向量空间里则圆缩小后无法从缩小前和缩小后之间的区域重新规划出集合,也就是存在并查集问题 因为上述原因,去重的结果只在单次有效 全量关联合并算法的问题 该算法的主要问题在于计算缓慢,对于正常用户环境的三十万条向量数据在懒猫微服上计算需要十分钟 但我们可以选择将其作为后台运行的任务并且将其计算结果作为缓存来处理,这样就能扬长避短了

2025/5/13
articleCard.readMore

更加现代的 PaperMC Minecraft 插件设计指南

你还在为你的大型 PaperMC 插件性能差劲而烦恼吗?来看看这些 tips吧。 常见卡顿原因 在 Minecraft的设计里,你的插件所有逻辑都是串行的,当你插件的某部分逻辑无法在指定时间内完成相应的任务则会造成卡顿。 我们一般使用 TPS(Tick PerSecond)来衡量服务器是否能按照预期的执行速度来执行整个游戏内加载好的逻辑,默认情况下这个数值是20,当你的服务器当前 TPS 低于20那么意味着你的服务器已经过载(Overloaded)了。 计算瓶颈 实体过多导致的过载 算法缺陷 加载项过多 设计失误 IO 瓶颈 主线程访问数据库 主线程访问外部网络 缓存过载 可以使用 PaperMC 自带的 Spark 简单分析瓶颈出现在什么地方。 但,这些都是有 tradeoff的:我们需要牺牲一些实时性来利用更强大的计算资源利用 优化手段 优化将会围绕两个重要的点进行:优化计算瓶颈,还是优化 IO 瓶颈? 计算瓶颈 对于计算瓶颈而言除了优化算法以外还可以优化架构等,优化算法算是个很小的切入点 分离计算到其他线程 假设我们可以从原始架构访问到基础类型,那么我们可以通过值传递的方式构造一个函数\(y=f(x)\),令 \(y\)为我们预期的副作用或者预期的值,使得该副作用可以以干净的方式包装起来应用到目标上,这样就实现了封装副作用的目的 举个例子 1 2 3 4 5 6 7 8 9 // 以下代码是在主线程外进行的 // // 通过各种包装使得 f 是一个可以安全地在主线程外计算的函数 var someHeavyComputing = f(); Bukkit.getScheduler().runTask(pluginInstance, () -> { // 在完成计算后以同步的方式应用回计算结果,使得副作用可控 applyHeavyComputingResult(someHeavyComputing); }); 使用 Singleflight 如果对于一个表达式会反复求值,并且其输入和输出可以遇见地一致,那么可以使用Singleflight等方式缓存下来该表达式的结果,在接下来的运算中将有机会节约该次运算的计算损耗。 当前 Java 生态并没有公认的 Singleflight 实现,因此请参考 Golang 的Singleflight 实现自己实现一个 任务队列 可以通过任务队列的方式限制执行数量,每 tick 只取指定数量任务执行 由于任务队列处理器可以感知处理任务消耗的时间,因此甚至可以实现智能截断防止任务过多使得服务器过载 因此可以实现以下几种任务执行策略: 溢出式 该策略允许执行固定数量的任务,比如 1024 个任务、 执行器只会执行小于等于 1024 个任务,多出来的任务本轮将不执行 自动截断 由于执行器可以感知已消耗的时间,因此可以在本轮任务即将超时或者已经超时时停止继续执行任务 抛弃式 只执行 1024 个任务,多余任务将会被抛弃 针对性缓存 可以利用精度缩减等方法来粗化搜索的粒度,并且实现针对性的缓存使得可以构建一个高效的缓存系统 精度缩减是一个很笼统的说法,举个例子: 将坐标归一化到方块中心,以方块中心为中心点开始搜索实体 由于已经归一化到方块中心并且搜索所有实体,该步骤可以缓存下来,本次tick 都可以利用该缓存 对于这种很耗时的搜索,同时可以归约一下搜索范围为统一的几个值使得缓存可以被更高效利用:例如规定代码内使用的搜索范围为3, 6, 8 等几个值 IO 瓶颈 很喜闻乐见的,有不少插件作者贪图简单选择直接在主线程里使用非常耗时的IO 请求,这是难以避免的 分离 IO 逻辑到其他线程 该部分做法和上文提到的分离计算到其他线程做法一致,都是封装副作用在线程外计算,随后在主线程内应用。 不过你应该结合 Java 的异步基建来实现这一点:例如Future

2025/5/7
articleCard.readMore

简单的 CFG 语法分析方法

有些时候吧人就是贱,想写点吃力不讨好的东西。今天就写点上下文无关文法(Context-FreeGrammar, CFG)的两个算法吧。 请注意,这部分不是我擅长的领域,我只是因为好奇这方面的事情而自学过。如有纰漏,还请海涵。 本篇内容偏多偏杂,请善用右侧的 ToC 来快速导航 :) 文法(Grammar) 首先先明确一下,文法(Grammar)和语法(Syntax)是两个东西。 文法是形式化的约定,使用一套符号系统来进行严格定义,为编译器或者解析器所使用的形式化规则 语法则为语言设计者所定义的语言规范 我们在这里要做的是讨论解析器/编译器所使用的形式化规则,那么我们的重点则是文法而不是语法。 文法的形式化表达 先放一个例子在这里,免得无聊吧 E → T E' E' → + T E' | ε T → F T' T' → * F T' | ε F → ( E ) | id 无聊是不无聊了,但是看到符号就食欲不振。让我们更加深入一些: 终结符 Terminal Symbols 如其所述,终结符意味着终结,代表着该符号不可被再分解,例如* ( ) id等等等。(这里的 id代指标识符,例如变量名或者关键字等) 以我们上文所给的例子: id:标识符(如变量名、数字等)。 +, *:运算符。 (, ):括号。 $:输入结束符(过程中添加的标记,表示输入的末尾)。 再回去看上文的例子,就感觉清晰不少了不是吗?好,我们继续深入这个例子中各项的意义。 非终结符 Non-TerminalSymbols 非终结符是文法中需要进一步推导的符号,通常使用大写符号来表示:例如例子中的ET,它们代表语法结构中的抽象概念,如表达式、项、因子等。 继续以我们上文提到的例子: E:表达式 Expression 。 E':表达式后缀(用于消除左递归)。 T:项 Term。 T':项后缀(用于消除左递归)。 F:因子 Factor。 其他符号 要是你仔细点的话,会看到这里还有其他符号: → 推导关系,左侧是非终结符,右侧是可能产生的表达式 如果不方便编写的话写作 := 也是可以的 | 表达或关系,意味着该非终结符可以被推导为多个可能的符号序列 例子:E' → + T E' | ε 即 E' 可以被推导为+ T E' 或者 ε ε 空推导,意义为可以被推导为什么都不做 还是以刚刚的例子,E' → ε 就可以认为 E'什么都不做,则 E' 可以直接消失 产生式 Productions 基于上述规则,我们可以组合出更复杂的东西:产生式。 产生式定义了对于非终结符号的定义规则,我们来看一些产生式例子: E → T E' 该产生式由一个项 T 和一个后缀构成 E' T 处理项(如乘法、除法等) E' 处理加法或减法的后缀 F → ( E ) | id 因子 F 可以是括号内的表达式 E 或标识符id E' → + T E' | ε 后缀 E' 可以是 + T E' 也可以是ε 等等,E, T, F又是什么东西?! 文法符号 Grammar Symbol 请注意,接下来有些概念可能会产生混淆:接下来的概念基本上都属于数学表达式的分层结构,而不是数字运算的优先级顺序 因子,F,Factor 原子单元,他们无法被进一步分解,例如id,(E) 项,T,Term 由 F 乘法除法组成的单元,例如T → F * F 表达式,E,Expression 由 T 通过加法减法组成的单元,例如E → T + T 特别的,id是在我们当前的语境下引入的新原子单元,称之为标识符identifier 等等,好像混进来奇怪的东西?(E) 又是啥玩意儿? 考虑到一个表达式可以被分解成由 F构造的产生式,我们将分解这个步骤使用 ()进行封装成为一个整体,这样可以减轻各方面的压力 如果再允许 (E)进行分解,那么很可能导致文法中出现左递归和更复杂的结构 举个例子单独说明 F → (E)的情况,在这种情况下不能认为包含了 E所以可以无限推导下去,因为 E必须被完全推导为具体的符号: 1 F → ( E ) → ( T E' ) → ( F T' E' ) → ( id * F T' E' ) → ... → ( id * id ) 以上的例子最终将 F → ( E )分解成为了具体的符号序列作为因子的实例

2025/4/28
articleCard.readMore

简单地使用 Caddy 实现 CORS 配置

其实可以在后端实现 CORS 配置,但是在后端实现 CORS不算是很方便管理。既然已经使用了 Caddy,那为什么不利用强大的 Caddy 实现CORS 配置? 查阅文档发现 Caddy 本身不支持直接写 CORS 的配置,但是 CORS基本上是使用 HTTP Header来实现的[0],所以我们应该只需要写对应的HTTP Header 就行了: 1 2 3 4 5 6 7 8 :80{ header { Access-Control-Allow-Origin * Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE" Access-Control-Allow-Headers "Content-Type" } ... } 然后我发现了个更奇妙的写法:Caddy 有snippet[1],利用 snippet一样可以做到这样的效果并且可移植性更好(大概?) 下面这段粘贴复制出去就可以用了,非常方便 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 (cors) { @cors_preflight method OPTIONS @cors header Origin {args.0} handle @cors_preflight { header Access-Control-Allow-Origin "{args.0}" header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE" header Access-Control-Allow-Headers "Content-Type" header Access-Control-Max-Age "3600" respond "" 204 } handle @cors { header Access-Control-Allow-Origin "{args.0}" header Access-Control-Expose-Headers "Link" } } 然后在站点配置里如下引用: 1 2 3 4 5 6 7 :80 { import cors * handle_path /* { reverse_proxy localhost:8080 } } 0.MDN↩︎ 1.Caddysnippet ↩︎

2025/4/28
articleCard.readMore