您的位置:首页 > 运维架构 > 网站架构

GCC中x86架构下simd intrinsic函数的实现的分析

2017-03-01 21:09 295 查看
intel提供了多种寄存器和指令来支持单指令多数据(simd)操作,按时间先后顺序包括MMX系列(支持64位寄存器),SSE系列(支持128位寄存器),AVX系列(支持256位寄存器)和AVX-512系列(支持512位寄存器)。本文以AVX系列的寄存器和指令位例子,分析GCC编译其中如何以intrinsic函数的形式来为程序开发者提供simd操作的支持。


数据类型

512位寄存器的数据类型主要分为两类:一类是以“__v”开头的内部类型,用以实现intrinsic函数;另一类是以“__m”开头的数据类型,是前一种类型的别名。这两种类型都定义为32个字节长度,avxintrin.h文件中有如下定义。
typedef double __v4df __attribute__ ((__vector_size__ (32)));
typedef float __m256 __attribute__ ((__vector_size__ (32), __may_alias__));
其定义是通过vector_size关键字,定义了一个长度为32字节的属性。vector_size是一个attribute,由attributes.c文件中的init_attributes函数来处理。文件中声明了一个attribute_spec类型的数组变量*attribute_tables[4],attribute_spec类型在tree-core.h文件中定义,其结构如下。
struct attribute_spec {
  const char *name;
  int min_length;
  int max_length;
  bool decl_required;
  bool type_required;
  bool function_type_required;
  tree (*handler) (tree *node, tree name, tree args, int flags, bool *no_add_attrs);
  bool affects_type_identity;
};

其中handler是一个指向函数的指针,给出了相应atttributes的处理函数,而vector_size attribute的处理函数为handle_vector_size_attribute。而handle_vector_size_attribute函数的主要功能如下。

根据vecotr_size的参数,计算向量的大小vecsize。单位为字节,如前面所定义的__v4df和__m256都是32字节的长度。

获得vector类型的基础类型type,也就是向量中所包含的元素的类型,一般为32位整型,64位浮点等。

根据vecsize的值和type类型的大小,计算出nunits,也就是向量中包含几个元素。

依据基础类型type和元素个数nunits,调用build_vector_type函数,建立新的向量类型。

利用新的数据类型重新建立node指针所指向的对象,使之指向向量类型。

操作实现

同样以avxintrin.h为例,其中intrinsic函数的命名形式都以_m256开头,不同的名字有不同的含义。例如_mm256_add_pd (__m256d __A, __m256d __B)就是实现两个双精度浮点向量的加法,其中pd是packed double的缩写。操作的实习可以分为四类。

重载普通运算符。此类实现重载了普通的+, -, *, /运算符,只是其类型是向量。
调用builtin函数。
使用其他_mm形式的intrinsic函数来实现。

重载普通运算符

intrinsic函数_mm256_add_pd的实现就是重载了普通运算符,如下。

_mm256_add_pd (__m256d __A, __m256d __B)  

{

  return (__m256d) ((__v4df)__A + (__v4df)__B);

}

采用了普通的“+”符号,只是数据类型变成了__v4df。对于这样的加法,在GCC中把它从gimple语句转换为汇编指令的过程主要分为以下几个步骤。

分析向量操作语句,决定是否要对其进行分解。若语句中指定的操作是机器所不支持的,则需要分解。例如,若机器最多支持128位的寄存器及相关指令,则需要拆分把一个__m256类型的运算拆分成两个__m128类型的运算。这一步骤主要使用expand_vector_operations_1函数来实现。

把语句从gimple形式转换为RTL形式。这一步骤主要通过pass_expand遍中的函数来实现。
从RTL形式生成汇编指令,需要结合编译器中与机器描述相关的md文件。在生成编译器的过程中,编译器中附带的一些转换工具,读取md文件,生成若干的数据结构和程序,辅助机器相关的指令的生成。

expand_vector_operations_1

该函数的主要处理过程可以被分为以下步骤。

获得gimple语句中的详细信息。包括操作码code(在本文的加法例子中,code为PLUS_EXPR);右端表达式类型rhs_class(本例中为GIMPLE_BINARY_RHS);左端表达式lhs和两个右端表达式rhs1和rhs2。
以操作码、类型和操作符表为参数调用optab_for_tree_code (code, type, optab_default),返回操作符op,取值为add_optab。其参数type是语句返回值的类型。optab_for_tree_code是一个GCC提供的与机器无关的函数。
以code、op和type为参数,调用函数get_compute_type (code, op, type)获得计算出的类型。get_compute_type的操作主要分为以下步骤。 
根据type的值,计算出模式(mode,本例中为V8SF,GCC此处用变量compute_mode来表示)。
以op和mode为参数,调用optab_handler(op, compute_mode),获得handler的代码,形式为CODE_FOR_***。若找到的代码不是CODE_FOR_nothing,则返回compute_type。

比较compute_type和type的区别,如果相同,则直接返回,不同则需要做拆分工作。

限于篇幅和精力,这里主要分析了不用拆分的情况,对于拆分的情况,get_compute_type函数和expand_vector_operations_1函数则需要更多的处理。

综上所述,expand_vector_operations_1主要功能是判断gimple语句是否需要拆分,若不需要,则不进行任何操作。把gimple语句转换为RTL指令序列的操作在下一节描述。

 

从gimple到RTL

