1⃣ 跨平台架构目标(Cross-Platform Architecture Goals)
在设计跨平台系统时,目标包括:
- 充分利用各个平台特性
- 不仅考虑功能,还要利用不同平台的性能优化和特性。
- 关注编译器特性
- 利用编译器提供的优化、警告、概念(concepts)等功能。
- 最小化样板代码(boilerplate)
- 避免重复写大量初始化、模板或辅助函数。
- 减少冗余代码
- 例如重复实现同一逻辑或类型特化。
- 尽量不修改已有代码
- 保证已有功能稳定,减少回归风险。
- 尽量减少预处理宏(preprocessor macros)
- 宏容易引入不可控的副作用和调试困难,应优先用模板、概念或
constexpr替代。
- 宏容易引入不可控的副作用和调试困难,应优先用模板、概念或
总结:目标是写高效、可维护、可扩展的跨平台代码。
2⃣ 设计方案(The Design)
- 示例:四元数类(Quaternion)族
- 这是一个大型项目中的例子,用于说明跨平台设计。
- 项目构建问题
- 不同平台可能需要包含不同的头文件。
- 概念层次(Concept Hierarchies)
- 使用 C++20 概念(concept)来定义类型约束和接口契约。
- 类和函数设计
- 通过模板和泛型编程减少重复代码。
- 提供统一接口,屏蔽平台差异。
核心思想:在不同平台上复用核心逻辑,同时对外提供统一接口。
3⃣ 开放–封闭原则(OCP: Open–Closed Principle)
- 定义:
对扩展开放,对修改封闭。
系统应允许通过添加新代码实现新功能,而不是修改已有代码。 - 在 C++ 中实现方式:
- 委托式多态(delegated polymorphism)
- 使用虚函数、接口类、模板或概念,将功能扩展委托给新类或函数,而不修改原有类。
- 好处:
- 避免“级联修改”(cascade of changes)。
- 提高代码可维护性和稳定性。
- 委托式多态(delegated polymorphism)
- 引用:
“Since programs that conform to the open–closed principle are changed by adding new code, rather than by changing existing code, they do not experience the cascade of changes exhibited by non-conforming programs.”
— Robert C. Martin (1996)
小结
- 跨平台架构设计目标:
- 高性能、低冗余、低样板、易维护。
- 设计方法:
- 使用模板、概念、抽象类和统一接口屏蔽平台差异。
- 遵守 OCP:
- 通过扩展而不是修改已有代码实现新功能。
- 利用多态、概念和模板实现委托式扩展。
例子:绘制图形 — C 风格(未应用开放–封闭原则 OCP)
1⃣ 数据结构(Shape Data)
在 C 语言中,为了表示不同类型的形状,通常会使用enum和struct:
typedefenum{circle,square}shape_type;shape_type枚举定义了形状类型:圆形(circle)或正方形(square)。
typedefstruct{shape_type type;intradius;}circle_shape;typedefstruct{shape_type type;intside_length;}square_shape;circle_shape结构体包含:type:形状类型,必须是circleradius:半径
square_shape结构体包含:type:形状类型,必须是squareside_length:边长
问题点:
- 每个形状都有自己的 struct,需要手动管理。
- 数据和行为(绘制函数)是分开的,缺乏封装。
2⃣ 绘图函数(Drawing Functions)
针对每种形状单独实现绘制函数:
voiddraw_circle(circle_shape*c);voiddraw_square(square_shape*s);draw_circle专门绘制圆形draw_square专门绘制正方形- 这意味着每添加一种新形状都需要新增函数。
3⃣ 绘制多个形状(draw_shapes 函数)
voiddraw_shapes(shape**shapes,intn){inti;for(i=0;i<n;i++){switch(shapes[i]->type){casecircle:draw_circle((circle_shape*)(shapes[i]));break;casesquare:draw_square((square_shape*)(shapes[i]));break;}}}shapes是一个指向形状的指针数组。- 使用
switch判断形状类型:- 如果是
circle,调用draw_circle - 如果是
square,调用draw_square
- 如果是
- 使用强制类型转换(cast)将
shape*转为具体类型指针。
4⃣ C 风格设计的问题点
- 违反开放–封闭原则(OCP):
- 如果要新增形状(比如三角形),必须修改
draw_shapes函数,违反“封闭修改”原则。
- 如果要新增形状(比如三角形),必须修改
- 类型安全问题:
- 使用
void*或强制类型转换容易出错。 - 编译器无法检查类型正确性。
- 使用
- 行为分离:
- 绘制逻辑在外部函数中,而数据结构只包含数据,缺乏封装。
- 可维护性差:
- 每新增形状需要修改多个地方(枚举、结构体、绘图函数、switch-case)。
总结
- 这是典型的 C 风格实现,以数据驱动类型判断(
enum + switch)来实现多态。 - 问题在于:
- 扩展困难(添加新形状需要修改已有函数)
- 类型不安全
- 行为与数据分离
- 在 C++ 中,OCP 可以通过继承 + 虚函数或概念 + 模板来改进,避免修改已有代码,只通过扩展新类或新模板实现新的形状。
例子:绘制图形 — C++ 多态方式(Polymorphic C++ Way)
1⃣ 形状类(Shape Classes)
抽象基类shape
classshape{public:virtualvoiddraw()const=0;};shape是一个抽象基类(abstract class)。- 包含纯虚函数
draw():- 表示所有形状都必须实现绘制方法。
- 封装与多态:
- 数据和行为封装在类里。
- 用户无需关心具体形状的实现。
派生类circle
classcircle:publicshape{intradius;public:voiddraw()constoverride;};circle继承自shape- 包含私有数据成员
radius - 重写
draw()方法,实现圆形绘制逻辑
派生类square
classsquare:publicshape{intside_length;public:voiddraw()constoverride;};square继承自shape- 包含私有数据成员
side_length - 重写
draw()方法,实现正方形绘制逻辑
2⃣ 绘制函数(Draw Function)
#include"shape.h"voiddraw_shapes(std::vector<shape*>shapes){for(autoconst*s:shapes){s->draw();}}shapes是一个指向shape*的向量- 核心点:
- 通过虚函数机制(virtual function)调用
draw()方法 - 实现运行时多态(runtime polymorphism)
- 通过虚函数机制(virtual function)调用
- 不需要
switch-case或类型转换 - 新增形状只需要:
- 新建派生类,重写
draw()方法 - 不需要修改
draw_shapes函数
- 新建派生类,重写
3⃣ 多态 C++ 风格的优点
- 符合开放–封闭原则(OCP)
draw_shapes函数无需修改,支持新形状扩展。
- 类型安全
- 无需强制类型转换,编译器检查类型。
- 封装数据和行为
- 每个形状类封装自己的数据和绘制逻辑。
- 易维护
- 扩展新形状时,只需增加新类,不破坏已有代码。
- 可复用性和可读性强
- 用户只使用基类接口
shape*,无需了解具体实现。
- 用户只使用基类接口
总结
| 特性 | C 风格 | C++ 多态风格 |
|---|---|---|
| 数据与行为 | 分离 | 封装在类里 |
| 类型安全 | 不安全,需要 cast | 安全,虚函数多态 |
| 扩展性 | 需修改 switch-case | 只需新增类 |
| 遵守 OCP | 否 | 是 |
| 可维护性 | 差 | 好 |
总结一句话:
C++ 多态方式通过继承与虚函数,将“类型判断 + 绘制逻辑”隐藏在类内部,实现了开放–封闭原则和更好的封装。
1⃣ 原始开放–封闭原则(Original OCP)
引用 Bertrand Meyer(1988)的定义:
- 模块应既“封闭”又“开放”:
- 封闭(Closed)
- 一旦模块的服务对外公开,客户端依赖这些服务进行开发时,模块的现有功能不应受到新服务引入的影响。
- 换句话说,已有模块接口稳定,现有客户代码不受影响。
- 开放(Open)
- 模块应能扩展新功能以支持未来的需求,而无需修改已有代码。
- 因为我们不可能一开始就实现所有客户端可能需要的功能。
- 封闭(Closed)
2⃣ OCP 在 C++ 中的体现
在 C++ 中,OCP 可以分为弱原则(Weak OCP)和强原则(Strong OCP):
(1) 无 OCP(No OCP)
- 行为:
- 必须修改已有代码来添加新功能。
- 客户端代码需要根据修改做出相应变化。
- 问题:
- 现有模块不稳定,维护成本高。
- 修改模块会引发连锁修改。
(2) 弱 OCP(Weak OCP)
- 行为:
- 添加新功能时仍然需要修改已有代码,但现有接口和行为保持不变。
- 需要重新编译受影响的模块。
- 特点:
- 接口稳定,但实现需要改动。
- 客户端依赖的接口不变,但模块的代码仍需维护。
(3) 强 OCP(Strong OCP)
- 行为:
- 添加新功能完全无需修改已有代码。
- 不影响现有模块的编译和客户端代码。
- 实现方式:
- 通过多态(polymorphism)或委托(delegation)来扩展功能。
- 保持模块接口和实现的独立性。
- 优点:
- 最大化模块稳定性和可维护性。
- 支持真正的“开闭”,即模块对扩展开放,对修改封闭。
3⃣ 对比总结
| 类型 | 修改已有代码 | 客户端影响 | 编译影响 |
|---|---|---|---|
| 无 OCP | 需要修改 | 客户端需改动 | 是 |
| 弱 OCP | 需要修改 | 客户端无需改动 | 是 |
| 强 OCP | 不修改 | 客户端无需改动 | 否 |
关键点
- 封闭(Closed)
- 已有模块稳定,客户端依赖的接口不变。
- 开放(Open)
- 模块可以扩展新功能,不修改已有模块代码。
- C++ 实践:
- 强 OCP 通常通过虚函数、多态、接口抽象实现。
- 弱 OCP 可能仍需修改实现,但保证接口稳定。
总结一句话:
OCP 的核心是“扩展无需修改”。强 OCP 通过抽象和多态实现,弱 OCP则保证接口不变但实现可修改。
1⃣ 架构设计指导原则(Architecture Guidelines)
- 设计非常接近Bertrand Meyer的原始设想:
- 不局限于传统面向对象(OOP),数据与函数可以分离。
- 添加新平台不应影响已实现的平台:
- 新增对某个平台的支持时,已有平台代码无需修改。
- 添加功能的版本或修订不应影响已实现的版本:
- 扩展功能版本时,已有版本仍然保持稳定。
2⃣ 平台(Platform)定义
- 平台:特定的功能集(feature set),每个平台包含实现同一功能但针对不同机器架构的实现。
- 功能(Feature):抽象的功能单元,需要根据目标机器架构实现不同版本。
- 硬件相关:
- CPU 架构(x86, ARM)
- SIMD 指令集(SSE, SSE2, Neon)
- DMA 控制器、GPIO 模块等
- 软件相关:
- 操作系统(Windows, Linux)
- 图形 API(DirectX, OpenGL)
- 硬件相关:
- 注意:功能不一定完全正交,例如:
- x86 + SSE
- DirectX + Windows
- 功能之间可能存在耦合关系。
3⃣ 目录和文件结构
(1) Flat 结构(平铺)
plt/simd Simd.h Neon32.h Sse.h Sse2.h ... plt/math Quat.h Quat_Common.h Quat_Neon32.h Quat_Sse.h Quat_Sse2.h ...- 所有文件平铺在同一目录下。
- 易于查找,但文件数量多时,可能杂乱。
(2) Deep 结构(分层)
plt/simd Simd.h Neon32.h Sse.h Sse2.h ... plt/math Quat.h Common/ Quat_Common.h Vec_Common.h Mtx_Common.h ... Neon32/ Quat_Neon32.h Vec_Neon32.h Mtx_Neon32.h ...- 使用子目录按功能或平台分类。
- 优点:
- 文件结构清晰,便于管理平台相关实现。
- 避免命名冲突。
- 缺点:
- 包含路径较长,需要宏或别名辅助引用。
4⃣ 头文件包含策略
- 每个功能(Feature)都有自己的头文件:
- 定义公共接口或宏。
- 头文件负责包含平台特定的实现。
- 使用预处理宏自动生成包含的文件名:
- 例如:
会展开为:INCLUDE_SIMD(Quat)"Quat_SSE2.h" - 这样用户只需写
INCLUDE_SIMD(Quat),无需关心平台实现。
- 例如:
5⃣ 头文件包含宏(示例)
#defineINCLUDE_PLT(Feature,File)INCLUDE_BUILD_FILENAME(Feature,File)#defineINCLUDE_PLT_FEATURE(Feature)INCLUDE_STRINGIZE(Feature.h)#defineINCLUDE_BUILD_FILENAME(Feature,File)INCLUDE_STRINGIZE(File##_##Feature.h)#defineINCLUDE_STRINGIZE(String)#String#defineINCLUDE_SIMD(File)INCLUDE_PLT(PLT_SIMD,File)- INCLUDE_STRINGIZE(String)
- 将宏参数转换为字符串。
- INCLUDE_BUILD_FILENAME(Feature, File)
- 拼接文件名,例如
File_Feature.h。
- 拼接文件名,例如
- INCLUDE_PLT(Feature, File)
- 生成平台相关的完整文件名。
- INCLUDE_SIMD(File)
- 针对 SIMD 功能,自动选择当前平台的 SIMD 文件。
总结:
- 通过宏封装平台相关实现,保持代码抽象。
- 用户只需要关注功能接口(如
Quat),无需关心平台差异。- 符合开放–封闭原则(OCP),添加新平台无需修改现有代码。
1⃣ 创建功能(Creating a Feature)
- 定义功能:
- 在构建系统(build system)中定义功能宏。
- 创建头文件:
- 为每个功能创建一个公共头文件(feature header)。
- 创建实现文件:
- 为每种特定平台或实现版本创建独立的头文件。
目的是保持功能接口统一,而实现可以随平台变化而变化。
2⃣ SIMD 功能在构建系统中的配置
- 在工具链文件中设置平台宏:
set(PLT_SIMD Common) - 在公共项目中,将宏加入编译预处理定义:
add_compile_definitions( PLT_SIMD=${PLT_SIMD} )
这样在编译时,代码可以根据
PLT_SIMD的值自动选择平台特定实现。
3⃣Simd.h:SIMD 功能头文件
#if!defined(PLT_SIMD)#errorYou must define PLT_SIMD.#endif#defineINCLUDE_SIMD(File)INCLUDE_PLT(PLT_SIMD,File)#includeINCLUDE_PLT_LOCAL(PLT_SIMD)- 功能说明:
- 检查是否定义
PLT_SIMD,如果没有定义,则报错。 - 定义宏
INCLUDE_SIMD(File),用于包含平台特定实现。 - 根据当前平台包含相应的 SIMD 文件。
- 检查是否定义
4⃣Simd_Common.h:通用 SIMD 头文件
namespaceplt::simd{structCommon{};}- 提供一个空的通用结构体,用于标记或作为概念实现的基础。
- 平台特定实现可继承或扩展此结构体。
5⃣ 四元数的数学定义(Quaternions, Mathematically Speaking)
- 四元数(Quaternion):
q = w + x i + y j + z k q = w + x i + y j + z kq=w+xi+yj+zk - 基本单位:
i 2 = j 2 = k 2 = i j k = − 1 i^2 = j^2 = k^2 = i j k = -1i2=j2=k2=ijk=−1 - 乘法规则:
i j = k = − j i , j k = i = − k j , k i = j = − i k i j = k = - j i, \quad j k = i = - k j, \quad k i = j = - i kij=k=−ji,jk=i=−kj,ki=j=−ik
5.1 四元数的运算
- 加法:
( a w + a x i + a y j + a z k ) + ( b w + b x i + b y j + b z k ) = ( a w + b w ) + ( a x + b x ) i + ( a y + b y ) j + ( a z + b z ) k (a_w + a_x i + a_y j + a_z k) + (b_w + b_x i + b_y j + b_z k) = (a_w + b_w) + (a_x + b_x)i + (a_y + b_y)j + (a_z + b_z)k(aw+axi+ayj+azk)+(bw+bxi+byj+bzk)=(aw+bw)+(ax+bx)i+(ay+by)j+(az+bz)k - 乘法:
( a w + a x i + a y j + a z k ) ( b w + b x i + b y j + b z k ) = ( a w b w − a x b x − a y b y − a z b z ) + ( a w b x + a x b w + a y b z − a z b y ) i + ( a w b y − a x b z + a y b w + a z b x ) j + ( a w b z + a x b y − a y b x + a z b w ) k \begin{aligned} (a_w + a_x i + a_y j + a_z k)(b_w + b_x i + b_y j + b_z k) = &(a_w b_w - a_x b_x - a_y b_y - a_z b_z) \ &+ (a_w b_x + a_x b_w + a_y b_z - a_z b_y)i \ &+ (a_w b_y - a_x b_z + a_y b_w + a_z b_x)j \ &+ (a_w b_z + a_x b_y - a_y b_x + a_z b_w)k \end{aligned}(aw+axi+ayj+azk)(bw+bxi+byj+bzk)=(awbw−axbx−ayby−azbz)+(awbx+axbw+aybz−azby)i+(awby−axbz+aybw+azbx)j+(awbz+axby−aybx+azbw)k - 共轭(Conjugate):
q ∗ = w − x i − y j − z k q^* = w - x i - y j - z kq∗=w−xi−yj−zk - 点积(Dot Product):
a ⋅ b = a w b w + a x b x + a y b y + a z b z a \cdot b = a_w b_w + a_x b_x + a_y b_y + a_z b_za⋅b=awbw+axbx+ayby+azbz - 模(Norm):
∣ q ∣ = q ⋅ q = w 2 + x 2 + y 2 + z 2 |q| = \sqrt{q \cdot q} = \sqrt{w^2 + x^2 + y^2 + z^2}∣q∣=q⋅q=w2+x2+y2+z2 - 乘法逆元(Multiplicative Inverse):
q − 1 = q ∗ q ⋅ q q^{-1} = \frac{q^*}{q \cdot q}q−1=q⋅qq∗ - 除法:
a b = a b − 1 = a b ∗ b ⋅ b \frac{a}{b} = a b^{-1} = \frac{a b^*}{b \cdot b}ba=ab−1=b⋅bab∗
6⃣ 四元数概念在 C++20 中的映射
- 数学定义:
- 四元数由数据与运算定义。
- C++20 概念(Concept)定义:
- 四元数由数据成员定义,运算可基于概念实现。
- 实现策略:
- 通用实现依赖于概念定义。
- 平台优化实现需符合概念,并可替换通用实现。
总结:
- 概念(Concept)定义了接口和数据结构约束。
- 实现可以依赖概念进行优化和扩展,保证 OCP(开放–封闭原则)。
1⃣ Quaternion 概念(C++20 Concept)
template<typenameQ>conceptQuaternion=requires(Q q){typenameQ::Scalar;Arithmetic<typenameQ::Scalar>;{q.w()}->std::same_as<typenameQ::Scalar>;{q.x()}->std::same_as<typenameQ::Scalar>;{q.y()}->std::same_as<typenameQ::Scalar>;{q.z()}->std::same_as<typenameQ::Scalar>;};解析:
Q::Scalar表示四元数元素的标量类型(例如float或double)。Arithmetic<typename Q::Scalar>确保标量类型是算术类型。{ q.w() } -> std::same_as<typename Q::Scalar>等语句要求Q提供访问四元数各分量的函数,返回类型为标量类型。
对应数学上的四元数:
q = w + x i + y j + z k q = w + x i + y j + z kq=w+xi+yj+zk
其中w , x , y , z w, x, y, zw,x,y,z对应q.w(), q.x(), q.y(), q.z()。
2⃣ 支持性概念(Supporting Concepts)
Arithmetic
template<typenameT>conceptArithmetic=std::is_arithmetic_v<T>;- 限定标量类型必须是算术类型(整数或浮点数)。
MutuallyArithmetic
template<typenameT,typenameU>conceptMutuallyArithmetic=requires(T t,U u){requiresArithmetic<T>;requiresArithmetic<U>;{t+u};{t-u};{t*u};{t/u};};- 限定两个类型间可以进行算术运算。
- 用于实现四元数间或四元数与标量间的运算。
3⃣ 标准四元数类型声明(Standard Quaternion Type)
template<typenameS,typenameI=plt::simd::PLT_SIMD>classQuat{public:usingScalar=S;private:Scalar w_,x_,y_,z_;// ... 构造函数// ... 访问器};S:标量类型(例如float、double)。I:SIMD 平台选择(可用于优化实现)。- 私有成员
w_, x_, y_, z_存储四元数分量。
4⃣ 构造函数(Constructors)
Quat()=default;Quat(Scalar w,Scalar x,Scalar y,Scalar z)noexcept(...):w_(w),x_(x),y_(y),z_(z){}Quat(Scalar&&w,Scalar&&x,Scalar&&y,Scalar&&z)noexcept(...):w_(std::move(w)),x_(std::move(x)),y_(std::move(y)),z_(std::move(z)){}template<Quaternion Q>requiresstd::convertible_to<typenameQ::Scalar,Scalar>Quat(constQ&rhs)noexcept(...):Quat(Scalar{rhs.w()},Scalar{rhs.x()},Scalar{rhs.y()},Scalar{rhs.z()}){}- 支持拷贝构造、移动构造、以及从其他符合
Quaternion概念的四元数构造。 - 保证了类型安全与异常安全(
noexcept)。
5⃣ 赋值操作符(Assignment Operator)
template<Quaternion Q>requiresstd::convertible_to<typenameQ::Scalar,Scalar>Quat&operator=(constQ&rhs)noexcept(...){w_=Scalar{rhs.w()};x_=Scalar{rhs.x()};y_=Scalar{rhs.y()};z_=Scalar{rhs.z()};return*this;}- 支持从任意满足
Quaternion概念的四元数赋值。 - 自动转换标量类型,保证类型安全。
6⃣ 访问器(Accessors)
constScalar&w()constnoexcept{returnw_;}constScalar&x()constnoexcept{returnx_;}constScalar&y()constnoexcept{returny_;}constScalar&z()constnoexcept{returnz_;}- 提供对四元数各分量的只读访问。
- 与数学表示一一对应:
q = w + x i + y j + z k q = w + x i + y j + z kq=w+xi+yj+zk
7⃣ 泛型操作实现(General Operation Implementation)
- 使用表达式树(expression trees)来实现操作。
- 可在任何平台上运行。
- 支持任何算术标量类型。
- 参数类型通过C++20 Concepts限定,保证接口一致性。
总结:
- C++20 的概念使四元数实现可泛化、类型安全且可优化。
- 数学上的四元数运算与 C++ 概念紧密对应。
- 可根据 SIMD 或其他平台进行优化实现而无需修改接口,符合OCP(开放–封闭原则)。
1⃣ 什么是表达式树(Expression Tree)
- 表达式树是一种延迟求值(lazy evaluation)的表示方法,将表达式拆分为节点,每个节点表示运算或操作数。
- 示例表达式:
q 1 + q 2 ∗ q 3 q_1 + q_2 * q_3q1+q2∗q3 - 对应的表达式树如下:
QuaternionAddition / \ q1 QuaternionMultiplication / \ q2 q3每个运算符(如
+、*)在树中生成一个节点,操作数为叶子节点。
2⃣ C++ 实现:运算符和表达式节点
2.1 加法运算符(Operator Overload)
template<Quaternion QL,Quaternion QR>inlineautooperator+(constQL&lhs,constQR&rhs)noexcept->QuaternionAddition<QL,QR>{returnQuaternionAddition(lhs,rhs);}- 该运算符返回一个表达式节点
QuaternionAddition。 - 实际的加法操作不会立即执行,而是构建表达式树。
2.2 四元数二元表达式基类
template<Quaternion QL,Quaternion QR>requiresMutuallyArithmetic<typenameQL::Scalar,typenameQR::Scalar>classQuaternionBinaryExpr:publicQuaternionExpr{usingSL=typenameQL::Scalar;usingSR=typenameQR::Scalar;public:usingScalar=typenamestd::common_type<SL,SR>::type;};- 通过
MutuallyArithmetic确保两边标量类型可以算术运算。 - 使用
std::common_type推导结果标量类型,保证兼容不同标量类型。
2.3 加法表达式节点
template<Quaternion QL,Quaternion QR>classQuaternionAddition:publicQuaternionBinaryExpr<QL,QR>{QL l_;QR r_;public:usingScalar=typenameQuaternionBinaryExpr<QL,QR>::Scalar;QuaternionAddition(constQL&lhs,constQR&rhs)noexcept:l_(lhs),r_(rhs){}Scalarw()constnoexcept{returnl_.w()+r_.w();}Scalarx()constnoexcept{returnl_.x()+r_.x();}Scalary()constnoexcept{returnl_.y()+r_.y();}Scalarz()constnoexcept{returnl_.z()+r_.z();}};- 保存左右子表达式。
- 访问函数(
w(), x(), y(), z())在访问时计算对应分量的加法。 - 延迟求值保证了表达式树可以组合复杂运算。
3⃣ 标量函数示例
template<Quaternion QL,Quaternion QR>inlineautoDot(constQL&lhs,constQR&rhs)noexcept->QuaternionBinaryType<QL,QR>{returnlhs.w()*rhs.w()+lhs.x()*rhs.x()+lhs.y()*rhs.y()+lhs.z()*rhs.z();}- 计算两个四元数的点积:
a ⋅ b = a w b w + a x b x + a y b y + a z b z a \cdot b = a_w b_w + a_x b_x + a_y b_y + a_z b_za⋅b=awbw+axbx+ayby+azbz - 返回标量类型,依赖于
QuaternionBinaryType的推导。
4⃣ 通用实现(Common Implementation)
Quat<float>eval(Quat<float>a,Quat<float>b,Quat<float>c){return(a*b+c)/(a-c);}- 所有四元数表达式基于概念(Concepts)和通用类实现。
- 可在任意平台上运行,只要标量类型在该平台上支持。
- 编译器会根据表达式树进行优化。
优点:
- 跨平台统一实现。
- 无需手动展开所有组合运算。
- 可依赖编译器进行自动优化。
5⃣ 针对 ARM Neon 优化
5.1 构建系统更新
- 修改 SIMD 宏:
set(PLT_SIMD Neon32)- 可能需要设置编译器标志以启用 Neon。
5.2 Neon32 特性头文件
namespaceplt::simd{structNeon32:Common{};template<typenameSIMD>conceptNeon32Family=std::derived_from<SIMD,Neon32>;}- 提供平台标识和概念约束。
5.3 四元数类针对 Neon32
template<>classQuat<float,plt::simd::Neon32>{float32x4_t value_;// Neon 寄存器public:usingScalar=float;Quat()=default;Quat(Scalar w,Scalar x,Scalar y,Scalar z){floatvals[]={w,x,y,z};value_=vld1q_f32(vals);// 加载到 Neon 寄存器}Scalarw()constnoexcept{returnvgetq_lane_f32(value_,0);}Scalarx()constnoexcept{returnvgetq_lane_f32(value_,1);}Scalary()constnoexcept{returnvgetq_lane_f32(value_,2);}Scalarz()constnoexcept{returnvgetq_lane_f32(value_,3);}};- 使用 Neon SIMD 寄存器存储四元数。
- 构造函数和访问器直接操作 SIMD 寄存器。
- 所有运算仍可使用通用实现,只是数据存储和加载更快。
6⃣ 当前状态总结
- 通用表达式树实现已经完成。
- 四元数可在 Neon 寄存器中进行存储和运算。
- 通用算法无需修改,即可获得平台优化的性能。
- 可在不同平台使用不同 SIMD 优化实现而不改变接口,实现开放–封闭原则(OCP)。
1⃣ 平台特化:ARM Neon32 四元数函数
template<plt::simd::Neon32Family SIMD>inlineautooperator-(Quat<float,SIMD>q)->Quat<float,SIMD>{float32x4_t value=q.NeonVal();float32x4_t negation=(-q).NeonVal();float32x4_t result=vcopyq_laneq_f32(negation,0,value,0);returnQuat<float,SIMD>(result);}- 针对 Neon32 平台的四元数取负操作。
- 使用 Neon SIMD 寄存器
float32x4_t,将四个浮点数打包到单个寄存器。 - 延迟求值仍然生效,表达式树可以使用这个特化类型执行运算。
vcopyq_laneq_f32是 Neon 特定的向量操作,将特定元素复制或重新排列。
1.1 平台特化使用示例
Quat<float>eval(Quat<float>a,Quat<float>b,Quat<float>c){return(a*b+c)/(a-c);}- 对于 Neon32 特化,
a, b, c的数据会在 Neon 寄存器中进行运算。 - 通用表达式树和算法无需修改,依然适用。
2⃣ SSE 平台特化
2.1 构建系统更新
- 创建新的工具链文件并设置:
set(PLT_SIMD SSE)- SSE 平台使用 128-bit 寄存器,可同时存放 4 个浮点数(float),不支持 2 个 double 同时存储。
2.2 SSE 特性头文件
namespaceplt::simd{structSse:Common{};template<typenameSIMD>conceptSseFamily=std::derived_from<SIMD,Sse>;}- 用于标识 SSE 平台,方便模板约束。
2.3 四元数 SSE 特化类
template<>classQuat<float,plt::simd::Sse>{__m128 value_;public:usingScalar=float;Quat(Scalar w,Scalar x,Scalar y,Scalar z){value_=_mm_setr_ps(w,x,y,z);// 加载到 SSE 寄存器}Scalarw()constnoexcept{return_mm_cvtss_f32(SseVal());}Scalarx()constnoexcept{return_mm_cvtss_f32(_mm_shuffle_ps(SseVal(),SseVal(),_MM_SHUFFLE(1,1,1,1)));}Scalary()constnoexcept{return_mm_cvtss_f32(_mm_shuffle_ps(SseVal(),SseVal(),_MM_SHUFFLE(2,2,2,2)));}Scalarz()constnoexcept{return_mm_cvtss_f32(_mm_shuffle_ps(SseVal(),SseVal(),_MM_SHUFFLE(3,3,3,3)));}__m128SseVal()constnoexcept{returnvalue_;}};__m128表示 SSE 寄存器,存储四个 float。- 使用
_mm_setr_ps加载四个浮点数。 - 通过
_mm_shuffle_ps提取寄存器中对应的分量。 - 访问器保证对 SSE 寄存器的安全读取。
2.4 SSE 平台的标量运算(点积)
template<plt::simd::Sse SIMD>inlineautoDot(Quat<float,SIMD>lhs,Quat<float,SIMD>rhs)->float{__m128 squares=_mm_mul_ps(lhs.SseVal(),rhs.SseVal());// 分量乘法__m128 badc=_mm_shuffle_ps(squares,squares,_MM_SHUFFLE(2,3,0,1));// 调整顺序__m128 pairs=_mm_add_ps(squares,badc);// 前两两相加__m128 bbaa=_mm_shuffle_ps(pairs,pairs,_MM_SHUFFLE(0,1,2,3));__m128 dp=_mm_add_ps(pairs,bbaa);// 汇总到单一向量floatresult=_mm_cvtss_f32(dp);// 提取结果returnresult;}- SSE 内置函数实现 4 分量的快速点积。
- 所有运算在寄存器中完成,避免数据搬运到通用寄存器。
3⃣ 多平台支持与独立优化
- 现在支持两种平台:Neon32 和 SSE。
- 优化是完全独立的:
- Neon32 使用 ARM SIMD 寄存器。
- SSE 使用 x86 SSE 寄存器。
- 没有使用任何
#if或#ifdef条件编译。 - 每个平台只包含该平台特定的头文件和内建函数。
- 通用表达式树算法无需修改即可使用平台优化实现。
4⃣ 处理特性修订(Feature Revisions)
- SSE2 支持 128-bit 寄存器,同时可存储 2 个 double。
- 对 SSE2 平台可新增以下类型:
Quat<float, Sse2>Quat<double, Sse2>
- 特化步骤与 SSE 相似,模板类和函数可复用,只需修改类型和寄存器指令。
5⃣ 总结
- 四元数类实现遵循 OCP 原则:
- 添加新平台(Neon32、SSE、SSE2)无需修改已有平台代码。
- 现有算法和表达式树通用实现不受影响。
- 平台特化与通用实现解耦:
- 通用实现负责算法逻辑。
- 平台特化负责寄存器布局和 SIMD 优化。
- 跨平台架构的关键点:
- 模板 + 概念(Concepts)定义接口。
- 特化类实现平台特定优化。
- 延迟求值(Expression Tree)保证组合性和性能。
1⃣ SSE2 特性与类实现
1.1 SSE2 平台头文件
#defineSIMD_HAS_SSE2#include"Sse.h"namespaceplt::simd{structSse2:Sse{};template<typenameSIMD>conceptSse2Family=SseFamily<SIMD>&&std::derived_from<SIMD,Sse2>;}- SSE2 继承自 SSE(
struct Sse2 : Sse {})。 Sse2Family概念约束模板参数,确保其为 SSE 家族成员且派生自 Sse2。- 模板约束保证了重载解析优先选择更特化的平台实现。
1.2 Quat<float, Sse2> 特化
template<>classQuat<float,plt::simd::Sse2>:publicQuat<float,plt::simd::Sse>{usingQuat<float,plt::simd::Sse>::Quat;};- 直接继承 SSE 特化,实现复用。
- 构造函数、访问器等直接从 SSE 特化继承。
1.3 Quat<double, Sse2> 特化
template<>classQuat<double,plt::simd::Sse2>{__m128d wx_;// w, x__m128d yz_;// y, zpublic:usingScalar=double;Quat()=default;Quat(Scalar w,Scalar x,Scalar y,Scalar z){wx_=_mm_set_pd(x,w);yz_=_mm_set_pd(z,y);}template<Quaternion Q>Quat(constQ&rhs):Quat(static_cast<Scalar>(rhs.w()),static_cast<Scalar>(rhs.x()),static_cast<Scalar>(rhs.y()),static_cast<Scalar>(rhs.z())){}Quat(__m128d wx,__m128d yz):wx_(wx),yz_(yz){}Scalarw()const{return_mm_cvtsd_f64(SseWx());}Scalarx()const{return_mm_cvtsd_f64(_mm_unpackhi_pd(SseWx(),SseWx()));}Scalary()const{return_mm_cvtsd_f64(SseYz());}Scalarz()const{return_mm_cvtsd_f64(_mm_unpackhi_pd(SseYz(),SseYz()));}__m128dSseWx()const{returnwx_;}__m128dSseYz()const{returnyz_;}};- 利用 SSE2 128-bit 寄存器存储 double 类型的 w,x 和 y,z。
_mm_set_pd设置寄存器值。_mm_unpackhi_pd提取寄存器高位 double 值。
1.4 SSE2 平台点积函数
template<plt::simd::Sse2 SIMD>inlineautoDot(Quat<double,SIMD>lhs,Quat<double,SIMD>rhs)->double{__m128d w2x2=_mm_mul_pd(lhs.SseWx(),rhs.SseWx());__m128d x2w2=_mm_shuffle_pd(w2x2,w2x2,_MM_SHUFFLE2(0,1));__m128d wx2wx2=_mm_add_pd(w2x2,x2w2);__m128d y2z2=_mm_mul_pd(lhs.SseYz(),rhs.SseYz());__m128d z2y2=_mm_shuffle_pd(y2z2,y2z2,_MM_SHUFFLE2(0,1));__m128d yz2yz2=_mm_add_pd(y2z2,z2y2);__m128d dp=_mm_add_pd(wx2wx2,yz2yz2);doubleresult=_mm_cvtsd_f64(dp);returnresult;}- SSE2 支持 double 的并行运算。
- 点积通过寄存器操作
_mm_mul_pd,_mm_shuffle_pd,_mm_add_pd高效计算。
2⃣ SSE3 平台扩展
- SSE3 继承 SSE2,不需要新寄存器。
- float 和 double 特化类可复用 SSE2 特化。
- 利用
_mm_hadd_ps和_mm_hadd_pd实现更高效水平加法。
2.1 float 点积 SSE3
template<plt::simd::Sse3 SIMD>inlineautoDot(Quat<float,SIMD>lhs,Quat<float,SIMD>rhs)->float{__m128 squares=_mm_mul_ps(lhs.SseVal(),rhs.SseVal());// 分量乘法__m128 add1st=_mm_hadd_ps(squares,squares);// 两两相加__m128 add2nd=_mm_hadd_ps(add1st,add1st);// 全部求和floatresult=_mm_cvtss_f32(add2nd);returnresult;}2.2 double 点积 SSE3
template<plt::simd::Sse3 SIMD>inlineautoDot(Quat<double,SIMD>lhs,Quat<double,SIMD>rhs)->double{__m128d w2x2=_mm_mul_pd(lhs.SseWx(),rhs.SseWx());__m128d y2z2=_mm_mul_pd(lhs.SseYz(),rhs.SseYz());__m128d add1=_mm_hadd_pd(w2x2,y2z2);__m128d add2=_mm_hadd_pd(add1,add1);doubleresult=_mm_cvtsd_f64(add2);returnresult;}_mm_hadd_ps和_mm_hadd_pd是 SSE3 新增指令,实现水平加法优化。
3⃣ 函数重载解析与概念(Concepts)
3.1 重载解析(Overload Resolution)
- 编译器根据函数候选列表选择最佳匹配。
- 若无最佳匹配,报错为“ambiguous overload”。
3.2 概念在重载解析中的作用
- 概念不仅约束模板参数有效性(类似
enable_if),还参与最佳匹配的选择。 - 对于类模板:
- 派生类的模板优先于基类模板。
- 概念组合:
- 概念可以在定义中组合其他概念。
- 概念本身不能继承,但可通过“subsumption”确定优先级。
- Subsumption:一个概念比另一个概念更“特化”,在重载解析中优先选择。
4⃣ 总结
- SSE2 与 SSE3 特化继承关系:
- SSE2 继承 SSE,SSE3 继承 SSE2。
- 更特化的平台优先匹配,保证性能优化生效。
- 浮点类型与寄存器布局:
- float:
__m128 - double:
__m128d,分成 wx_ 和 yz_ 两个寄存器存储。
- float:
- 通用算法与平台特化解耦:
- 表达式树和通用算法无需修改即可支持新平台。
- 平台优化通过模板特化实现。
- C++20 Concepts 与重载解析:
- 概念用于模板约束。
- 更特化的概念或派生类优先匹配,保证正确调用平台特化实现。
1⃣ Subsumption(概念覆盖)在代码中的作用
1.1 概念定义与覆盖关系
structSse:Common{};structSse2:Sse{};template<typenameSIMD>conceptSseFamily=std::derived_from<SIMD,Sse>;template<typenameSIMD>conceptSse2Family=SseFamily<SIMD>&&std::derived_from<SIMD,Sse2>;template<typenameSIMD>conceptSse2Derived=std::derived_from<SIMD,Sse2>;- Sse2Family是 SseFamily 的子集:
- 当
Sse2Family<Derived>为 true 时,SseFamily<Derived>也为 true。 - 但 Sse2Derived 并不覆盖 SseFamily,因此不能参与重载优先匹配。
- 当
- 概念覆盖(subsumption)的作用:
- 概念 A 覆盖概念 B → 对 A 的约束更特化,优先匹配。
- 这是 C++20 模板重载选择的关键机制。
1.2 没有概念覆盖的函数重载问题
template<typenameSIMD>conceptSse3Family=std::derived_from<SIMD,Sse3>;template<plt::simd::SseFamily SIMD>inlineautooperator*(Quat<float,SIMD>lhs,Quat<float,SIMD>rhs)->Quat<float,SIMD>;template<plt::simd::Sse3Family SIMD>inlineautooperator*(Quat<float,SIMD>lhs,Quat<float,SIMD>rhs)->Quat<float,SIMD>;- 问题:Sse3Family 仅是一个单独标签概念,没有与 Sse2Family 或 SseFamily 的覆盖关系。
- 结果:
operator*调用会产生ambiguous overload(重载二义性)错误。 - 原因:Sse3Family 的参数类型严格是 Sse3,SseFamily 可接受更多类型,但两者没有覆盖关系,编译器无法确定哪一个函数优先。
1.3 有概念覆盖的正确实现
template<typenameSIMD>conceptSse2Family=SseFamily<SIMD>&&std::derived_from<SIMD,Sse2>;template<typenameSIMD>conceptSse3Family=Sse2Family<SIMD>&&std::derived_from<SIMD,Sse3>;- 特点:
- Sse2Family 覆盖 SseFamily
- Sse3Family 覆盖 Sse2Family
- 覆盖关系是可传递的(transitive)
- 效果:
- 编译器会优先选择最特化的概念对应的函数(Sse3Family)。
- 避免了重载二义性。
2⃣ 平台特化与继承策略
2.1 SSE / SSE2 / SSE3
- 数据类和函数在 SSE 上实现。
- SSE2 继承 SSE 优化函数,无需额外代码。
- SSE3 仅实现少量更优化函数,其余继承 SSE2。
- 结果:
- 新版本函数不会影响旧平台。
- 保持 OCP(开放-关闭原则)强约束:无需修改旧代码,也不需要重新编译旧平台。
2.2 AVX 平台
- AVX 引入 256-bit 寄存器:
- Quat 无法直接使用宽寄存器,仍继承前代实现。
- Quat 可以完整放入单寄存器,需新数据类型。
- 问题:
- Quat<double, Avx> 若与 SSE4 共用 math 头文件,会出现寄存器不兼容错误。
- 初始头文件拆分(Quat.h, Quat_Sse.h)还不够,需要进一步分离函数实现。
2.3 分离头文件策略
- 类定义与函数实现分离:
- Quat.h / Quat_Sse.h / Quat_Avx.h → 数据类
- QuatMath.h / QuatMath_Sse.h / QuatMath_Avx.h → 函数实现
- AVX 示例:
QuatFloatMath_Avx.h包含QuatFloatMath_Sse4.h,继承 float 操作QuatDoubleMath_Avx.h不包含旧头文件,避免 double 操作不兼容
- 好处:
- 避免不同平台 math 实现冲突
- 保持旧平台编译结果不变 → 近乎“强 OCP”
3⃣ 总结要点
- 概念覆盖(Subsumption):
- Sse3Family → Sse2Family → SseFamily
- 确保模板重载正确选择最特化版本。
- 平台隔离:
- 每个平台的数据类型与函数实现独立。
- 新平台不会影响旧平台构建。
- 函数复用:
- SSE / SSE2 / SSE3 / AVX 复用已有优化函数,减少冗余。
- OCP 实现:
- 新特性添加不修改旧代码。
- 分离数据类和函数实现,保证不同平台独立优化。
- C++20 Concepts 的优势:
- 提供了类型约束和重载优先级控制。
- 通过 subsumption 和派生关系,可实现跨平台 SIMD 重载无二义性。
代码太多看不懂
https://github.com/noahstein/Ark