作者归档:mzh

Go RISC-V runtime去掉 Duff’s Device 性能提升最大225%

上游Google团队的 Keith Randell 6月时抛出一个有趣的话题,由于近年的CPU预测技术的发展,如果Go runtime去掉Duff device,性能反而会达到一定的提升。
原帖:https://groups.google.com/g/golang-dev/c/bVoLyx0s3tg

我摘出来有趣的部分并翻译了下:

达夫设备(Duff's Device) 通过在运行时部署一个大规模展开的清零/复制循环来优化内存清零和内存复制操作。编译器会合成一个跳转指令,直接跳转到这个展开循环的特定中间位置,以精确执行所需次数的循环操作。
在Go语言中,其实现形式类似于:

s[0] = 0; s = s[1:]
s[0] = 0; s = s[1:]
s[0] = 0; s = s[1:]
...大量重复指令...
s[0] = 0; s = s[1:]
s[0] = 0; s = s[1:]
s[0] = 0; s = s[1:]
return

当编译器需要清零7个字节时,它会计算从末尾倒数第7条指令的地址并直接跳转执行。
这听起来很巧妙,但每次调用都会产生额外开销。况且现代分支预测器已非常高效,消除调用点的循环开销并不再那么重要。

Keith 还提供了几个CL,不过ARM64/AMD64 的实现是不管对齐的问题的,这就意味着 RISC-V 不能这样用(因为 RISC-V 的 unalignement access 是臭名昭著的软件实现/内核 trap/性能损失大)我决定自己实现 type alignment 的版本。

Keith 版 SSA 非常简洁

(Zero [s] ptr mem) && s > 16 && s < 192 => (LoweredZero [s] ptr mem)
(Zero [s] ptr mem) && s >= 192 => (LoweredZeroLoop [s] ptr mem)

然后编写对应的 code gen 就可以了,但……RISCV就麻烦了,因为要传入一个type alignment进去

(Zero [s] {t} ptr mem) =>
        (LoweredZero [t.Alignment()]
                ptr
                (ADD <ptr.Type> ptr (MOVDconst [s-moveSize(t.Alignment(), config)]))
                mem)

如果直接塞多一个参数,变成

(Zero [s] {t} ptr mem) =>
        (LoweredZero [s]
                ptr
                (MOVconst [t.Alignment()])
                mem)

这样在materialize的时候就会多一个MOV const, reg的指令,这样不优雅!

然后想到bit shift,把LoweredZero [s<<32|t.Alignment()],但这样在SSA优化的时候,mem size就非常恐怖了……怎么样优雅的把这个值塞进 AuxInt,就成了头大的问题。

只好看看 SSA compiler 的实现,然后在 AuxInt 的 type list (cmd/compile/internal/ssa/op.go)发现了“auxARM64BitField”

 // architecture specific aux types
 auxARM64BitField     // aux is an arm64 bitfield lsb and width packed into auxInt

给 RISC-V 直接套上,就会造成程序的文不对题。

改成auxRISCVBitFields,还要加几个前置CL来实现。这不优雅!

于是我全部类型都看了遍,终于发现一个合理的:"SymValAndOff",代价就是……Lowered(Zero|Move)需要添加一个 symEffect 说明这个指令有啥副作用(当然都是Write啦),这下就优雅多了

(Zero [s] {t} ptr mem) && s <= 24*moveSize(t.Alignment(), config) =>
    (LoweredZero [makeValAndOff(int32(s),int32(t.Alignment()))] ptr mem)

完美!

然后测试性能发现,咦,Zero竟然没有任何改进?

MemclrKnownSize112-4             5.602Gi ± 0%    5.601Gi ± 0%         ~ (p=0.363 n=10)
MemclrKnownSize128-4             6.933Gi ± 1%    6.545Gi ± 1%    -5.59% (p=0.000 n=10)
MemclrKnownSize192-4             8.055Gi ± 1%    7.804Gi ± 0%    -3.12% (p=0.000 n=10)
MemclrKnownSize248-4             8.489Gi ± 0%    8.718Gi ± 0%    +2.69% (p=0.000 n=10)
MemclrKnownSize256-4             8.762Gi ± 0%    8.763Gi ± 0%         ~ (p=0.494 n=10)
MemclrKnownSize512-4             9.514Gi ± 1%    9.514Gi ± 0%         ~ (p=0.529 n=10)
MemclrKnownSize1024-4            9.940Gi ± 0%    9.939Gi ± 1%         ~ (p=0.989 n=10)

再次祭出SSA debuger,发现,原来runtime的memclr都跑去调用memNoPtrClr了……那自然不会用咱们这个LoweredZeroOp。

然后改用了基于SSA Zero的ClearFat,才有下面的测试结果(删掉了部分不重要的)

ClearFat3-4                   1.300Gi ± 0%    1.301Gi ±  0%         ~ (p=0.447 n=10)
ClearFat4-4                   3.902Gi ± 0%    3.902Gi ±  0%         ~ (p=0.971 n=10)
……
ClearFat16-4                  1.600Gi ± 0%    5.202Gi ±  0%  +225.10% (p=0.000 n=10)
ClearFat18-4                  1.018Gi ± 0%    1.300Gi ±  0%   +27.77% (p=0.000 n=10)
ClearFat20-4                  2.601Gi ± 0%    4.938Gi ± 12%   +89.87% (p=0.000 n=10)
ClearFat24-4                  2.601Gi ± 0%    5.201Gi ±  0%   +99.96% (p=0.000 n=10)
ClearFat32-4                  1.982Gi ± 0%    5.203Gi ±  0%  +162.55% (p=0.000 n=10)
ClearFat56-4                  3.640Gi ± 0%    5.201Gi ±  0%   +42.88% (p=0.000 n=10)
ClearFat64-4                  2.250Gi ± 0%    5.202Gi ±  0%  +131.25% (p=0.000 n=10)
……
geomean                       2.005Gi         3.020Gi         +50.58%

可以看到原来的小byte的实现不变(没改,当然没变),到了16 bytes 就突然涨起来,我看了下原版的是有优化的,理论上应该不变啊

(Zero [16] {t} ptr mem) && t.Alignment()%8 == 0 =>
    (MOVDstore [8] ptr (MOVDconst [0])
        (MOVDstore ptr (MOVDconst [0]) mem))

原来ClearFat的type alignment是 uint32 (也就是4 bytes),所以没有优化,就落到了循环实现(连TM duff也要求 8 bytes alignment)

func BenchmarkClearFat16(b *testing.B) {
        p := new([16 / 4]uint32)        
        Escape(p)                       
        b.ResetTimer()                  
        for i := 0; i < b.N; i++ {      
                *p = [16 / 4]uint32{}   
        }                               
}                                       

既然如此,那我就白捡个性能优化225%吧 :)

