优化数据结构和内存访问模式以改进数据局部性

优化数据结构和内存访问模式以改进数据局部性 (PDF 782KB)

摘要

高速缓存是最重要的现代 CPU 资源之一:它是体积更小、速度更快的一部分内存子系统,用于保存最常用的内存位置副本。 当驻留在高速缓存中的指令需要数据时,该指令将会立即执行。 否则,指令执行过程可能会中止,直到从内存获取到所需的数据。 由于从内存中拷贝数据是一项延迟较长的操作,因此我们希望通过对算法和数据结构进行设计,以充分利用数据局部性,从而最大限度降低缓存缺失。

本文将介绍数据局部性较差的表现、检测相关性能瓶颈的技巧,以及可解决该问题的优化方法。

本文是“英特尔多线程应用开发指南”系列的一部分,该系列介绍了针对英特尔® 平台开发高效多线程应用的指导原则。

背景

关于数据局部性较差问题及其影响,最典型的示例是简单实施矩阵乘法算法。 我们首先从该算法的串行版本开始,该算法按照以下算法乘以方阵 A 和方阵 B,并将结果保存在矩阵 C 中: Cij = ? AikBkj;每个矩阵以主序矩阵的形式保存在阵列中,而且各矩阵的尺寸都非常大 (NxN):

这只是计算密集型算法的一个简单示例,该算法的特征与其他算法相同: 1) 通过嵌套 for-loop 实施;2) 操控大型数据块。 另外一个不太明显的特征是,外层循环迭代保持独立,而且似乎是进行并行化的最佳选择。 以下代码显示了借助英特尔® Cilk™ Plus 实施的并行版算法(我们仅将关键词 for 替换为 Intel® Cilk Plus™ cilk_for,并使用支持英特尔 Cilk Plus 语言扩展的英特尔® 编译器):

通过对比并行实施和串行实施大矩阵的性能,我们发现并行版本的实际运行速度更慢一些。

高级分析对查找该问题的根本成因并无多大用处,这只是其中的一个示例。 确实,如果运行英特尔® VTune™ Amplifier XE 并发性分析,我们可以看出存在一个问题:运行热点函数 (parallel_mm) 时并发性较差,但并不告诉我们原因:

通过运行英特尔® Parallel Amplifier 的并发性分析,我们可以得到类似的结果。 然而,使用英特尔 VTune Amplifier XE — 至尊版英特尔 Parallel Amplifier — 可以为性能统计数据的表现形式增加一个维度:时间线视图。 时间线视图显示应用的工作线程,以及它在各时间段的行为。 在将并发性分析应用于矩阵乘法程序的情况下,英特尔® VTune™ Amplifier XE 检测到了英特尔® Cilk™ Plus 运行时系统创建的除主线程(时间线视图上标记为“Cilk Worker”)之外的三个工作线程,以及每个工作线程随时间推移的 CPU Time 分布(时间线上的棕色部分)。 根据并发性分析收集的数据,我们得出结论:尽管工作线程的 CPU 利用率非常低,但在整个算法执行过程中 CPU Time 非常高。

查找并发性低的原因的下一个步骤是,运行英特尔 VTune Amplifier XE 支持的一个低级别分析器:

英特尔 VTune Amplifier XE 支持多个低级别分析类型,它们均以基于事件的采样 (EBS) 为基础: 轻量级热点和少数几种分析类型可归为“Advanced”类别。 EBS 分析的目的是通过对专用寄存器 PMU(性能检测单元)进行取样以收集硬件事件,如发生某种硬件事件(比如分支预测失误,高速缓存缺失等),可对该 PMU 进行编程以增加其数值。 CPU 可生成许多不同的事件,查找其中一种或一组事件通常并无用处,因为它不清楚分析器显示的数字是否是导致性能降低的那些事件。 通常必须使用正确的事件组合和特定公式以确认应用中是否出现了特定的性能问题。 如果您是第一次运行低级别分析器,或者不确定应该查找哪种类型的问题,建议从“General Exploration”开始。 “General Exploration”是一种预配置分析,可收集多个硬件事件并使用多种公式帮助理解应用的问题类型:该工具收集硬件事件、显示某种公式的数据结果,并突出显示表明性能问题的数字。 “英特尔 VTune Amplifier XE 帮助”中详细介绍了所有事件和公式;大家可通过工具提示查看其简要介绍。