从gimple到RTL的产生,主要是由expand_gimple_basic_block函数完成的,其主要流程与普通的加法一致,主要区别在于数据类型的不同。对本文例子中的操作来说,最终完成RTL生成的函数是gen_addv8sf3,是编译器从机器描述文件sse.md中的指令描述生成的。编译器执行到此处的函数调用栈如图1所示。sse.md中使用的是define_expand描述,如下。

(define_expand "<plusminus_insn><mode>3<mask_name><round_name>"

  [(set (match_operand:VF 0 "register_operand")

        (plusminus:VF

          (match_operand:VF 1 "<round_nimm_predicate>")

          (match_operand:VF 2 "<round_nimm_predicate>")))]

  "TARGET_SSE && <mask_mode512bit_condition> && <round_mode512bit_condition>"

  "ix86_fixup_binary_operands_no_copy (<CODE>, <MODE>mode, operands);")

 

从RTL到汇编指令

    因为此类对普通运算符重载的操作,在机器描述文件中有对应的define_expand条目,所以从RTL生成汇编指令的过程与普通RTL类似,都是调用final.c文件中的相关函数,并结合生成编译器过程中产生的insn-output.c文件中的数据信息来生成的。



图1 生成汇编函数被执行时的调用栈

builtin函数的实现

类型定义

x86架构下builtin相关的类型的定义在文件i386-builtin-types.def中,其中定义了内置的类型,包括基本数据类型、向量数据类型和函数类型等。生成编译器的过程中,脚本文件i386-builtin-types.awk会读取该def文件,在编译编译器的目录下面生成i386-builtin-types.inc文件,包含了机器相关的具体的类型定义。i386-builtin-types.def中主要定义了以下几个项目。

DEF_PRIMITIVE_TYPE (ENUM, TYPE)。基本类型的定义,ENUM是一个枚举值;TYPE代表对应类型的一个树结点,定义在tree.h文件中。
DEF_VECTOR_TYPE (ENUM, TYPE [, MODE])。向量类型的定义。ENUM是一个枚举值;TYPE是组成向量的元素的类型,可以是之前已经定义的基本类型的枚举值;MODE是模式,定义在i386-modes.def文件中。
DEF_POINTER_TYPE (ENUM, TYPE [, CONST])。指针类型的定义。ENUM是一个枚举值;TYPE是指针所指向的数据的类型;CONST代表指针是否指向一个常量类型。
DEF_FUNCTION_TYPE (RETURN, ARGN*),定义一个函数类型。RETURN代表返回类型;ARGN*代表若干个参数的类型。最终组成的函数类型是一个形式为“RETURN ## _FTYPE_ ## ARG1 ## _ ## ARG2 ...”的字符串。
DEF_FUNCTION_TYPE_ALIAS (ENUM, SUFFIX)。定义一个函数类型的别名。最终组成的字符串的形式为“ENUM ## _ ## SUFFIX”。用于函数的expand。

builtin函数的类型

定义builtin函数的数据结构为builtin_description,如下所示。而builtin函数的类型主要分为10类,分别用10个builtin_description类型的数组来描述。

struct builtin_description

{

  const HOST_WIDE_INT mask;  

  const enum insn_code icode;

  const char *const name;  

  const enum ix86_builtins code;  

  const enum rtx_code comparison;

  const int flag;  

};

下面主要通过例子__builtin_ia32_vfmaddps256来说明builtin函数的定义、解析和转换过程。这个builtin函数的作用是完成一个乘加操作,调用形式为__builtin_ia32_vfmaddps256(va, vb, vc),其3个参数都是256位的向量,每个向量中包含8个单精度的浮点数,表达式 (va * vb + vc)的结果是函数的返回值。该函数定义在数组bdesc_multi_arg[]中,如下所示。

  { OPTION_MASK_ISA_FMA | OPTION_MASK_ISA_FMA4, CODE_FOR_fma4i_fmadd_v8sf,

    "__builtin_ia32_vfmaddps256", IX86_BUILTIN_VFMADDPS256,

    UNKNOWN, (int)MULTI_ARG_3_SF2 },

mask是两个宏的或操作,表示该内置函数需要在ISA_FMA或者ISA_FMA4的架构下才能被使用。CODE_FOR_fma4i_fmadd_v8sf是insn code,在机器描述(md,machine description)文件中对应于define_expand "fma4i_fmadd_<mode>"条目,在编译器生成过程中产生的insn-emit.c文件中有对应的gen_fma4i_fmadd_v8sf函数。接下来是函数的名字。Builtin代码是定义在当前文件(i386.c)文件中的枚举类型ix86_builtins中。标识MULTI_ARG_3_SF2被定义为V8SF_FTYPE_V8SF_V8SF_V8SF,是一个函数类型,返回值是V8SF,三个参数也都是V8SF。

builtin函数测的初始化

    初始化builtins函数的工作在i386.c中的ix86_init_builtins中完成。定义内置函数的c_define_builtins函数通过语句targetm.init_builtins ()来调用ix86_init_builtins,其中targetm是目标机器,也就是运行gcc所编译的二进制代码的芯片。

builtin函数被转换的过程

    文件i386.c中的函数ix86_expand_binop_builtin负责把builtin函数转换为对应的RTX。因为builtin函数与机器的汇编指令相对应,所以RTX也是对应于insn。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  gcc x86 builtin simd