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,这会改变调用限制和优化条件,影响与原模型的比较。
备注:内容仅供参考。