针对矩阵乘法程序运行“General Exploration”显示了几个性能瓶颈(标为粉色的单元):

  1. CPI 比率: 显示每条指令使用的平均 CPU 周期数量。 理想状态下,我们希望该数字小于或等于 1,但此时该数字大于 5。
  2. 退回停顿: 显示无微操作退回时的周期数量与总周期数量的比例。 在没有长延迟操作和依赖链的情况下,该数字应等于或大于 1,但此时该数字约为 0.8。
  3. LLC 缺失: 显示带有明显 LLC(末级高速缓存)缺失的周期数量与总周期数量的比例。 缺失 LLC 的内存请求必须由本地或远程 DRAM 提供服务,会出现明显的延迟现象。 理想情况下该数字应接近于 0。
  4. 执行停顿: 显示未执行微操作的周期数量与总周期数量的比例。 等待关键计算资源是出现执行停顿的原因。

根据英特尔 VTune Amplifier XE 显示的数据可以推断出,由于较高的 LLC 缺失率,CPU 需耗费时间等待从本地或远程 DRAM 中获取数据。 如果双击该函数,英特尔 VTune Amplifier XE 将显示函数源视图以及每条源线的硬件事件分布。 例如在我们的示例中,大部分样本由最内层 for-loop 生成。

我们来检查 parallel_mm 函数中实施的数据访问模式,看是否为 i = 0,j = 0:

每次最内层循环迭代均接触接近矩阵 A 的元素,接近前次迭代所接触的元素,而且这种访问模式可高效利用数据局部性。 然而矩阵 B 的访问模式效率不高,因为程序的每次迭代需要访问第 k 栏(k 表示内层循环计数器)中的第 j 个元素。 这种低效的数据访问模式是造成程序性能低下的原因,因为它会造成较高的高速缓存缺失率。

我们可以考虑一种更高效的算法实施,它仅需要对代码作出一点小小的改变,就可显著改进数据访问模式:

交换两个内层循环可改进数据局部性(i=0,k=0):

利用这种新的实施方法,对于每个 i(外层循环的计数器)我们可以计算矩阵 C 的整行,并仅接触矩阵 A 和 B 的连续元素。这种改变能够提高 CPI 并最大限度减少 LLC 缺失,从而大幅提高性能:

建议

英特尔® VTune Amplifier XE 具有许多实用的特性,能够帮助您查找程序中的性能瓶颈。 尽管高级别分析类型也有助于算法优化,但低级别分析类型可重点分析应用执行过程中的硬件行为。 现代 CPU 非常复杂,可能需要专家来执行低级别性能分析。 但英特尔 VTune Amplifier XE 的宗旨是简化低级别性能分析,让非专家级用户也可顺利执行这种分析。 当高级别分析类型无法解决该问题时,可以尝试运行英特尔 VTune Amplifier XE General Exploration,它可为您提示接下来应该执行的步骤并查找问题成因。

之前我们仅探讨过一个有关数据局部性问题的示例,更改算法可帮助我们改进数据局部性并提高性能。 但数据结构设计不够合理也可能造成数据局部性较差,因此必须改变数据布局才能改进数据局部性。 这第二种数据局部性问题其表现也相同:低并发性级别、低 CPI、高缓存缺失率,英特尔 VTune Amplifier XE 同样能够检测到这些表现。

设计数据结构和算法时充分考虑数据局部性可获得出色的性能和可扩展性。 利用良好的数据局部性,将大大有利于串行和并行应用。

使用指南

性能分析是一个迭代流程。 使用其中一种英特尔 VTune Amplifier XE 分析类型时,首先对代码做出一些更改,然后重新运行该分析类型。 英特尔 VTune Amplifier XE 具备自动对比功能,可帮助您对两次运行的分析结果进行对比,并依此跟踪进展。

在本文中我们介绍了“General Exploration”分析。 该分析旨在帮助您着手执行低级别性能分析。 还有许多其他更为具体的分析类型(“Memory Access”、“Cycles and uOps” 等),可以作为下一个运行步骤。另外,如果您知道正在调查的性能瓶颈类型,或需要更多具体详情,都可以运行这些分析类型。

更多资源

有关编译器优化的更完整信息,请参阅优化通知