DoD(Data-Oriented Design,数据导向设计)这一概念,并给出理解

1. 基本定义

DoD,即 数据导向设计,是一种程序设计范式,其核心关注点不是代码的对象抽象或类的封装,而是 数据本身及其在程序中的变换
根据维基百科的定义:

“As a design paradigm, data-oriented-design focuses on optimal transformations of data and focuses on modelling programs as transforms.”
成就是:
作为一种设计范式,数据导向设计关注 数据的最优变换,并将程序建模为 对数据的转换
这里有几个关键词需要理解:

  1. 最优变换(optimal transformations of data)
    DoD 的关注点是如何高效地操作数据,而不是如何组织类或对象。例如,你可能会考虑如何一次性处理一批数据(批量操作),而不是针对单个对象频繁调用方法。
  2. 程序建模为数据转换(programs as transforms)
    在 DoD 中,程序可以被看作是一系列从输入数据到输出数据的函数或变换:
    f:Input Data→Output Data f: \text{Input Data} \rightarrow \text{Output Data} f:Input DataOutput Data
    举例来说,如果有一组实体的位置和速度,程序可以被建模为一个变换函数:
    update_positions(positions,velocities,Δt)→new positions \text{update\_positions}(\text{positions}, \text{velocities}, \Delta t) \rightarrow \text{new positions} update_positions(positions,velocities,Δt)new positions
    这里关注的不是实体对象的封装或方法,而是 数据本身及其转换逻辑

2. 与传统 OOP 的区别

  • OOP(面向对象编程)
    关注对象和行为:类是中心,方法在对象上操作数据。
    核心思路:封装、继承、多态
  • DoD(数据导向设计)
    关注数据和变换:数据是中心,程序是对数据的函数式处理。
    核心思路:优化数据流、减少缓存错失、批量处理数据

注意:很多人误以为 DoD 只是为了优化缓存行或者结构体布局,但这只是 DoD 的一种实际优化手段,并非核心理念。

3. 数学化表述

可以把 DoD 的核心思想形式化为函数式变换。设数据集合为 DDD,程序中的操作为一系列变换 TiT_iTi,则整个程序可以表示为:
Dout=Tn∘Tn−1∘⋯∘T1(Din) D_{\text{out}} = T_n \circ T_{n-1} \circ \dots \circ T_1 (D_{\text{in}}) Dout=TnTn1T1(Din)
其中:

  • DinD_{\text{in}}Din:输入数据集合
  • DoutD_{\text{out}}Dout:输出数据集合
  • TiT_iTi:单个数据变换函数
  • ∘\circ:函数组合(composition),表示将前一个变换的输出作为下一个变换的输入
    这种方式让程序可以被清晰地分析和优化,例如:
  • 批量处理可以减少内存访问次数
  • 数据排列可以提高缓存命中率
  • 并行处理可以天然映射到 SIMD 或 GPU 上

4. 直观理解举例

假设我们有一个游戏中的实体(Entity),每个实体有位置 xxx 和速度 vvv。在 OOP 中,你可能写成:

for (auto& entity : entities) {
    entity.position += entity.velocity * dt;
}

在 DoD 中,你可能把数据拆成数组:
positions=[x1,x2,x3,…,xn] \text{positions} = [x_1, x_2, x_3, \dots, x_n] positions=[x1,x2,x3,,xn]
velocities=[v1,v2,v3,…,vn] \text{velocities} = [v_1, v_2, v_3, \dots, v_n] velocities=[v1,v2,v3,,vn]
然后统一做批量更新:
positions[i]=positions[i]+velocities[i]⋅Δt \text{positions}[i] = \text{positions}[i] + \text{velocities}[i] \cdot \Delta t positions[i]=positions[i]+velocities[i]Δt
这样做的好处:

  1. 数据连续存储,提升缓存效率
  2. 容易向 SIMD/GPU 映射,做向量化计算
  3. 逻辑简单清晰:函数只关注数据转换

5. 总结

  • 核心思想:数据是中心,程序是数据的变换
  • 核心目标:优化数据访问、提高变换效率
  • 区别于 OOP:不强调对象封装和继承,而强调数据流和变换
  • 数学抽象:程序可被视作函数组合
    Dout=Tn∘⋯∘T1(Din) D_{\text{out}} = T_n \circ \dots \circ T_1 (D_{\text{in}}) Dout=TnT1(Din)

