# VCS 仿真提速实战：445s→33s 的杠杆顺序与 -fgp 判据

## 结论先行

一个 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. 三个容易踩的坑

1. **对照组不干净**：曾把「`-debug_access+pp +rad` 带来的 21%+15%」错记到 `-fgp` 头上，得出「FGP 有 1.47×」的反向结论。任何并行/选项结论，两组必须只差那一个变量。
2. **dump 的比例会变**：原始模型上 dump 只占约 5%，换成 fast 模型后占约 45%（33.4s vs 18.0s）。加速之后要重新量，否则会低估「缩小 dump 范围」这一档的价值。
3. **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`）。

