PFC 的 FISH 很好用。改边界、加监测、算统计,几行就能跑起来。
但模型一大,循环就慢得明显。
我拿一个 3.5 万个接触的 2D 试样做组构张量测试:FISH 跑一趟 146 ms,C++ 只要 4.2 ms。同一套公式、同一批接触,A、φ、N 逐点完全一样,速度差 34.9 倍。
先说结论:PFC 6.0 支持把 C++ 编译成 DLL,注册成 FISH 函数,在 FISH 里直接调用。它不是替代 FISH,而是把“遍历几万个元素”的内循环外包给编译后的 C++。大模型的重统计、重后处理,可以快一个数量级。
FISH 是解释执行的。loop foreach 每转一圈,contact.normal(c)、comp.x(...)、数组下标,都要走一次名字查找和参数转换。
单次开销不大,但乘以几十万次就明显了。
这不是写法问题,是解释器的固有成本。FISH 擅长的是“调度”:读参数、加边界、控制加载、注册监测。它不擅长“对 3 万个接触逐个做十几次浮点运算”。
PFC 6.0 给了一个出口:C++ FISH intrinsic 插件。
做法不复杂:写一个 DLL,实现 IFishLibrary 接口,用 program load function 加载。加载后,DLL 里注册的函数会变成 FISH 里的普通函数,名字类似 myfish.fabric。
图 1 左边是 FISH 逐条解释的循环;右边是编译进 DLL 的 C++ 循环。FISH 只发起一次调用,整段遍历在 C++ 里完成。
可以把它理解成:FISH 是调度员,C++ 是外包工人。调度员把“这一批接触的组构张量算一下”交出去,工人算完把三个数带回来。
很多人以为 C++ 插件只能做纯计算。实际上,拿到 IProgram 之后,可以访问 PFC 的元素接口:
删除球没有直接的 C++ 接口,可以发 ball delete 命令。逐 cycle 自动回调也没有注册入口,仍然用 FISH 的 fish callback 去调 C++。
所以定位很清楚:C++ 负责“对很多元素做同一件事”的内循环;FISH 负责流程和调度。
组构张量(Oda fabric tensor)是描述接触网络取向的常用量。二维里,对每个接触法向 n=(nx,ny),取走向向量 s=(-ny, nx):
A 越接近 0 越各向同性,越接近 1 越定向;φ 是主方向。
测试模型是一个受控 2D 盒式试样:
图 2 组构各向异性 A 随竖向应变的变化。橙色空心圆是 FISH,蓝色实线是 C++;两条曲线完全重合。
图 3 主走向角 φ 的演化。两边同样逐点重合。
| 4.19 ms | ||
| 109 ms | ||
图 4 26 次采样的总耗时对比:3804 ms 对 109 ms,聚合加速 34.9 倍。
需要说明:这组加载是加速的分步位移,只为了让接触网络在可接受的时间内演化,不是力学强度研究。A 的绝对值很小(0.0024 到 0.0041),φ 对微小各向异性很敏感,不要拿这组曲线下物理结论。
这里比的是“算得快不快”,不是“算得对不对”。 对不对由 A、φ、N 的逐点零差异回答。
官方安装目录里有模板:
它包含 IFishLibrary 的骨架、四个必须导出的函数,以及 ball/contact 遍历示例。
我把“复 制模板、替换工程名、改类名、改 DLL 命名”这一串做成了脚本:
生成:
模板里已经带了 ball/contact 遍历、组构张量和性能基准示例,不需要的可以删。
图 5 生成、编译、验证三步;第四步是在真实算例里加载使用。
DLL 必须导出四个函数:getName、getMajorVersion、getMinorVersion、createInstance。真正决定“FISH 里叫什么名字”的,是 createIntrinsics:
这里注册了一个叫 myfish.fabric 的函数:无参数,返回三浮点向量,只读。
get() 在函数出现在等号右边时调用;set() 在左边时调用。算法写在 get() 的 switch 里。
核心循环只有十几行:
getActive() 这一行很关键。FISH 的 contact.list('ball-ball') 只返回活动接触;C++ 的容器里还包含非活动接触。不过滤,接触数会多,A 会偏。
最后算 A 和 φ,把 (A, φ, N) 打包成 DVect3 返回:
FISH 侧这样取:
FISH 版算法长得几乎一样:
区别只在:一个由解释器逐条执行,一个是编译后的机器码。
产物:
两个容易忽略的点:
model configure plugins 不能省。少了它,加载看起来不报错,第一次调用才报“Must be configured for C++ plugins”。
如果要在已有 .sav 上继续算,先 load,再 restore。restore 不会自动找回插件;如果 .sav 本身没开 plugins,restore 后还要再执行一次 model configure plugins。
验证脚本会自动建一个最小模型,调用 freq、ball.loop、contact.loop、fabric,并检查日志:
日志里出现 function plugin myfish loaded 和 CHECK fabric A=,才算真正跑通。
一个简单的判据:如果一段 FISH 循环要跑很多次、每次要遍历上万个元素,就值得搬到 C++。
我把生成、编译、验证三步做成了 pfc-fish-cpp 技能:
gen_fish_plugin.py:模板 → 可编译工程;build_fish_plugin.py:自动找 MSBuild、SDK、Qt,编 2D/3D;verify_fish_plugin.py:建最小算例 → PFC 无头运行 → 日志验收。2D 和 3D 都实机跑过。模板里带组构张量和性能基准示例,可以直接改成自己的统计量。
一句话:C++ 不是来替代 FISH 的。FISH 继续负责“怎么算、什么时候算”,C++ 只负责“这一批元素怎么快速算完”。分工清楚,代码就不会失控。
FISH 的解释器让 PFC 很灵活,但也决定了它不适合扛大循环。
C++ FISH intrinsic 的价值,不是把整个脚本改写成 C++,而是只把最热的那一层循环换掉:数据还在 PFC 里,流程还在 FISH 里,结果由编译后的 DLL 返回。
35k 接触的组构张量,146 ms 变成 4 ms。这个差距,在长时间加载和多次采样的算例里,会直接体现在墙钟上。
VibePFC 社区 · 一起把 AI 玩进 PFC
我成立了一个 VibePFC 社群,专门聊 AI 怎么落地到 PFC / 数值模拟——让大模型读懂模型、自己改脚本、自动排查"看不懂"的问题。收一个 50 元的门槛费。
想进群的同学,加 QQ 763388012,备注"vibePFC"即可。
program load function、IFishLibrary)。_archive/fish-cpp-plugin-test(含 fabric_evolve_box.dat、fabric_benchmark.csv)。~/.claude/skills/pfc-fish-cpp(生成 / 编译 / 验证三脚本)。