1. DoD 的极简定义

DoD 的核心可以极简地表示为一个公式:
Data∗output=F(Data∗input) \text{Data}*{\text{output}} = F(\text{Data}*{\text{input}}) Dataoutput=F(Datainput)
意思是:程序可以被抽象为 对数据的变换,其中:

  • Datainput\text{Data}_{\text{input}}Datainput:输入数据
  • Dataoutput\text{Data}_{\text{output}}Dataoutput:输出数据
  • FFF:对数据的具体变换(Transformation)
    用图示理解:
Previous transformation → Input Data → Specific Transformation → Output Data → Next transformation

这里强调 每个程序块都是对数据的特定变换,而不是对对象或类的操作。

2. DoD 在实际代码中的使用方式

实际中,DoD 的实现往往具有以下特征:

  • 平台相关(Platform specific code)
  • 过程式/命令式(Procedural/imperative code)
  • 问题/数据集特定(Problem/dataset specific code)
  • 手动优化缓存行和结构体布局(Hand-optimize cache lines and struct layout)
    换句话说,DoD 的实现往往与硬件和特定问题高度耦合。

3. 改进思路(第一轮尝试)

  • 可以尝试用 AI 工具(如 ChatGPT)帮助优化
  • 但是仅靠工具无法解决根本问题
  • 核心仍然是:
    Data∗output=F(Data∗input) \text{Data}*{\text{output}} = F(\text{Data}*{\text{input}}) Dataoutput=F(Datainput)
  • 核心关注应放在 数据本身

4. 相反哲学(Functional Programming 思路)

与传统 DoD 对应的“相反哲学”是:

数据导向代码 (DoD) 相反哲学(函数式/声明式)
平台相关 跨平台(Cross platform)
过程式/命令式 声明式(Declarative)
数据集/问题特定 泛型代码(Generic code)
手动优化缓存行和结构 由编译器处理缓存/结构布局
这与 函数式编程(FP) 的思想非常接近:
  • 程序通过 函数链(function chaining) 构建
  • 可以把多个变换 F,G,HF, G, HF,G,H 链接起来:
    Data∗output=H(G(F(Data∗input))) \text{Data}*{\text{output}} = H(G(F(\text{Data}*{\text{input}}))) Dataoutput=H(G(F(Datainput)))
  • 核心仍是数据变换,只是更抽象、更泛型、更跨平台

5. DoD 与函数式编程的关系

  • DoD 和 FP 可能在实现上有强差异(如性能优化、缓存利用)
  • 但两者在 程序建模为数据变换 的理念上有强相似性
  • 相比 OOP(面向对象编程),DoD 与 FP 的相似度更高
    数学上可以用组合函数表示整个程序:
    Data∗out=Tn∘T∗n−1∘⋯∘T1(Datain) \text{Data}*{\text{out}} = T_n \circ T*{n-1} \circ \dots \circ T_1(\text{Data}_{\text{in}}) Dataout=TnTn1T1(Datain)
  • TiT_iTi:单个数据变换函数
  • ∘\circ:函数组合(composition)

6. C++ 中可用的函数式编程特性

在 C++ 中可以应用 FP 思想:
运行时(runtime):

  • Lambda 表达式
  • STL 算法(sort, transform 等)
  • Ranges
  • 函数指针
    编译时(compile-time):
  • 类型作为数据(Metaprogramming)
  • 模板和 constexpr 函数

7. 引入“变换导向设计(Transformation-Oriented Design, TOD)”

概念定义:

  • TOD = 编译器级别的函数式编程
  • 核心关注点仍然是 变换(Transformation),即函数
  • 可以理解为 DoD + FP 的进化版:强调 编译器优化 + 数据变换抽象
    公式上依然是:
    Data∗output=F(Data∗input) \text{Data}*{\text{output}} = F(\text{Data}*{\text{input}}) Dataoutput=F(Datainput)
    只是 FFF 可以在 编译时展开、组合和优化,实现更高效的程序。

