跳到主要内容

性能分析

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%精确
HybridSampling + 热点 Instrument1~2%折中

默认 Sampling, 生产环境用这个, 几乎无感。需要精确 FB 级数据时切 Instrumented。

采样时长

  • 实时窗口: 最近 N 秒数据滚动显示 (默认 60s)
  • 定时录制: 录制 N 秒后停止, 生成报告
  • 事件触发: 某条件 (如扫描 > 20ms) 触发录制前后各 5 秒

热点函数 (Hotspots)

最耗时的 POU 从上到下列出:

#POU类型调用次数/秒单次平均占比
1FB_PID_Temp.Execute方法10085 μs15.3%
2Main_OB1PROGRAM201.2 ms12.0%
3FB_Robot_Calc_IK方法100022 μs11.5%
4FB_Motor_ControlFB20055 μs10.8%
5DataLog_WriteBatchFUNCTION10350 μs7.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 读写1L1 缓存
INT 加减2ALU 整数运算
REAL 加减3SSE 浮点
REAL 乘4SSE
REAL 除15除法器慢
SQRT25SIMD sqrtss
SIN / COS80查表+插值
数组元素访问2边界检查+索引
STRING 拷贝 (32 字节)40memcpy
STRING 拼接 (短)120长度+拷贝
FB 调用开销15栈帧+输入拷贝
FUNCTION 调用开销8轻量
TON 执行30定时器逻辑
R_TRIG 执行5边沿检测
EtherCAT 单帧10000125μs 周期占 8%
OPC UA 读 (单变量)5000050μ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:

  1. 每次发布前录制 Profiler 报告
  2. 与基线对比
  3. 退化 > 5% → 构建失败
  4. 防止意外引入慢代码
# 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: 性能回归自动发现
  • 硬件基线: 给客户的承诺基于特定硬件, 不要换设备不测

相关文档