从 SortExec 的排序来谈 Spark Tungsten 计划中的缓存友好特性

背景

本文基于Spark 3.5.3

Spark Tungsten(钨丝计划)是 Databricks 引入的 Spark SQL 优化引擎。主要致力于提高Spark的执行效率:

它主要从以下三个方面:

  1. 内存管理优化
  2. 全阶段代码生成
  3. 缓存敏感计算

前两个好理解,如 内存管理优化 主要是通过手动管理内存,使用紧凑二进制的UnsafeRow的数据结构来替换jvm的数据结构
全阶段代码生成 通过消除虚函数的调用(会有二次寻址),来提高JVM运行效率,

那第三个是什么呢?缓存敏感计算到底指的是什么?这一篇文章就来说清楚。

结论

先说结论:在Sort中 Spark通过将排序键和指向数据的指针存储在一起,主要是先利用排序键做前缀索引,来进行Record的数据比较,如果排序键相同的话,再从内存中获取对应的数据进行比较,这种从内存中获取数据是随机访问,导致CPU的局部缓存命中率低,不像把数据排序的键存在一起,可以高效的利用CPU的局部缓存。

支持前缀比较器的类型有: BooleanType | ByteType | ShortType | IntegerType | LongType | DateType |TimestampType | TimestampNTZType | FloatType | DoubleType | _: AnsiIntervalType| DecimalType 且 精度位数小于 18

注意这里的StringType不支持前缀比较器,因为String类型的长度是不定的,不能用long类型来表示,从而无法进行比较。

分析

先看SortExec涉及到排序的数据流:

复制代码
SortExec.doExecute
   ||
   \/
SortExec.createSorter
   ||
   \/
insertRow  ->   UnsafeExternalRowSorter.sort
                 ||
                 \/
                UnsafeExternalSorter.getSortedIterator
                 ||
                 \/
                UnsafeInMemorySorter.getSortedIterator

这里的UnsafeInMemorySorter包括了 插入数据的recordPointer + keyPrefix
注意:这里的数据保存在LongArray中(从taskMemoryManager分配过来的),这里的LongArray以8字节作为一个步长,用Unsafe.putLong和setLong进行操作,也就是存储8字节的数据

这里的 recordPointer 来源于 memory page以及page内的offset

复制代码
 private MemoryBlock currentPage;
 ...
 recordPointer = taskMemoryManager.encodePageNumberAndOffset(currentPage, pageCursor)

keyPrefix 来源于SortPrefixUtils.getPrefixComparatorcomputePrefix的方法,这里会根据不同的类型返回不同的前缀比较器。
SortPrefixUtils.getPrefixComparator的代码如下:

复制代码
def getPrefixComparator(sortOrder: SortOrder): PrefixComparator = {
    sortOrder.dataType match {
      case StringType => stringPrefixComparator(sortOrder)
      case BinaryType => binaryPrefixComparator(sortOrder)
      case BooleanType | ByteType | ShortType | IntegerType | LongType | DateType | TimestampType |
          TimestampNTZType | _: AnsiIntervalType =>
        longPrefixComparator(sortOrder)
      case dt: DecimalType if dt.precision - dt.scale <= Decimal.MAX_LONG_DIGITS =>
        longPrefixComparator(sortOrder)
      case FloatType | DoubleType => doublePrefixComparator(sortOrder)
      case dt: DecimalType => doublePrefixComparator(sortOrder)
      case _ => NoOpPrefixComparator
    }
  }

UnsafeInMemorySorter.getSortedIterator的流程如下:

复制代码
                ┌─────────────────────────────────────┐
                │  UnsafeInMemorySorter.getSortedIterator()  │
                └─────────────────────────────────────┘
                                    │
                                    ▼
                ┌─────────────────────────────────────┐
                │    prefixComparator 支持 Radix Sort?│
                └─────────────────────────────────────┘
                        │                      │
                       Yes                     No
                        │                      │
                        ▼                      ▼
            ┌───────────────────────┐  ┌───────────────────────┐
            │    RadixSort          │  │    TimSort (Sorter)   │
            ├───────────────────────┤  ├───────────────────────┤
            │ • 直接排序 key prefix  │  │ • 需要比较器           │
            │ • O(n * 8) 复杂度      │  │ • O(n log n) 复杂度   │
            │                       │  │ • 需要完整记录比较      │
            │ • 缓存友好             │  │ • 可能缓存未命中        │
            └───────────────────────┘  └───────────────────────┘
                          │                      │
                          └──────────┬───────────┘
                                     │
                                     ▼
                    ┌─────────────────────────────────────┐
                    │      返回 UnsafeSorterIterator      │
                    └─────────────────────────────────────┘ 

这里会根据 prefixComparator 是否支持Radix sort(基数排序)来判断,是直接走 Radix sort还是 TimSort

如果是Radix sort,则直接根据前缀比较器进行比较;

如果是TimSort的话,则先直接根据前缀比较器进行比较,如果相等,则在根据pointer获取数据进行比较。

注意无论是 Radix sort 还是 TimSort ,如果只是用到前缀比较器的话(没有用到pointer获取数据),因为排序键的前缀都存在一个LongArray类型中,这样CPU需要数据的时候,会以数据块为单位从内存加载到缓存,根据局部性的原理(若一数据被访问,邻近数据也大概率被访问),相邻的排序键的前缀也都会加载到缓存中,这样CPU的缓存命中率就会大大提升:

复制代码
 offset = RadixSort.sortKeyPrefixArray(
          array, nullBoundaryPos, (pos - nullBoundaryPos) / 2L, 0, 7,
          radixSortSupport.sortDescending(), radixSortSupport.sortSigned());
 ...
 MemoryBlock unused = new MemoryBlock(
          array.getBaseObject(),
          array.getBaseOffset() + pos * 8L,
          (array.size() - pos) * 8L);
 LongArray buffer = new LongArray(unused);
 Sorter<RecordPointerAndKeyPrefix, LongArray> sorter =
   new Sorter<>(new UnsafeSortDataFormat(buffer));
 sorter.sort(array, 0, pos / 2, sortComparator);
相关推荐
ZGi.ai1 小时前
企业AI资产管理体系:提示词、工作流、知识库应该怎么管
大数据·知识库·工作流编排·ai资产·提示词管理
爱分享的康康2 小时前
低成本自动驾驶数据采集设备理性分析:康谋入门套装适配性解析
大数据·人工智能
程序鉴定师3 小时前
上海小程序开发的坚实保障与行业优势解析
大数据·小程序
Elastic 中国社区官方博客4 小时前
我们如何在 Elasticsearch Serverless 上将向量搜索吞吐量提升一倍
大数据·数据库·人工智能·elasticsearch·搜索引擎·云原生·serverless
zgl_200537794 小时前
源代码:跨数据库通用SQL语法解析与标注拆解
大数据·数据库·数据仓库·sql·etl·源代码管理
Ajie'Blog5 小时前
Claude 大模型深度评测:从参数架构到实战边界
大数据·人工智能·架构
暴躁小师兄数据学院6 小时前
【AI大数据工程师特训笔记】第13讲:数据库性能手术刀
大数据·数据库·数据仓库·sql·postgresql
无忧智库6 小时前
车路云一体化复杂交通博弈多智能体系统可行性研究报告(WORD)
大数据·人工智能·自动化
数据皮皮侠AI6 小时前
上市公司耐心资本数据(2010-2025)
大数据·人工智能·笔记·能源·1024程序员节
陕西企来客6 小时前
陕西 KNIT 可信知识网络构建模块对于 GEO 优化行业的影响深度调查:企来客科技技术落地真相揭示
大数据·人工智能