8. 小结

  1. DoD 核心:程序是数据变换
    Data∗out=F(Data∗in) \text{Data}*{\text{out}} = F(\text{Data}*{\text{in}}) Dataout=F(Datain)
  2. 实际实现:平台/问题特定、命令式、手动优化缓存
  3. 相反哲学:函数式/声明式、泛型、由编译器处理底层优化
  4. 函数式编程:通过函数链构建程序
    Data∗out=H(G(F(Data∗in))) \text{Data}*{\text{out}} = H(G(F(\text{Data}*{\text{in}}))) Dataout=H(G(F(Datain)))
  5. TOD:编译器级别的函数式 + 数据导向设计,强调 变换的抽象和优化

1. 传统 OOP 方法

设计特点:

  • 定义一个 基类 Image
    • 含有虚函数 ReadFile()WriteFile()
    • 保存图像数据
  • 定义派生类(Derived classes)如 PNGFile, GIFFile, JPEGFile
    • 特化加载方法和识别方法
    • 可保存文件特定数据
      示意:
Image File
  virtual void ReadFile()
  virtual void WriteFile()
  - Image data
PNG File
  void ReadFile()
  void WriteFile()
GIF File
  void ReadFile()
  void WriteFile()
JPEG File
  void ReadFile()
  void WriteFile()

OOP 的缺点:

  1. 虚函数和 vtable 调用不可避免(运行时开销)
  2. 灵活性低,设计被锁定(数据和函数绑定)
  3. 扩展新类型需要修改原有类层次结构

2. TOD(变换导向设计)方法

核心思想:

  • 对象只包含 函数(transformations)
  • 数据和函数 不天然绑定
  • 无基类
  • 类型列表(typelist) 替代继承结构
    示意:
FilePNG
  const Image& ReadFile()
  void WriteFile(const Image&)
FileGIF
  const Image& ReadFile()
  void WriteFile(const Image&)
FileJPEG
  const Image& ReadFile()
  void WriteFile(const Image&)
using ImageFormatDriver_t = typelist<FilePNG, FileGIF, FileJPEG>;

特点:

  • 每个“变换包(transform package)”是独立的类型
  • 不需要虚函数
  • 编译期可组合、检查、优化

3. TOD 与 OOP 对比表


概念 OOP TOD
统一原则 Base class Type list of transform packages
派生类 Derived classes Specific classes
数据是否绑定 否(大多数情况)
虚函数 必需
签名方法(Signature method) 严格 自由

4. 变换包(Transform Package)定义

变换包的内容:

  • 静态函数/变换(Static transformations)
  • 类型定义(using definitions)
  • 枚举(Enums)
  • 静态数据(Static data)
    性质:
  • 仅在编译期可操作
  • 不可实例化或销毁
  • 可自由选择是否使用
    公式化表述 TOD 核心:
    Output Data=F(Input Data) \text{Output Data} = F(\text{Input Data}) Output Data=F(Input Data)
    其中 FFF静态变换函数,在编译期可组合成整个处理流程。

5. TOD 设计影响

  1. 抽象化:从具体变换解耦
  2. 最小化文档:每个变换包独立清晰
  3. 模块化代码:方便重用
    代码结构:
Library
  - Transformation definitions
  - Utility functions
User-side code
  - Package implementation files
  - Specific transformations used
  - Execution path definition

6. TOD 编程要点

  1. 编译期函数组合 替代 OOP 的虚函数调度
  2. 替换硬编码的 switch/case 分支
  3. 只调用存在的函数
  4. 尽量 不依赖反射
    公式表示:
    Program=Tn∘Tn−1∘⋯∘T1(Datainput) \text{Program} = T_n \circ T_{n-1} \circ \dots \circ T_1(\text{Data}_{\text{input}}) Program=TnTn1T1(Datainput)
  • TiT_iTi:单个变换函数
  • ∘\circ:函数组合(Compile-time composition)

7. 总结

  • OOP:数据 + 方法绑定,虚函数调度,运行时灵活但性能受限
  • TOD:函数式/变换导向,数据与函数解耦,编译期组合,性能可优化
  • TOD 的核心
    1. 静态变换包(Transform packages)
    2. 编译期函数式编程
    3. 利用现代 C++ 特性(模板、constexpr、类型列表等)