优化开源NTP Pool监控节点:基于RISC-V平台的实践

开源 NTP Pool 现状:严峻的服务缺口

当前开源 NTP Pool(pool.ntp.org)在中国区面临显著的服务缺口(Under-Served)
以下就简称开源的公共 NTP Pool 为 NTP Pool。

  • 服务节点/人口比例失衡:全球 NTP Pool 约90%的服务节点位于等欧美发达国家,中国仅占不足3%(数据来源:NTP Pool Project 2024报告),却承载全球21%的互联网流量。
  • 服务延迟差异:境内用户访问境外NTP节点延迟普遍≥150ms,而本土节点因数量不足导致部分区域延迟波动超300ms
  • 监控盲区:原有监控节点多部署于海外,无法真实反映国内网络环境质量(如防火墙策略、骨干网路由抖动),进一步放大服务不稳定性。

如图所示,NTP pool中国区5千万人才拥有一个NTP Pool服务节点,这一地理与网络拓扑的错配,使国内用户难以享受低延迟、高可靠的公共授时服务。

服务缺口的核心问题:本土监控节点缺失

NTP Pool的服务节点调度机制依赖监控节点(Monitor) 实时评估节点健康度。然而:
监控节点国内为0个:2025年前健康检测数据均来自境外,这导致了

  1. 误判率高:网络波动被标记为“节点故障”
  2. 调度失衡:健康节点因跨境延迟被降权
  3. 扩容停滞:缺乏数据支撑中国区服务器准入,服务节点数常年处于50个以下

没有本土监控,NTP Pool的中国服务优化如同“无源之水”。


如图是针对腾讯 NTP 服务器的监控结果,最右边的一列就是监控节点到服务节点的延迟(RTT),越低越好。
大家可以注意到没有中国大陆的监控节点(CN开头的)。
其中的cncan1就是本博客添加的监控节点,可见如果没有cncan1,所有节点都是超过200ms以上的延迟,对于 NTP 准确性会有严重影响。

破局:基于RISC-V监控终端

这次,我选用OrangePi RV2 构建低成本、低功耗监控节点,售价才200出头,加上天线等其他硬件总成本控制在300元左右,还可以通过GPIO添加PPS功能。

相对的,某宝上一般 NTP 服务器都要600以上,还没有算天线等其他设备。

硬件架构

开发板:OrangePi RV2
GNSS模块:Wheeltech GNSS模块 + PPS输出
GNSS天线:北斗+GPS 双模蘑菇头 + 8米 SMA 馈线

