性能分析
DarraRT PLC 内置 Task Profiler, 提供代码级性能分析: 热点函数 / 调用树 / 每周期 FB 耗时 / 优化建议。不像 C/C++ Profiler 需要 Instrument 代码, PLC Profiler 基于 Runtime 内置的扫描时间统计, 零侵入、无性能损失。
启动 Profiler
Ribbon → 诊断 → 性能分析, 或快捷键 Ctrl+Shift+P。
采样模式
| 模式 | 采样方法 | 开销 | 准确度 |
|---|---|---|---|
| Sampling | 定时采样 PC (每 1ms) | < 0.5% | 统计级 |
| Instrumented | 每 POU 入口/出口打点 | 3~5% | 精确 |
| Hybrid | Sampling + 热点 Instrument | 1~2% | 折中 |
默认 Sampling, 生产环境用这个, 几乎无感。需要精确 FB 级数据时切 Instrumented。
采样时长
- 实时窗口: 最近 N 秒数据滚动显示 (默认 60s)
- 定时录制: 录制 N 秒后停止, 生成报告
- 事件触发: 某条件 (如扫描 > 20ms) 触发录制前后各 5 秒
热点函数 (Hotspots)
最耗时的 POU 从上到下列出:
| # | POU | 类型 | 调用次数/秒 | 单次平均 | 占比 |
|---|---|---|---|---|---|
| 1 | FB_PID_Temp.Execute | 方法 | 100 | 85 μs | 15.3% |
| 2 | Main_OB1 | PROGRAM | 20 | 1.2 ms | 12.0% |
| 3 | FB_Robot_Calc_IK | 方法 | 1000 | 22 μs | 11.5% |
| 4 | FB_Motor_Control | FB | 200 | 55 μs | 10.8% |
| 5 | DataLog_WriteBatch | FUNCTION | 10 | 350 μs | 7.2% |
点击某行, 右侧显示时间分布:
FB_PID_Temp.Execute 平均 85 μs, 标准差 3 μs
├─ 70% 基本运算 (加减乘除) 60 μs
├─ 15% 输入/输出端口访问 13 μs
├─ 10% 抗积分饱和回算 8 μs
└─ 5% 日志/诊断 4 μs
调用树 (Call Tree)
自顶向下展开调用关系:
OB1 (12% 占比, 1.2ms/周期)
├── Main.Run (10%)
│ ├── fbProcess.Execute (5%)
│ │ ├── fbSubStep1.Step (2%)
│ │ ├── fbSubStep2.Step (2%)
│ │ └── CalcQuality (1%)
│ ├── fbPID.Calculate (3%)
│ │ ├── ClampOutput (0.5%)
│ │ └── UpdateIntegral (1.5%)
│ └── LogStep (2%)
│ └── fbLogger.Append (2%)
└── Main.Cleanup (2%)
└── fbStats.Update (2%)
颜色编码:
- 绿 (<5%): 可忽略
- 黄 (5-15%): 关注
- 橙 (15-30%): 考虑优化
- 红 (>30%): 优化优先级高
时间线视图 (Timeline)
显示某一周期内每个 POU 的执行顺序:
时间 → 0ms 0.5ms 1ms 1.5ms 2ms
OB1 ┃━━━Main_Init━┃━━━━━━━━━━━━━Main_Run━━━━━━━━━━━━━━━━━━━━━┃━━━Main_End━┃
fbMotor.Exec┃
fbProcess.Step┃
fbPID.Calc┃
fbAlarm.Check┃
OB30 ┃ ━━━━━━━━━━━━━━━━━━━━━━━ ↑ 抢占 ━━━━━━━━━━━━━━━━━━━━━━━━━━
用于:
- 发现意外长的 FB 调用 (超过预期)
- 发现高优先级 OB 频繁抢占 (导致 OB1 延长)
- 验证执行顺序符合设计
每周期 FB 耗时
对每个 FB 实例统计耗时:
FB_PID_Temp (实例 fbPID1):
┌─ 直方图 (过去 1000 周期的耗时分布) ─┐
│ │
│ ▂▆█▆▂ │
│ ▁███████▁ │
│ ▁█████████▁ │
│ ▁▃███████████▃▁ │
│ ▁▂▄▆███████████████▆▄▂▁ │
└─────────────────────────────────────┘
40μs 60μs 80μs 100μs 120μs 140μs
Min 42μs, Avg 82μs, Max 132μs, P99 118μs
P99 (99 分位) 是更有意义的指标 — 100 次中有 1 次超过这个值, 接近最坏情况。
优化建议
Profiler 自动生成建议:
建议 1: 数组整体拷贝替代逐项循环
识别:
// 慢 (15μs)
FOR i := 0 TO 999 DO
arDest[i] := arSource[i];
END_FOR;
优化:
// 快 (1μs), 编译器用 memcpy
arDest := arSource;
建议 2: 字符串避免在高频周期
识别: FB 在 OB35 (1ms) 里拼接字符串
// 慢 (20μs), 堆分配
sMsg := CONCAT('Temp=', REAL_TO_STRING(rTemp));
LogInfo(sMsg);
优化: 把日志移到 OB1 或用数值缓冲
// 快, 只存数值
arTempLog[iIndex] := rTemp;
iIndex := (iIndex + 1) MOD 1000;
建议 3: REAL 代替 LREAL
识别: 计算精度过剩
// 慢 (16μs), 64 位运算
lrResult := lrA * lrB + lrC;
优化: 不需要 15 位精度时
// 快 (4μs), 32 位运算
rResult := rA * rB + rC;
建议 4: 除法替换为乘法
识别: 频繁除以常数
// 慢
rScaled := rValue / 16000.0;
优化: 编译时计算倒数
// 快 (乘法比除法快 4-10 倍)
VAR CONSTANT
INV_16000 : REAL := 1.0 / 16000.0;
END_VAR
rScaled := rValue * INV_16000;
建议 5: FB 缓存热点数据
识别: 多次读取同一变量地址
// 慢 (每次都解引用)
FOR i := 0 TO 999 DO
IF stData.arItems[i].bValid THEN
stData.arItems[i].rValue := stData.arItems[i].rValue * 2.0;
END_IF;
END_FOR;
优化: 局部变量缓存
// 快 (编译器优化为寄存器)
FOR i := 0 TO 999 DO
pItem := REF(stData.arItems[i]);
IF pItem^.bValid THEN
pItem^.rValue := pItem^.rValue * 2.0;
END_IF;
END_FOR;
建议 6: 避免在循环内重复计算
识别: 循环不变式在循环体里
// 慢
FOR i := 0 TO 99 DO
arResult[i] := arData[i] * SQRT(rFactor); // SQRT 100 次!
END_FOR;
优化: 提到循环外
// 快
rSqrtFactor := SQRT(rFactor);
FOR i := 0 TO 99 DO
arResult[i] := arData[i] * rSqrtFactor;
END_FOR;
建议 7: 短路求值顺序
识别: AND / OR 条件顺序不当
// 慢 (expensive check 先算)
IF SlowCheck() AND bQuickFlag THEN
优化: 便宜的先算, AND 短路
// 快
IF bQuickFlag AND SlowCheck() THEN
性能基线
Darra 给出典型操作的性能基线 (Intel i5-10400, 实时内核):
| 操作 | 耗时 (ns) | 说明 |
|---|---|---|
| BOOL 读写 | 1 | L1 缓存 |
| INT 加减 | 2 | ALU 整数运算 |
| REAL 加减 | 3 | SSE 浮点 |
| REAL 乘 | 4 | SSE |
| REAL 除 | 15 | 除法器慢 |
| SQRT | 25 | SIMD sqrtss |
| SIN / COS | 80 | 查表+插值 |
| 数组元素访问 | 2 | 边界检查+索引 |
| STRING 拷贝 (32 字节) | 40 | memcpy |
| STRING 拼接 (短) | 120 | 长度+拷贝 |
| FB 调用开销 | 15 | 栈帧+输入拷贝 |
| FUNCTION 调用开销 | 8 | 轻量 |
| TON 执行 | 30 | 定时器逻辑 |
| R_TRIG 执行 | 5 | 边沿检测 |
| EtherCAT 单帧 | 10000 | 125μs 周期占 8% |
| OPC UA 读 (单变量) | 50000 | 50μs, 走 TCP |
用这些基线评估你的代码是否合理: 如果一个简单 FB 耗时 500μs, 但运算量只是 50 次浮点乘 → 必有异常 (可能是字符串或诊断日志没关)。
预算 (Budget)
按任务周期分配耗时预算:
OB35 (1ms 周期):
预算: 500 μs (50% 利用率)
├── 输入读取 50 μs (10%)
├── 安全功能块 150 μs (30%)
├── 运动控制 200 μs (40%)
├── 诊断采样 50 μs (10%)
└── 输出写入 50 μs (10%)
余量: 500 μs (防御抢占)
Profiler 里为每个 POU 标记预算, 超预算亮红, 引导优化。
对比前后 (A/B)
优化代码后, 对比 Profiler 报告:
优化前:
FB_PID.Execute 平均 85 μs P99 120 μs
OB1 总时间 1.5 ms 占 WD 75%
优化后 (替换 REAL → 避免除法):
FB_PID.Execute 平均 52 μs P99 72 μs
OB1 总时间 1.1 ms 占 WD 55%
提升: 平均 -38.8%, P99 -40%
回归测试
性能纳入 CI:
- 每次发布前录制 Profiler 报告
- 与基线对比
- 退化 > 5% → 构建失败
- 防止意外引入慢代码
# ci/performance.yml
baseline: build_v1.2.0.profile.json
thresholds:
OB1_avg: +5% # 允许 5% 退化
OB35_max: +0% # 不允许退化
memory: +10%
排错表
| 现象 | 可能原因 | 解决 |
|---|---|---|
| 热点是意料之外的 FB | 日志 / 字符串 / REAL_TO_STRING | 从高频 FB 移除 |
| 抖动大但平均小 | 被 GC 或 DPC 中断 | 核心隔离 + 禁用 SMI |
| Instrumented 模式开销大 | 只对热点 Instrument | 用 Hybrid |
| P99 远大于 Avg | 长尾抖动 | 排查 OS 抢占 |
| 优化后反而慢 | 编译器优化被抑制 | 检查 volatile / 不必要引用 |
最佳实践
- 先测量, 后优化: 没有数据支撑的优化是瞎猜
- 专注 P99: 平均值骗人, P99 反映真实最坏情况
- 小改动, 大收益: 识别 Top 3 热点专门优化
- 预算导向: 给每个 POU 定耗时预算, 超了就优化
- A/B 对比: 每次优化都录 Profiler 报告, 量化改进
- 纳入 CI: 性能回归自动发现
- 硬件基线: 给客户的承诺基于特定硬件, 不要换设备不测