Exec 跨度与复测 承接上一篇《性能三问:从一张 docx 对照表到 100C100R》。又追了三条:①数据浮动这么大,原因何在?②你取的数据有意义吗?③C 版本的 exec 是不是也这样?先把三个答案摆在前面Q1:C 版本的 exec 也是这样吗?——是先补一句背景:「同门」 两臂走完全相同的内核路径与同一份二进制协议,唯一变量是用户态库由 C 还是 Rust 写;「门」 gdev 的两种部署形态——内核门(经过内核模块 gdev.ko)与用户门(纯用户态驱动)。本篇两臂都走内核门。详见上一篇。臂发数中位min–max跨度C(内核门)1000.4050.124–1.0838.7×Rust(内核门)1000.3170.117–1.17910.1×同一个 GPU、同一扇门、同样 40µs 的活,两个语言版本的 Exec 服从同一条又宽又稳的分布(KS 检验 D0.160 临界 0.192,无法区分)。所以「浮动」不是哪一版的毛病——它不由语言负责。Q2:为什么浮动这么大?——「等完成」的唤醒路径里,藏着一个 0~1ms 的随机惩罚Exec 那一段计的是「提交内核 等它算完」。插针版把它拆开(36 发带分项计时):分量实测稳不稳提交(cuLaunchGrid)C 4–7µs / Rust 10–13µs稳GPU 真算(512×512 矩阵加)~40µs硬件确定性等完成(cuCtxSynchronize,内核门)0.3–0.93ms全部方差在这内核门的「等完成」是睡在内核里、由完成中断叫醒。唤醒函数里有一处竞争回退(gdev_sched_wakeup,gdev_drv.c:297-304):如果完成通知到得太早——等待方还没真正睡稳——通知方就自己睡到下一个时钟滴答再补叫。这台机器CONFIG_HZ1000,一个滴答 1ms——这笔罚的上限因此是 1ms。0.13ms 的地板(提交执行无竞争唤醒) 等待路径的随机延迟 0.13~1.2ms 的观测分布。400 发(本轮 200 档案 200)的定量检验给出更细的图景(下图):约四成发走无竞争快车道(≤0.3ms),其余构成慢带——慢带不是均匀铺开的,而是右偏单峰(众数≈0.3ms,长尾到 1ms):主体是普通的调度唤醒延迟(中断到了、任务醒了,但 vCPU 排上真实 CPU 核要排队——天然右偏),滴答回退贡献 ≤1ms 的长尾成分。「慢带单一均匀罚」的简化说法已被 KS 检验拒绝(D0.226 ≫ 临界 0.086)——检验过程见「环境这一环」一节的证据分级表。对照组一锤定音:用户态门的「等完成」是轮询自己 mmap 的寄存器,不睡内核、不进调度器——同负载 10 发只动 6µs(CV≈2%),稳如磐石。哪个环节在抖,一对照就现形。三次独立采样(各 100 对)的 R/C 0.90 / 1.13 / 0.78,方向翻了两次、全部区间交叠、全部不显著。三次都指向同一句话:同门之下,C 和 Rust 之间不存在值得立案的性能差。代码现场:这段代码为什么这么写根因代码原样贴出(内核模块mod/gdev/gdev_drv.c:292-306,C/Rust 两臂共享):/* 等待方:睡进内核,不被信号打扰 */voidgdev_sched_sleep(void){set_current_state(TASK_UNINTERRUPTIBLE);schedule();}/* 通知方:叫醒等待者 */intgdev_sched_wakeup(void*task){if(!wake_up_process(task)){/* 返回 0 没叫醒:对方还没睡下 */schedule_timeout_interruptible(1);/* ← 波动源头:睡到下一个滴答,再补叫一次 */if(!wake_up_process(task))return-EINVAL;}return0;}完整链条(逐行核实过):用户进程发起同步等待 → 入队后按上面的模式睡下;GPU 算完触发中断 →通知处理器(gdev_drv.c:63)叫醒 gdev 的调度内核线程(:88)→ 调度器选出已完成的上下文、将其出队,再gdev_sched_wakeup(se-task)叫醒用户进程(common/gdev_sched.c:297-301)——竞争回退就发生在最后这一步。失败分支的报错文案写的就是「Perhaps context %d is already up」(:303)。在防什么:唤醒丢失(lost wakeup)经典并发陷阱。wake_up_process()只能叫醒已经睡着的任务;如果通知到得太早——等待方还没执行到set_current_state那行、还在正常跑——这次唤醒就是无效的(返回 0),而通知是一次性的、不会排队。没有这段 fallback,等待方随后睡下就永远没人叫:每次撞上这个窗口 死锁。所以修法是「等一个滴答(这时对方必然已走到睡点),再叫一次」——用有界的延迟换不丢唤醒,教科书级的正确性权衡。在 2012 年前后的裸机研究原型里(竞争窗口纳秒级、命中率极低、HZ1000 下最多 1ms),这是个完全合理的选择。环境这一环:哪些已证,哪些是推断「赖虚拟机」恰恰是整条因果链里唯一没有直接测量的一环——全程只有虚拟机这一个环境,没有裸机基线。把每一环按证据强度排开:环节等级依据抖动全部来自「等完成」路径已证插针分解(提交/GPU 执行均稳)慢带呈多峰离散结构(0.2-0.4 隆起、0.72-0.76 与 0.92-0.98 窄簇),非均匀非简单右偏已证400 发 0.02ms 细桶直方图;「单一均匀罚」被 KS 检验拒绝(D0.226 ≫ 0.086)虚拟化放大命中频率推断机制成立(见下),缺裸机对照机制(讲得通,未测):测试机是宿主机上的一台虚拟机(GTX 580 PCI 直通),GPU 完成中断要宿主机转发注入,虚拟机的 CPU 是宿主机上的线程、与同宿主其他虚拟机抢物理核。关键不是「通知来得早」,而是睡觉的一方被耽误——提交之后、走到睡觉代码之前,vCPU 一旦被宿主机调度走几十微秒,而完成链(另一个核)照常推进,「去叫时人还没睡下」即命中;虚拟机让这种抢占频繁发生。旁证与责任划分:同一台虚拟机里,走用户门(不睡内核、轮询等待)的对照组纹丝不动(CV≈2%)——虚拟机本身不给计时加噪,加噪的是这段对调度抖动脆弱的等待代码。所以最准确的说法是:根在代码的等待设计(对任何调度抖动都脆弱,裸机上同样会命中、或许罕见),虚拟机是疑似放大器。验证(按代价排序,阶段一已完成):阶段一(已完成):用 400 发既有数据检验模型的定量预测——「均匀罚」被拒绝,模型细化为两成分;顺带发现两臂慢带命中率差 12 个百分点(上表新线索);阶段二(未执行):给gdev_sched_wakeup回退分支挂 ftrace 计数,分臂统计命中次数——一次实验同时裁两件事:命中率从推断变实测、两臂差异定论(不改源码)。阶段三(未执行):宿主机给测试虚拟机绑独占物理核,看慢带占比是否随 vCPU 抢占消失而降另两条已证的机制事实:代价是随机的:schedule_timeout_interruptible(1)睡到的是下一个滴答边界,落在1ms 周期内的哪个相位纯看运气——(0,1ms] 均匀分布,不是固定 1ms;另一方面:TASK_UNINTERRUPTIBLE让等待不可被信号打断——GPU 楔死时,等待方kill -9都杀不掉(模块内 5 处睡眠点全是同一模式)。防丢唤醒的坚韧,和故障处置的困难 (每次楔死只能整机复位),是同一个设计决定的正反面。意外收获:楔死的节律复测途中楔死 38 发,全部留证。规律清晰得意外:每个「复位装驱动」的新鲜周期,恰好能跑~19 发绿,然后楔死簇发(2~7 连楔);暂停 3 分钟/5 分钟/40 分钟都不能自愈,只有整机复位恢复。楔死瞬间的内核日志全是同句:nouveau PFIFO 写缺页(显存页不存在)——资源耗尽形态,已单独整理。复测怎么跑的(一段工程花絮)每个周期 整机复位 → 重装两个内核模块 → 建设备节点 → 从断点号续跑;runner 自带两臂计数、楔死穿透(单楔只记录不中止)、连续 3 楔自保停机。数字采样C medR medR/Cp(置换)档案同门轮0.4210.3790.900.17本轮预跑(18v19)0.2820.3191.130.87本轮终版(100v100)0.4050.3170.780.094