开发板添加对应的DTS overlay

/dts-v1/;
/plugin/;
/ {
        compatible = "ky,orangepi-rv2\0ky,x1";
        fragment@0 {
                target-path = "/";
                __overlay__ {
                        pps_gpio: pps {
                                compatible = "pps-gpio";
                                gpios = <&gpio 91 0>;
                                assert-rising-edge;
                        };
                };
        };
};

软件栈

  1. Chrony:维护PPS同步,NTP时间服务
  2. GPSD:接收北斗卫星信号→PPS硬件时钟同步(精度±200ns)
  3. ntppool-agent: 负责监控进程(重新编译,Go语言已支持RISC-V)

效果如图,这样就可以获得一个精度在±500ns以下的Stratum 1 NTP 服务器

获得NTP Pool官方破例支持

向NTP Pool上游持续贡献代码

  1. add riscv64 binary release https://github.com/ntppool/monitor/pull/6
  2. https://github.com/abh/ntppool/pull/255
  3. https://github.com/abh/ntppool/pull/262

1年来,在跟上游管理团队来来回回好十几封邮件后,这块RISC-V开发板,获得了豁免监控节点准入限制:常规要求NTP服务节点稳定运行18个月方可成为监控节点,这次为中国区首开绿色通道。

结语:从服务缺口到技术领先

国产RISC-V开发板+北斗GNSS的组合,不仅填补了中国区NTP Pool监控节点空白,更证明了:RISC-V可承担关键基础设施角色

本博客运行平台迁移至RISC-V

RISC-V powered!

好消息:迁移非常顺利,跟x86平台迁移一样,原平台就叫server,rv板子就叫board吧:
在自己的机器上:

  1. mysqldump database > db.sql 备份mysql数据库
  2. rsync -azv -R server:/home/mzh ./ 转移家目录
  3. rsync -azv -R mzh board:/home
  4. scp db.sql board:/home/mzh

在rv板子上

  1. apt install php-fpm caddy mariadb-server
  2. 配置好caddy (其实就官方文档就好)
  3. 导入数据库 mysql -u mzh < db.sql
  4. 最关键的一步,把板子的80、443暴露给服务器

设置反向代理,添加以下文件/lib/systemd/system/reverse-tunnel\@.service

[Unit]
Description=Reverse SSH Tunnel
After=network-online.target
[Service]
EnvironmentFile=/etc/default/remote-tunnel@%i
ExecStart=/usr/bin/ssh -i ${ID_KEY} -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -nNT -R 0.0.0.0
:${REMOTE_PORT}:localhost:${LOCAL_PORT} ${REMOTE_SERVER}
RestartSec=5
Restart=always
KillMode=mixed
[Install]
WantedBy=multi-user.target

每个端口一个进程

systemctl daemon-reload && systemctl start reverse-tunnel@https

搞定,这速度杠杠的。

mzh@muse-card-1:/etc/default$ wp --info                                                                  
OS:     Linux 6.6.63 #2.2.4.2 SMP PREEMPT Thu Jun 26 05:06:32 UTC 2025 riscv64                           
Shell:  /bin/sh                                                                                          
PHP binary:     /usr/bin/php8.3                                                                          
PHP version:    8.3.6                                                                                    
php.ini used:   /etc/php/8.3/cli/php.ini                                                                 
MySQL binary:   /usr/bin/mariadb                                                                         
MySQL version:  mariadb  Ver 15.1 Distrib 10.11.8-MariaDB, for debian-linux-gnu (riscv64) using  EditLine
 wrapper                                                                                                 
SQL modes:                                                                                               
WP-CLI root dir:        phar://wp-cli.phar/vendor/wp-cli/wp-cli                                          
WP-CLI vendor dir:      phar://wp-cli.phar/vendor                                                        
WP_CLI phar path:       phar:///usr/local/bin/wp                                                         
WP-CLI packages dir:                                                                                     
WP-CLI cache dir:       /home/mzh/.wp-cli/cache                                                          
WP-CLI global config:                                                                                    
WP-CLI project config:                                                                                   
WP-CLI version: 2.12.0                                                                                   

KubeCon China 2025参会小记

战利品