TOD 示例代码及相关内容做详细解析,并结合数学公式或形式化表达帮助理解。

1. 示例 1:类型列表与类型检查

代码片段使用了 编译期类型列表(TypeList),用来判断某个类型是否存在于类型列表中。

namespace Local { // 局部命名空间,防止与外部命名冲突,常用于模板元编程
  // 递归模板函数,用于在编译期判断 Type 是否存在于类型列表 TList 中
  // Index: 当前检查的索引位置
  // Type: 需要检查的类型
  // TList: 类型列表
  template<size_t Index, typename Type, typename TList>
  constexpr bool TypeInListImpl() {
      // 如果当前索引对应的类型与 Type 相同,返回 true
      if constexpr (std::is_same_v<Type, TypeAt_t<Index, TList>>) 
          return true;
      // 如果已经检查到列表开头还没找到,返回 false
      else if constexpr (Index == 0) 
          return false;
      // 否则递归检查前一个索引
      else 
          return Local::TypeInListImpl<Index - 1, Type, TList>();
  }
} // namespace Local
// 对外接口函数:判断 Type 是否在 TList 中
// Type: 需要检查的类型
// TList: 类型列表
template<typename Type, typename TList> 
constexpr bool TypeInList() {
    // 调用内部实现,从类型列表最后一个索引开始检查
    // Length_v<TList> 是 TList 的长度
    return Local::TypeInListImpl<Length_v<TList> - 1, Type, TList>();
}

