本文是阅读Dorit Naishlos的文章“Autovectorization in GCC”时做的笔记。
在使用了语法树上的静态单赋值(tree SSA)优化框架之后,GCC已经具备了支持自动向量化的能力。目前对向量化的一个限制是,向量化必须在不存在迭代间数据依赖的前提下才能实施。
SIMD的向量化与传统向量机的不同在于,SIMD每次向量化的数组元素较少,随着数组元素类型的不同,可以向量化的数组元素数目也有变化,这个叫做向量化因子。
GCC中的自动向量化可以分为两类。
- 针对循环的。处理循环中不同迭代之间的数据并行。
- 针对普通串行代码的。这部分的并行技术叫做SLP(Superword Level Parallelism)。
传统自动向量化技术与SIMD自动向量化技术的区别。
- 传统的技术主要是面向Fortran语言的科学计算程序。而SIMD则更侧重于C语言。C语言中的指针机制会带来新的问题。
- SIMD架构下的内存结构要弱一些,对能够自动向量化的代码有着更严苛的要求。
- 只能访问连续内存,要求向量长度倍数的对齐。
- 有的平台下面有一些处理这些内存问题的机制,但是往往比较难以使用并且具有较高的开销。
- SIMD指令不够通用和规范 。有些操作是与领域相关,有些只作用于某些数据类型上,不同架构的指令又不相同。
GCC中的数据依赖分析与向量化
数据依赖分析主要通过三个步骤。
- 建立数据依赖图DDG(Data Dependence Graph)。
- nodes: 代表loop语句。
- edges: 表示依赖关系,可以是标量之间,也可以是内存引用之间。
- 内存相关性的检测,可以通过比较下标的相关性测试来判断,在(tree-data-ref.c文件中实现)。
- 通过检测DDG中的SCC(Strong Connected Components)来判断是否可以自动并行。
为解决SCC带来的无法向量化问题,可以使用loop distribution操作来把一个循环中的语句拆开,一个SCC对应一个循环(这个技术在作者写作本文时尚未在GCC中实现,但是目前已经实现)。
选项-ftree-vectorize同时使能了两个选项:ftree-loop-vectorize和ftree-slp-vectorize。前者实现循环中的自动向量化,后者实现串行代码中的自动向量化。
实现循环向量化的主体函数是vectorize_loops,它主要分为分析(vect_analyze_loop)和转换两个部分(vect_transform)。
- vect_analyze_loop
- vect_analyze_loop_form
- analyze_data_refs
- analyze_scalar_cycles
- analyze_data_ref_dependence
- analyze_data_ref_accesses
- analyze_data_ref_alignment
- analyze_operation
- vect_transform
- vect_transform_stmt
- vect_transform_loop_bound
自动向量化对循环的形式是有要求的,需满足如下条件。
- 循环的执行次数是可以预测的。
- 只能处理最内层的循环。
- 目前只支持只包含一个基本块的循环。对于某些简单的if-then-else结构,可以通过使用条件操作来把该结构转换成为一个基本块。
数据引用分析函数analyze_data_refs,负责找到所有的内存引用,并且检查它们是否是可分析的,也即建立一个访问函数。主要分析内容如下。
- 内存依赖。
- 访问模式。
- 对齐分析。
标量的依赖环分析。需要分析循环体中的标量之间的依赖环,并进一步采取手段去打破它们。
操作分析函数analyze_operations。扫描所有的操作,决定向量化因子。目前的自动向量化机制具有下属约束。
- 每个平台只支持一个向量长度。
- 每个循环只支持一种数据类型的自动向量化。
向量化的转换主要分为以下步骤。
- 从头至尾扫描循环体中的语句。
- 为每个语句增加一个指向向量化后语句的指针。
- 显式地删除原始的store语句。
- 其他的语句在死代码删除优化中被删除掉。
也存在着其他的一些情况,而无法对语句进行一对一的向量化。
内存引用的处理
目前主要处理两种内存引用。
- ARRAY_REFs
- INDIRECT_REFs
为了提高并行度,增大自动向量化的机会,可以进行一些增强型的数据流分析。
- 使用更复杂的数据依赖测试。
- 去掉那些距离大于向量化因子的引用。
- 通过重新排列结点来尝试去掉一些依赖。
当存在指针别名的时候,仍然可以进行向量化,但需要用runtime_overlaptest来保证向量化的正确性。
内存访问若连续,则可以直接向量化。否则,就需要进行特殊的数据操作,然后把它们装载到一个向量中。
- gather/scatter操作。
- 把两个向量中的数据打包(pack)到一个向量中。
- 在数据元素上使用交换操作。
- 为特定的访问模式提供支持。
函数analyze_access_pattern验证访问模式是否能被向量化。当前只支持连续数据访问。
处理对齐的几个不同级别。
- 静态对齐分析。
- 通过变换来强迫对齐。
特殊的指令来支持高效的不对齐访问。
强制对齐数据引用(enforce_data_refs_alignment)的手段。
- loop versioning。循环的多版本。
- loop peerling。循环剥离。
处理不对齐的两个步骤。
- 有一个计算对齐差距的函数。
- 再有一个函数,按照对齐的差距,使用两个vector指针,来生成一个vector。
目前处理自动向量化有两个自相矛盾的目标。
- 较高层的中间表示(在语法树上处理)。
- 要描述一些低级别的特性(例如对齐等)。
判断一个循环是否能够被向量化,从以下三个方面。
- loop form
- data reference
- operation
当前可以进行向量化的loop必须具备的特点。
- 最内层的循环,只有单个基本块。
- 只有连续的数组访问,并且保持对齐。
- 没有可以产生标量环的操作,所有的操作都施加于同一数据类型,并且可以用现有的语法树操作码来表示。
未来的工作
支持更多的loop形式。
- unknown loop bound。
- if-then-else结构。
支持更多的数据引用类型。
- 增强数据依赖的测试。
- 支持其他的数据访问模式。
- 利用数据重用。
支持更多的操作。
- 支持作用于不同数据类型上的操作,包括强制类型转换cast。这需要有data packing和unpacking的支持。
- 支持对于induction和reduction等操作的向量化。需要使用target hook或者新的tree code。
其他的优化。
- 同一平台上支持多个vector length。
- 建立向量化的代价模型。
- 与其他优化或者pass的接口。
- loop parallelism.
- SLP pass.