结论先行
一个 FPGA RTL 仿真跑 7 分半,用 VCS 自带性能分析定位热点后,按「厂商原语行为模型 → 调试选项 → 时间精度 → 进程级并发」的杠杆顺序把它压到 1 分钟,并实测证明线程级并行 -fgp 在浅流水设计上是负收益。
| 手段 | 单次仿真 CPU | 相对基线 | 代价 |
|---|---|---|---|
基线(-debug_access+all,厂商原语编译模型) |
444.8s | — | — |
-debug_access+pp 替代 +all |
341.6s | -23% | 失去交互 force/单步 |
再叠加 +rad |
308.0s | -31% | 无 |
| 厂商原语换行为级 fast 模型 | 33.4s | -92%(13.3×) | 时间基 -0.04%,SERDES 存在位边界残余差异 |
再叠加 -override_timescale=1ns/10ps |
17~22s | -95% | 时间轴压缩约 490ppm |
8 个 case 用 -P 8 并发 |
总 wall 55s(串行约 440s) | 吞吐约 8× | 每 case 独立目录 |
1. 先量,再动任何选项
拆开编译与仿真分别计时:这类设计编译只有 20~25s,其余全在仿真,把编译当瓶颈会做无用功。
用 -simprofile 拿热点(跑完读 profileReport.txt 的 Module / Construct View):
- 热点落在厂商原语编译模型(
xil_*_vl_mod*、B_OSERDESE2这类 secureip)→ 换行为模型,数量级收益; - 热点落在自触发
always、assign #(real)串联延迟链、时钟树 → 这段天然串行,任何线程级并行都不可能有效。
只比 CPU Time(或 /usr/bin/time 的 user+sys),不要用 wall 比方案:wall 受机器负载影响。
2. 杠杆 1:给厂商原语写行为级 fast 模型(最大的一档)
被测设计的 76.9% 时间在 10 个 OSERDESE2 secureip 实例里。用行为 RTL 替换 unisims + secureip,接口与参数不变、不建模内部延时与 timing check,实测 444.8s → 33.4s。
要点:
- 要替换:
glbl、IBUFDS/OBUFDS/BUFG/BUFIO/BUFR、PLLE2_ADV、OSERDESE2、ISERDESE2,同时把 flist 里的 4 个secureip/*.vp去掉。 - PLL:
TVCO = CLKIN1_PERIOD * DIVCLK_DIVIDE / CLKFBOUT_MULT,取 DIVIDE 最小的输出作时基,其余 CLKOUT 由时基整数分频。这样 70MHz:490MHz = 7:1 永远精确、不随时间漂移,这是 OSERDES 7:1 能正常工作的前提。分频器要数整周期(always @(posedge base));用双边沿(半周期)计数会让 70MHz 边沿落在 490MHz 的下降沿上,整体相位差半个 VCO 周期,LVDS 串行字边界也跟着整体偏移。 - PLL 的
LOCKED按原模型实测标定(示例:复位释放后 753 个输入周期)。依赖pll_locked做复位的设计,锁定时长会直接改变行为,不要随手改。 - OSERDESE2 / ISERDESE2:
dpar[0]=D1是第一个送出的位;字边界由 CLKDIV 同步,不要靠自己数的边沿计数自由跑——初始相对相位不确定,数据每周期变化时字边界会整体错位。 DATA_WIDTH>8(10/14 级联)不要假装支持,用initial $display明确告警。
验证必须做三层:单原语并排比对(固定数据验相位、变化数据验位序)→ 整设计 sim.log 剥时间戳后逐行相同且 $finish 时刻一致 → 参考模型输出的数据序列对齐后比一致率。fsdbsanitycheck 确认波形完整。
3. 杠杆 2/3:编译选项与时间精度
| 选项 | 效果 | 备注 |
|---|---|---|
-debug_access+pp 替代 +all |
-23% | FSDB/KDB 照常,只是不能交互调试 |
+rad |
-15% | 无副作用 |
-override_timescale=1ns/10ps |
再 -44% | 490MHz 周期 2.041→2.040ns,约 -490ppm,23ms 累计偏移约 9.8µs |
完全去掉 -debug_access |
更慢 | $fsdb* 变 Undefined System Task;改显式 -P novas.tab pli.a 能编过但比 +pp 慢 |
+nospecify +notimingcheck |
更慢 6% | 去掉路径延迟后翻转事件变多 |
-j N / -partcomp -j N |
编译反而更慢 | 设计太小,并行开销为主 |
4. 杠杆 4:进程级并发才是真并行
单次 simv 是单线程的,吞吐只能靠多 case 并发。用 -P N 回归驱动:每 case 独立目录、独立 flist / 日志 / 波形,最后打印 PASS/FAIL 汇总表和总 wall。
8 个 case(每 case 22s 编译 + 32s 仿真)用 -P 8:总 wall 55s,串行约 440s,接近线性。
驱动实现里两个容易忽略的点:case 列表用 名字|flist|库|编译附加项|运行附加项 的竖线格式最省事;汇总只能统计本次列表里的 case(直接扫 run/* 会把历史 case 一起统计进去)。
5. 为什么 -fgp 没加速(以及什么情况下能加速)
语法(VCS 2023.12 实测):编译期 -fgp 只是开关,-fgp=4 会被忽略并告警;线程数只能在运行期给 simv -fgp=num_threads:N;运行期只给 -fgp 会报 FGP_REQ_NUM_THREAD;-fgp=diag:exclreason 只在 ENGINEER build 可用,正式版直接 UNSUP-FGPOPT。
实测(同一 case、同一编译选项,只改线程):
| 配置 | 原始模型 CPU | fast 模型 CPU | 实测线程 |
|---|---|---|---|
| 不用 FGP | 296.3s | 33.4s | 1 |
-fgp(默认线程) |
318.2s (+7%) | 36.1s (+8%) | 3 |
num_threads:3 |
361.0s (+22%) | 57.3s (+72%) | 13 |
num_threads:16 |
384.1s (+30%) | 72.8s (+118%) | 26 |
num_threads:32 |
408.5s (+38%) | 99.3s (+198%) | 42 |
线程越多越慢,而且不是配置没调对:
- 进程 CPU 利用率即使 32 线程也只有 103~115%,即最多 1.15 核在干活;
-fgp=hier(换分区策略)只有个位数差异;- 把 FSDB dump 关掉(唯一确定单线程的 PLI 回调)后,fast 模型单线程 18.0s、8 线程 30.2s(CPU 多 68%),仍然负收益 —— 根因是并行宽度,不是 PLI;
- Construct View 的结构:13.8% 是 PLL 的单个自触发振荡
always,12.4% 是 OSERDES 串行移位块(10 实例 × 约 1.2%,每事件几十纳秒),7.8% 是 tb 里 7 级串联的assign #(dly)延迟链,21.3% 是内核调度器。
即分子(同一时隙内可并行的独立工作量)极小,分母(每次线程同步开销)很大。
-fgp 能加速的情形:同一时刻有大量互不依赖的活动(多子系统 SoC:CPU 簇、DDR、ISP、DMA、多通道 PHY 同时跑流量);每个事件计算量大(门级网表、大 memory 随机访问、算法级模型),线程同步才摊得薄;跨分区事件少(多时钟域且跨域信号少);波形/PLI 不是瓶颈。公开经验值大约 1.3~2.5×,很少超过 3×。
6. 三个容易踩的坑
- 对照组不干净:曾把「
-debug_access+pp +rad带来的 21%+15%」错记到-fgp头上,得出「FGP 有 1.47×」的反向结论。任何并行/选项结论,两组必须只差那一个变量。 - dump 的比例会变:原始模型上 dump 只占约 5%,换成 fast 模型后占约 45%(33.4s vs 18.0s)。加速之后要重新量,否则会低估「缩小 dump 范围」这一档的价值。
- tb 里的宏开关能翻转结果:同一份 tb,仅
$fsdbSuppress的对象不同,会导致 PLL 锁定时长、I2C 读回值不同(可复现 2/2)。换模型/换选项后结果变了,先做这个对照再归因。
7. 复现清单
- 计时:
-simprofile定位热点,CPU Time做对比; - 换库:同一 flist 只替换原语文件,
diff两侧sim.log(剥时间戳)应逐行相同; - 精度改动:tb 里挂
@(posedge {frame_pulse}) $display("TRACE %t", $time)对比绝对时间,折算 ppm; - 回归:
-P N驱动 + 每 case 独立目录 + PASS/FAIL 汇总(判定 = 编译无 Error 且跑到$finish)。