理解:

  • TypeList 是一个编译期类型集合
  • TypeInList<Type, TList>() 判断 Type 是否在 TList
  • 利用 递归模板和 if constexpr 在编译期实现
  • 形式化表述:
    TypeInList(T,L)={true,if T=L[i] for some ifalse,otherwise \text{TypeInList}(T, L) = \begin{cases} \text{true}, & \text{if } T = L[i] \text{ for some i} \\ \text{false}, & \text{otherwise} \end{cases} TypeInList(T,L)={true,false,if T=L[i] for some iotherwise

2. 示例 2:静态函数调用(Transform Call)

定义变换调用宏:

// 定义一个宏,用于生成 "TransformCall" 模板函数
// Function: 想要调用的函数名,例如 Mess、ReadFile 等
#define TRANSFORM_CALL(Function) \
\
template< typename Type,      /* 当前要调用函数的类型 */ \
          typename TList,     /* 类型列表(TypeList) */ \
          typename TReturn,   /* 函数返回值类型 */ \
          typename... Args>   /* 可变模板参数,表示函数参数类型 */ \
TReturn TransformCall##Function(Args&& ...args) { \
\
    // 获取类型列表的第一个类型,作为默认类型
    using Default = TypeAt_t<0, TList>; \
\
    // 如果 Type 不在类型列表中,则调用默认类型的函数
    // TypeInList<Type, TList>() 在编译期判断类型是否存在于类型列表
    if constexpr (!TypeInList<Type, TList>()) \
        return Default::Function(args...); \
\
    // 如果 Type 存在,并且 Type 具有该函数,则调用 Type 的函数
    if constexpr (requires() { Type::Function; }) \
        return Type::Function(args...); \
\
    // 否则调用默认类型的函数(Type 存在但没有该函数)
    else \
        return Default::Function(args...); \
}

理解:

  1. TransformCallFunction 会在 编译期决定调用哪个类型的函数
  2. 如果 Type 不在类型列表中 → 调用默认类型(通常是第一个类型)
  3. 如果 Type 存在但没有该函数 → 调用默认类型的函数
  4. 如果 Type 存在并有该函数 → 调用 Type 的函数
  • 形式化表示:
    TransformCallFunction(T,L)={Default::Function(args...),T∉LT::Function(args...),T∈L 且存在该函数Default::Function(args...),otherwise \text{TransformCallFunction}(T, L) = \begin{cases} \text{Default::Function(args...)}, & T \notin L \\ T::Function(args...), & T \in L \text{ 且存在该函数} \\ \text{Default::Function(args...)}, & \text{otherwise} \end{cases} TransformCallFunction(T,L)= Default::Function(args...),T::Function(args...),Default::Function(args...),T/LTL 且存在该函数otherwise
    调用示例:
TransformCallMess<A, TransformObjectList, void>();  // 输出 "...default"
TransformCallMess<B, TransformObjectList, void>(1); // 输出 "...b"
TransformCallMess<C, TransformObjectList, void>();  // 输出 "...default"
  • A 类在类型列表中,但没有 Mess 函数 → 调用默认
  • B 类有 Mess(int) → 调用 B 的实现
  • C 类不在类型列表中 → 调用默认

3. 示例 2:类型列表与签名查找

// 定义一个类型列表 ImageFormat,包含常用图像格式
using ImageFormat = TypeList<BMP, JPEG, PNG, TIFF>;
namespace Local {
  // 递归模板函数,用于在编译期查找某个类型在类型列表中的索引位置
  // Index: 当前检查的索引
  // TList: 类型列表
  template<size_t Index, typename TList>
  size_t SignatureImpl() {
      // 如果类型列表当前索引对应的类型存在(非空或有效)
      if (TypeAt_t<Index, TList>) 
          return Index;  // 返回该索引作为签名值
      // 如果已经检查到列表开头(Index == 0)还没有找到
      if constexpr (Index == 0) 
          return -1;    // 返回 -1 表示类型未找到
      // 否则递归检查前一个索引
      else 
          return Local::SignatureImpl<Index - 1, TList>();
  }
}
// 对外接口函数,用于获取类型 Type 在类型列表 TList 中的索引位置
// Type: 需要查找的类型
// TList: 类型列表
template<typename Type, typename TList> 
constexpr size_t Signature() {
    // 从类型列表最后一个索引开始查找
    // Length_v<TList> 表示类型列表长度
    return Local::SignatureImpl<Length_v<TList> - 1, Type, TList>();
}

理解:

  • 编译期查找某个类型在 TypeList 中的位置
  • 若类型不存在 → 返回 -1
  • 可用于 编译期选择合适的变换或函数实现
  • 形式化表述:
    Signature(T,L)={i,if T=L[i]−1,if T∉L \text{Signature}(T, L) = \begin{cases} i, & \text{if } T = L[i] \\ -1, & \text{if } T \notin L \end{cases} Signature(T,L)={i,1,if T=L[i]if T/L

4. TOD 的关键思想总结

  1. 类型列表(TypeList) 替代虚函数和 switch/case
  2. 编译期选择函数,无需运行时虚表
  3. 函数调用安全:仅调用存在的函数,缺省调用默认实现
  4. 函数和数据解耦:对象本身无需持有数据
  5. 优化在编译期完成:减少运行时开销

5. 未来发展方向

  • 利用 C++20/23 特性:
    • tag_invoke
    • metaclasses
    • static reflection
    • 数据作为类型(Data as types)
  • 或通过 std::type_sequence 等标准工具实现类似效果

6. 核心公式总结

  • 数据变换公式:
    Data∗out=F(Data∗in) \text{Data}*{\text{out}} = F(\text{Data}*{\text{in}}) Dataout=F(Datain)
  • TransformCall 函数逻辑:
    TransformCallFunction(T,L)={Default::Function(args...),T∉LT::Function(args...),T∈L 且存在函数Default::Function(args...),otherwise \text{TransformCallFunction}(T, L) = \begin{cases} \text{Default::Function(args...)}, & T \notin L \\ T::Function(args...), & T \in L \text{ 且存在函数} \\ \text{Default::Function(args...)}, & \text{otherwise} \end{cases} TransformCallFunction(T,L)= Default::Function(args...),T::Function(args...),Default::Function(args...),T/LTL 且存在函数otherwise
  • 类型签名查找公式:
    Signature(T,L)={i,T=L[i]−1,T∉L \text{Signature}(T, L) = \begin{cases} i, & T = L[i] \\ -1, & T \notin L \end{cases} Signature(T,L)={i,1,T=L[i]T/L

7. 总结要点

  • TOD 与传统 DoD 或 OOP 不同:核心是 编译期数据与函数变换组合
  • 无需运行时虚函数或 switch/case
  • 模板和类型列表 是主要工具
  • 可利用现代 C++ 功能实现 高性能、类型安全、可扩展的变换调用
  • TOD 可以与其他范式共存,不必全模板化
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