第 6 章 内存管理与表达式模板:tmp、autoPtr 与 fused 算子融合
第 5 章讲了 OpenFOAM 用什么类型表示数值。本章回答一个更"工程"的问题:在成千上万次"产生中间场、用它算下一个场、然后丢弃"的过程中,OpenFOAM 如何避免内存抖动、避免无谓拷贝、并把多层算子融合成一次遍历? 答案有三件武器:
tmp延迟拷贝语义 、表达式模板 、fused 算子融合。
6.1 内存管理的三个智能指针
OpenFOAM 有自己的一套智能指针家族(src/OpenFOAM/memory/),先厘清它们的分工:
| 类型 | 语义 | 典型用途 |
|---|---|---|
autoPtr<T> |
独占所有权(unique ownership) | 类里的"唯一拥有的子对象"(如 Time::controlDict_) |
tmp<T> |
拥有 或 引用 或 引用计数共享 | 算子返回值、中间场 |
refPtr<T> |
引用 + 可选所有权 | "可能拥有也可能只是指向别人"的成员 |
refCount |
侵入式引用计数基类 | 被 Field、FieldField、fvMatrix 等继承 |
autoPtr 相当于 std::unique_ptr 的等价物(可移动、不可拷贝)。tmp 则是 OpenFOAM 的"核心发明",也是本章重点。文档注释(tmp.H 第 30--38 行)一句话说清了它的定位:
cpp
Description
A class for managing temporary objects.
This is a combination of std::shared_ptr (with intrusive ref-counting)
and a shared_ptr without ref-counting and null deleter.
This allows the tmp to double as a pointer management and an indirect
pointer to externally allocated objects.
In contrast to std::shared_ptr, only a limited number of tmp items
will ever share a pointer.
也就是说:tmp 既是"带引用计数的管理指针",又能充当"指向外部对象的间接引用"(相当于 shared_ptr + 一个"空删除器")。
6.2 tmp<T> 的四种身份
tmp 内部用一个枚举 refType(tmp.H 第 86--93 行)区分四种身份:
cpp
enum refType
{
PTR, //!< A managed pointer (ref-counted)
CACHE_PTR, //!< A managed pointer (ref-counted), but immovable
REF_Types, //!< Magic value (ptr: less-than, ref: greater than)
CREF, //!< A const reference to an object
REF //!< A non-const reference to an object
};
PTR:托管一个真实拥有 的指针,参与引用计数。析构时若引用计数归零就delete。CACHE_PTR:托管但不可移动 (immovable)。当ptr()被调用要"释放"它时,只能clone()一份再给出去(tmpI.H第 290--291 行)。REF/CREF:只是外部对象的引用,不拥有、不计数,析构时什么都不做。REF_Types:一个"魔数"哨兵(小于表示指针、大于表示引用),用于快速区分。
这个设计的意义在于:一个返回 tmp<Field> 的算子,可能返回的是"新算出来的场"(PTR),也可能只是"把入参原样返回"(REF)。调用方无需关心区别,接口却是统一的。
6.3 关键操作:reuse、ptr()、clear()
6.3.1 reuse:零拷贝的核心开关
最关键的构造函数是(tmp.H 第 151 行):
cpp
//- Copy/move construct. Optionally reusing ref-counted pointer.
inline tmp(const tmp<T>& rhs, bool reuse);
其实现(tmpI.H 第 148--176 行):
cpp
template<class T>
inline Foam::tmp<T>::tmp(const tmp<T>& rhs, bool reuse)
:
ptr_(rhs.ptr_),
type_(rhs.type_)
{
if (is_pointer())
{
if (ptr_)
{
if (reuse)
{
rhs.ptr_ = nullptr; // 掏空来源,独占接管
rhs.type_ = PTR;
}
else
{
ptr_->refCount::operator++(); // 引用计数 +1,共享
}
}
...
}
}
reuse = true:把rhs的指针接管过来 ,并把rhs掏空------等价于"移动语义"。零拷贝。reuse = false:引用计数 +1,两个tmp共享同一个对象。
这套 reuse 真实语义,在 fvc:: 算子的签名里随处可见(例如 fvc::grad(U, "gradU") 会把结果写进一个可能是"复用存储"的临时对象)。理解 reuse,你就理解了 OpenFOAM 为什么能在大规模算例里把内存开销压到很低。
6.3.2 ptr():交付所有权(要求独占)
tmpI.H 第 263--292 行:
cpp
template<class T>
inline T* Foam::tmp<T>::ptr() const
{
if (!ptr_) FatalError ... "deallocated";
if (type_ == PTR)
{
if (!ptr_->refCount::unique())
{
FatalErrorInFunction
<< "Attempt to acquire pointer to object referred to"
<< " by multiple temporaries of type " ...
}
// Release pointer
T* p = ptr_;
ptr_ = nullptr;
return p;
}
// CACHE_PTR (immovable) is cloned, as per a reference
return ptr_->clone().ptr();
}
ptr() 的语义是"交出底层指针、放弃所有权 "。它强制要求该指针是独占 的(refCount::unique()),否则报错------这是 OpenFOAM 防"双重释放"的一道硬闸。对"引用型 tmp"或"不可移动缓存",它退化为 clone() 出一份新对象。
6.3.3 clear():析构即回收
tmpI.H 第 295--310 行:
cpp
template<class T>
inline void Foam::tmp<T>::clear() const noexcept
{
if (ptr_ && is_pointer())
{
if (ptr_->refCount::unique())
{
delete ptr_;
}
else
{
ptr_->refCount::operator--();
}
ptr_ = nullptr;
}
}
析构函数就是 clear()。引用计数归零则 delete,否则递减------这是标准的侵入式(intrusive)引用计数:计数存在对象内部 (因为 Field 等继承了 refCount),而非外部控制块。
6.3.4 越界使用的告警
tmp.H 第 104--105 行有个 checkUseCount():"Fatal if ref-count of a managed pointer is oversubscribed"。当同一对象被过多 tmp 共享时给出警告,帮助定位"意外共享导致重复计算/额外拷贝"的性能问题。
6.4 tmp 如何塑造了 fvm/fvc 的接口
OpenFOAM 的离散算子分成两类:
fvm::(隐式):返回tmp<fvMatrix<Type>>(一个待解方程矩阵);fvc::(显式):返回tmp<GeometricField<...>>(一个算好的场)。
它们几乎不用裸值 / 引用返回,而是一律返回 tmp。这样做有三个好处:
- 统一所有权 :调用方拿到
tmp后,可以.ref()(只读引用)、()(解引用)、或把它塞进下一个算子的reuse参数。 - 允许延迟:某些算子能识别"入参就是一个可复用的空场",从而把结果直接写进它,省一次分配。
- 配合表达式模板 :
tmp与表达式模板一起,让fvc::div(phi, U) + fvc::laplacian(nu, U)这类复合表达式的中间产物被逐一消灭。
这也是为什么 OpenFOAM 学习曲线陡峭------你必须先理解 tmp,才能读懂算子的签名与链式调用。
下表是常见"如何从 tmp 取值"的速查:
| 写法 | 语义 |
|---|---|
tmp<T> t = ...; |
构造/接管 |
const T& obj = t(); |
解引用(可读可写,取决于 const) |
t->member() |
指针式访问 |
t.ref() |
取引用(不转移所有权) |
t.ptr() |
交出指针(放弃所有权,要求独占) |
t.clear() / t.reset(...) |
释放 / 重置 |
T x = t;(隐式) |
触发一次 clone/拷贝(应尽量避免) |
6.5 表达式模板:把 a = b + c*d 变成一次遍历
6.5.1 问题
如果 Field + Field、Field * scalar 都用"立即求值"(返回新 Field)实现,那么 a = b + c*d 会产生:一个 c*d 的临时场、再产生一个 b + (...) 的临时场。对大网格(上亿单元)而言,这是巨大的内存带宽浪费。
表达式模板(Expression Templates)的思想是:运算符不立即求值,而是返回一个"表达式对象",把运算过程编码进类型系统;直到赋值(或显式求值)时,才在一个循环里一次性完成所有运算。
6.5.2 CRTP 基类 GenericExpression
src/OpenFOAM/expressionTemplates/GenericExpression.H(第 49--61 行):
cpp
template<typename E>
class GenericExpression
{
public:
static constexpr bool is_leaf = false;
auto evaluate() const
{
// Delegation to the actual expression type. This avoids dynamic
// polymorphism (a.k.a. virtual functions in C++)
return static_cast<E const&>(*this).evaluate();
}
};
这是经典的 CRTP + 静态多态 :evaluate() 通过 static_cast 分派到真正类型 E,完全避免虚函数开销。注释特意点明 "This avoids dynamic polymorphism"。
is_leaf 标记"这是叶子节点(真实的字段)还是中间节点(运算)"。它用于优化存储:叶子按引用保存,中间节点按值保存(见下面的宏)。
6.5.3 运算符类由宏批量生成
同一文件用宏生成一元/二元运算节点(第 68--96 行是 EXPRESSION_FUNCTION1 的展开):
cpp
#define EXPRESSION_FUNCTION1(BaseClass, Func, BaseFunc, OpFunc) \
template<typename E1> \
class OpFunc \
: \
public BaseClass<OpFunc<E1>> \
{ \
typename std::conditional<E1::is_leaf, const E1&, const E1>::type u_; \
\
public: \
static constexpr bool is_leaf = false; \
\
OpFunc(const E1& u) : u_(u) {} \
\
auto evaluate() const \
{ \
return BaseFunc(u_.evaluate()); \
} \
}; \
template<typename E1> \
OpFunc<E1> Func(const BaseClass<E1>& u) \
{ \
return OpFunc<E1>(static_cast<const E1&>(u)); \
}
要点:
OpFunc<E1>是一个运算节点,它保存操作数 (叶子按引用const E1&,中间节点按值const E1,由std::conditional在编译期选择),并实现evaluate():先求操作数,再施加BaseFunc。- 还生成了一个自由函数
Func(如sqrt、sin),以模板参数推导出表达式对象。 EXPRESSION_FUNCTION2同理生成二元运算(+、-、*、/)。
最终,b + c*d 的静态类型是一棵"运算树",赋值时才在单个循环里展开。
6.5.4 表达式模板的四个维度
src/OpenFOAM/expressionTemplates/ 里的文件说明它是"多维度"的:
ListExpression.H:List/Field层面的逐元素运算与归约(点积、外积、+ - * /等,由宏批量生成)。GeometricFieldExpression.H:几何场层面的表达式(把内部场与边界场一起处理)。dimensionSetExpression.H:维度在编译期沿表达式树传播 ------a = b + c*d中c*d的维度由c、d的维度相加得到。这是表达式模板最"聪明"的一处:把第 5 章的运行期维度检查,尽量前推到模板里。boolExpression.H:布尔表达式的类似处理。
6.5.5 tmp 与表达式模板的配合
注意,表达式模板并非取代 tmp,而是在单个表达式内部 消灭临时对象;跨语句、跨算子的对象生命周期仍由 tmp 管理。两者是互补的:
- 表达式模板 :
a = b + c*d内部零临时。 tmp:fvc::div(...)与fvc::laplacian(...)之间零拷贝传递。
6.6 fused:把手写算子融合推到极致
顶层目录 src/fused/ 是 v2606 里一个专门做"算子融合"的库。其子目录暴露了它的范围:
bash
src/fused/
├── finiteVolume/ # 融合版离散算子
│ ├── fusedGaussLaplacianScheme.H
│ ├── fusedGaussGrad.H
│ ├── fusedGaussDivScheme.H
│ ├── fusedGaussConvectionScheme.H
│ ├── fusedLeastSquaresGrad.H
│ └── fvcSurfaceOps.H
└── turbulenceModels/ # 融合版湍流模型
├── incompressibleKEpsilon.H
└── incompressibleKOmegaSST.H
以 fusedGaussLaplacianScheme.H(第 28--40 行)为例,注释直白说明它的目的:
cpp
Class
Foam::fv::GaussLaplacianScheme
Group
grpFvLaplacianSchemes
Description
Variant of GaussLaplacian that avoids intermediate fields
"avoids intermediate fields "(避免中间场)------融合版 fusedGauss 与原版 Gauss 的区别就在于:它把"插值表面场 → 计算面法向梯度 → 组装"这一串本来会产生多个中间场的步骤,压缩成尽量少的遍历,甚至一次完成。
在 src/Allwmake 里能看到它们被单独编译(第 98 行 wmake $targetType fused/finiteVolume、第 131 行 fused/turbulenceModels),并且通过 RTS 注册为可选项------你在 fvSchemes 里写 laplacianSchemes { default fusedGauss linear corrected; } 就会选中融合版。
6.6.1 fusedMultiplyAdd
融合的底层数值原语是 FMA(fused multiply-add) :a*b + c 在一条指令内完成、且只进行一次舍入(精度也更高)。src/OpenFOAM/expressionTemplates/ 里提供了 fusedMultiplyAdd 相关的仿函数,供 fused 版算子在内层循环里使用。这样,像"通量 × 梯度 + 源项"这类高频模式就能被编译器/硬件高效执行。
6.6.2 fused 的意义
fused 库的存在本身就是 OpenFOAM 的性能哲学的一次升级:
- 传统路径:
fvc::算子各自成篇,中间场虽被tmp优化,但仍有多次内存往返。 - 融合路径:把一连串算子手写成"一次遍历完成"的特化版本,换取内存带宽的大幅节约。
对于内存带宽受限的大规模 CFD(这正是 CFD 的常态),fused 往往能带来可观的加速。它也是"模板元编程 + 手写内层循环"结合的范例。
6.7 小结:性能三件套
把第 5、6 章合起来,OpenFOAM 的数值性能来自三件套的协同:
- 连续内存的
Field(第 5 章):缓存友好、可向量化。 tmp延迟拷贝 :跨算子零拷贝传递,reuse语义消除中间分配。- 表达式模板 + fused:单表达式内部零临时,关键算子手写融合。
理解这三者,就理解了"为什么一个纯 C++ 模板框架能在工业级 CFD 里跑得动"。下一章开始进入网格------有限体积法的空间基础。
本章要点回顾
autoPtr(独占)、tmp(拥有/引用/共享三合一)、refPtr(引用+可选拥有)、refCount(侵入式引用计数)分工明确。tmp的refType(PTR/CACHE_PTR/REF/CREF)区分"拥有"与"引用";reuse=true即零拷贝移动。ptr()交出所有权并要求独占;clear()/析构按引用计数delete或递减。fvm::/fvc::算子统一返回tmp,是零拷贝链式调用的基础。- 表达式模板用 CRTP
GenericExpression+ 宏生成运算节点,evaluate()静态分派、无虚函数,把a=b+c*d折叠成一次遍历。 dimensionSetExpression.H让维度在编译期沿表达式树传播,把维度检查尽量前推。src/fused/提供"避免中间场"的融合算子和湍流模型,底层用 FMA 原语,为内存带宽受限的大规模计算加速。