DOTS(1) 面向数据设计DOD

  1. 1. 程序设计方法
  2. 2. OOD -> DOD
  3. 3. Cache的3C与3R
  4. 4. 面向数据设计需要思考的问题
  5. 5. DOTS面向数据设计原则

DOTS是面向数据的技术栈
DO的缩写正是DOTS是以面向数据为基础的一个技术解决方案
所以充分了解面向数据设计DOD是学习DOTS的基础

程序设计方法

  • 这些程序设计方法都有很明显的时代特征,与程序语言的特性
  • Instructional Programming
    • 指令化编程
    • 正是计算机脱离纸带打孔输入后,伴随着机器汇编语言发展起来的
  • Functional Programming
    • 函数化编程
    • 伴随着Pascal语言出现的,以小函数模块化组合的编程范式
    • 由于其限制小、易于调试等特点,主要用在数学和科学计算领域
    • 如今如火如荼的机器学习、AI领域,函数化编程依然焕发着活力
  • Procedural Programming
    • 过程化编程
    • 是一种源于命令式的编程范式,基于过程调用的概念,包含一些要执行的步骤,任何给定的过程都可以在程序执行过程中的任何时刻调用
    • 过程化编程伴随着一些更高级的编程语言,如Fortran、ALGOL、COBOL、BASIC等而出现
  • Object-Oriented Programming/Design
    • 面向对象编程/设计(OOP/OOD)
    • 随着程序化规模越来越大,传统的编程范式已不足以满足易于理解、易于设计的需要了,这时,面向对象设计和编程的方式开始出现了
    • 它是以对象为概念的多范式模型,包含字段形式的数据与过程形式的代码,通常以类为基础,强调数据的封装、类的继承与对象的多态特征,程序开始有了更高级的设计的概念
    • 随之是一系列相关设计模式,诸如C++、Java、Python等广泛使用的面向对象的多范式编程语言的出现
  • Data-Oriented Design(DOD)
    • 面向数据设计
    • 是伴随着现代CPU多核并行计算、多级缓存、大缓存的设计而流行起来的
    • 这里的DOD并不是面向数据编程,它并不是一种编程范式,而DOTS更可以理解为面向数据编程的一种范式

OOD -> DOD

  • 我们需要有从OOD面向对象设计到DOD面向数据设计的思想转变
  • 面向对象设计的核心在于抽象、封装和继承,这样的设计对人类来说可能更直观、易于理解,但对于现代CPU来说,它的处理效率并不高效
  • 而面向数据设计则侧重于数据,开发人员需要考虑需要什么数据,以及如何在内存中更好的构造数据,以便CPU处理数据系统时能够更有效地访问数据
  • DOD面向数据设计的本质,可以理解为是面向内存或缓存友好的设计
  • 这要从CPU架构与缓存层级结构说起
    现代CPU架构下缓存层级结构简图1
    现代CPU架构下缓存层级结构简图2
  • CPU中会设置L1、L2、L3,3级缓存
  • 其中一级缓存为每个指令处理单元独享,又可分为缓存数据的L1 D数据缓存与缓存指令用的L1 I指令缓存
  • L2级缓存则为CPU核内多个指令处理单元共享
  • L3级缓存则为CPU多个核共享,同时它还负责与内存以及显卡中的显存交换数据
  • CPU在执行程序指令时,会通过prefetching来获取指令与数据,每次访问的单位会根据系统与架构的不同而有所差异,一般是32或64个字节,把这个基础大小单位称为catch line缓存行。即使你请求一个字节大小,实际上你会得到一个catch line大小的缓存行数据
  • 而在catch缓存内,可以将n个缓存行大小的缓存通过direct map直接映射到同一逻辑缓存行,而逻辑缓存行可以对应n个物理行,来帮助最小化缓存行的抖动。这里的抖动可以理解为 : 扭动指针到每个物理缓存行头
  • CPU逻辑处理单元,通过Fetch获得L1 I指令缓存中的指令,再通过Decode解码Execution执行,以及在指令完成后将数据回写到L1 D中,来完成一条程序指令
  • 我们可以把Fetch、Decode、Execution这样一个循环时间定义为一个CPU指令的Cycle
    缓存层级结构金字塔图
  • CPU处理指令时,从不同的缓存Catch中获取数据的时间开销也是不同的,这会导致我们获取的数据在某一级缓存没有命中时,向下一级缓存获取时花费的时间开销可能是数倍时间,甚至是数量级差异的时间开销,因此我们在编写程序时,如何做好面向数据设计,以达到缓存访问友好,对程序性能开销至关重要

Cache的3C与3R

  • 3C即缓存未命中的三种情况

  • Compulsory misses : 首次读取数据时,不可避免的Miss

  • Capacity misses : 缓存空间不足时,连续使用期间访问数据过多的话,无法保存所有活动的数据

  • Conflict misses : 发生访问冲突时,由于数据映射到相同的缓存行,导致缓存的抖动

  • 3C伪代码 :

    1
    2
    3
    4
    5
    6
    int* data = pointerToSomeData;
    unsigned int sum = 0;
    for (unsigned int i = 0; i < 1000000; ++i, ++data /*data+=16*/)
    {
    sum += *data;
    }
    • 当变量i = 0时,第一次进入循环,此时要访问data中的第一个int数据,这时catch中没有数据,会发生第一种情况下的Miss catch,这时需要进行profetching来加载一个catch line数据
    • 接下来的第二遍循环会从catch line中加载数据,指令cycle数比第一次要少
    • 当data数据足够大、循环次数足够多时(pointerToSomeData、1000000),超过catch大小时会发生第二种情况,缓存不足导致的Miss catch
    • 这里只是举例,真实情况不一定发生,而且现在GPU缓存很大,简单的代码触发缓存不足导致的Miss catch情况非常少
    • 将 ++data 替换成 data+=16 时,这时会触发第三种缓存抖动带来的Miss
      catch,也就是实际数据访问不连续,一次catch line获取的数据并没有你需要的,这种情况发生比较多,尤其是我们使用数组结构体AoS的数据Layout时
  • 3R即3种优化访问catch命中的方法

  • Rearrange 重新排列(代码、数据) : 更改布局以增加数据空间的局部性

  • Reduce 减少(大小、缓存行读取) : 更小更智能的格式、压缩,如修改数据类型或使用位计算

  • Reuse 重用(Cache lines) : 增加数据的时间(和空间)的局部性,主要是对齐、连续访问,减少发生缓存抖动的几率

面向数据设计需要思考的问题


总之,面向数据设计需要我们更了解内存/缓存的特性,需要更了解系统和硬件,需要更了解芯片指令与数据结构设计,与面向对象设计相比,数据比代码更重要

DOTS面向数据设计原则

  • 先设计,后编码
  • 为高效使用内存与缓存而设计
  • 为Blittable Data设计
  • 为普通情况设计
  • 拥抱迭代