PAYATHON 2026

Modelica 中如何避免以 parameter 为输入的函数被重复求值?

数据小陈

如何避免重复计算?

将 myFun2(z) 移到模型级的 parameter 绑定中,再把结果传给 myFun1。这样可以把昂贵计算从随时间变化的函数调用路径中移出,无需依赖 myFun2 的 inlining。

仿真运行阶段可以复用参数值,但这并不保证纯函数的函数体从模型翻译到仿真结束,在所有工具中都恰好执行一次。工具可能提前求值或消除调用。

原写法为什么会重复求值?

原代码中的 k 是函数局部变量,其绑定如下:

protected
Real k = myFun2(b);

每次调用 myFun1 时,k 都会初始化,结果不会跨调用保存。这一行为由 Modelica 规范 §12.4.4 定义。

调用处传入的 z 虽然是 parameter,但 myFun1 中的 b 只是一个 input Real。由于 a=x 随时间变化,整个 myFun1(a=x, b=z) 仍需随模型求值。编译器可以优化其中不变的部分,但原写法没有明确建立可复用的模型级参数结果。

如何修改模型?

把 myFun1 的第二个输入改为已经算好的系数,并在模型中声明 kCached:

model ModelFunFunCached
  Real x, y;
  parameter Real z = 3;

protected
  parameter Real kCached = myFun2(z);

  function myFun1
    input Real a;
    input Real k;
    output Real c;
  algorithm
    c := k*a;
  end myFun1;

  function myFun2
    input Real d;
    output Real e;
  algorithm
    e := d*d;
  end myFun2;

equation
  x = time -2;
  y = myFun1(a=x, k=kCached);

end ModelFunFunCached;

修改后,先由 z 计算参数 kCached,之后 myFun1 只读取这个参数。示例在数学上仍满足 y = 9*(time - 2)。

对于真实模型,应把所有只依赖参数的昂贵准备工作移到这一步,并将随时间变化的计算留在 myFun1 中。如果准备结果包含多个值,可以考虑用 record 组织后再传入。

这是一种标准的 Modelica 建模方式,可用于 Dymola、OpenModelica 等工具,不依赖某个工具的缓存开关。具体是在翻译阶段还是初始化阶段求值,需要结合工具版本和生成代码确认。

如何验证调用次数?

可以继续使用日志做初步检查。在 myFun2 的 algorithm 开头临时加入:

Modelica.Utilities.Streams.print(
  "myFun2 called", "myFun2_calls.txt");

每次测试前清空旧日志,并确认工具实际使用的工作目录。比较修改前后的记录时,注意区分翻译、初始化和时间积分阶段。参数计算可能提前发生,因此仿真运行日志中没有记录也可能是正常结果。

若需要确认实际执行次数,可使用当前工具版本提供的函数 profiling 功能;如果没有合适的统计项,可以检查生成代码,或在可调试的生成程序中给 myFun2 设置断点并统计命中次数。内联或常量折叠后,函数可能不再作为独立调用存在。

日志可用于辅助排查,但不能据此推断纯函数必须执行多少次。Modelica 规范 §12.3 允许编译器合并或省略符合条件的纯函数调用。统计结果也只代表当前的构建和运行配置。

使用时要注意什么?

  • kCached 必须只依赖仿真期间不变的数据。如果真实的 myFun2 还依赖变化中的外部状态,就不能直接套用这个方案。
  • 声明需要移到模型级。只在 myFun1 内把局部 k 改成 parameter,不能将其变成跨函数调用的持久缓存。
  • 参数扫描或下一次仿真改变 z 后,需要重新得到对应的 kCached。这里复用的是一次仿真所用的参数结果。
  • 不要为了计数随意把函数改为 impure,这会改变调用限制和优化条件,影响与原模型的比较。

备注:内容仅供参考。