注册

  • 会议的个人门票755港币,包午饭,总体来说还是比较划算的
  • 香港的个人通行证要在福田地铁附近的公安机器上刷新,我是深圳户口,所以15块一次.
  • 地铁可以直接到会场,所以很方便,就是香港的酒店怪怪的,入口不显眼(可能因为山多?)

    会议感想

  • 现场没有翻译字幕(参会的老外和中国人各种懵逼)
  • 大公司都是老样子,复杂的解决方案,底下的人哪里用得上
  • 小公司讲得大部分题目又过于琐碎,记得有个演示多云部署还失败了(crossplane的你们就不会录好视频直接放么)
  • 很多人英语口语不好也让硬讲(对,说的就是华为)何苦呢……
  • 演讲不要双人讲的时候,感觉特别明显,两人口音不一样,节奏不同的话,千万不要分开讲!听众会晕的
  • 开篇的keynote不错,分析了整个CNCF生态的状态,还cue到了riscv,看得出中美的技术竞争真的是方方面面的
  • AI,AI,到处是AI,喂,这里是CNCF啊!跟AI相关的基本不记得是啥玩意了。

    maintainer meetup

    这个才是重头戏,本来是闭门会议的,我悄咪咪的溜进去了

  • CNCF有专门的3人doc team,其实也做技术辅导,文章辅导还有promotion
  • ToC有个理论是,首次贡献者的贡献必须要一周内反馈
  • CNCF有完整的项目孵化流程,每个都是要符合特定标准的,如果不符合,可以找专门的team来帮助完成,而且CNCF人会定期检查是不是真的落实了,而不是停留在纸上
  • 重要的是“供应商中立性”,比如一个社区不能超过50%的贡献者是同一家公司,amd64 和 arm64都要支持(riscv讨论的时候说到的)
  • CNCF 有专门的 https://clomonitor.io 来监控各个项目的健壮性之类的指标,666

    活动

    刷各种二维码回答问卷才能抽奖得奖品,有点小气。
    CNCF会议方有发衬衫,但是就一件,有些贴纸啥得

    偶遇

  • 午饭时偶遇了个老外,humanlogio的Antoine Grondin,E人就是E人,直接隔壁吃着饭就能跟你握手say hi,我两交换了Github账号(没名片就是痛)
  • Maintainer meetup后,因为太冷跑去喝茶,听到隔壁两日本人教个法国人认识汉字,竖起耳朵旁听才知道,日本人是苹果的,法国人叫Sara,竟然是联合国的,对,联合国也在搞上云CNCF,后来聊到迁移联合国的系统也是极度痛苦,又老又旧没文档那种哈哈哈
  • 另一个日本人是游戏公司的tech leader,kahirokunn 特喜欢用Go写代码,聊了不少Go的贡献和底层实现的趣事
  • 碰到个上交的研究生maintainer,聊了一通,不得不感慨东部发达城市的孩子就是有想法,敏锐的感知到了知道云原生行业发展已经不行,要好好顺应潮流搞AI,还要去最好的地方学(啊,老美)

    餐食

  • 一直有咖啡饮料供应,还是不错的
  • 午饭是高档盒饭,味道不错

OpenWRT 防火墙开启IPv6 端口转发的方法

前言

OpenWRT当启用IPv6时,默认情况下流量会经过主路由防火墙。在外部网络环境中,若想访问家庭局域网内的服务(如SSH、Proxy、NAS等),默认请求将被防火墙拦截,因此需在主路由防火墙中添加允许规则。
这里并不推荐禁用IPv6防火墙:虽然IPv6地址和端口扫描难度较大,但仍存在被扫描风险。所有局域网服务可能暴露于外网,即使未受攻击,家庭宽带提供公网服务也可能被ISP警告。

LUCI网页界面配置

打开OpenWRT路由管理页面,进入 网络 - 防火墙 - 流量规则。

点击 添加 创建新规则。流量规则区的规则优先级高于默认规则,无需担心与默认规则冲突,但需注意同页其他规则的优先级。可通过拖拽调整规则顺序。

规则配置示例:

  • 名称:清晰标识规则用途(例如如 NAS IPv6)。
  • 协议:按需选择协议,不确定时可同时选TCP和UDP。
  • 源区域:选择 WAN(外网流量入口)。
  • 源地址/源端口:留空。
  • 目标区域:选择 LAN(内网目标区域)。
  • 目标地址: 链路本地IPv6地址(通常以 fe80:: 开头)。

移除链路本地前缀(如 fe80::20c:29ff:fed7:fbf 改为 ::20c:29ff:fed7:fbf)。

追加掩码 /::ffff:ffff:ffff:ffff,最终填入 ::20c:29ff:fed7:fbf/::ffff:ffff:ffff:ffff。此配置确保客户端即使因ISP分配的前缀变化仍能被匹配。

目标端口:指定允许的端口(如 80-443)。留空将放行所有端口,存在重大安全风险,不建议。

动作:选择 接受(Accept)。

高级设置:地址族改为 IPv6,其他保持默认。

保存后,可测试外网访问内网服务。若在 状态 - 防火墙 的 forward_wan 链中看到规则及计数器,则规则已生效。