Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LoongArch64实验性向量调用约定实现 (Experimental implementation of LoongArch64 vector calling convention) #114

Open
ChenghuiPan opened this issue Jan 10, 2024 · 11 comments

Comments

@ChenghuiPan
Copy link

ChenghuiPan commented Jan 10, 2024

For English version, please keep scrolling down.

这是目前基于GCC对LA64实现的一个实验性的向量调用约定,代码位于dev/vecarg分支,如果存在问题和不完善的情况欢迎大家进行讨论。如果中英文版本之间存在描述模棱两可的情况也请提出,谢谢大家!

代码位于#113

关键字

  • VR: 向量寄存器(Vector Register)
  • VAR: 向量参数寄存器(Vector Argument Register)
  • VRLEN: 向量寄存器位宽(Vector Register Length)

向量类型

向量的比特位宽可以是128bit或者256bit,并且总是包含多个元素。向量元素从最低比特起占据向量空间,并且拥有从0开始递增的index。
向量的元素类型遵循于LP64数据模型。

向量寄存器

LA64可以选择性的实现32个128位或者256位的向量寄存器硬件。如果实现向量寄存器,则必须实现双精度浮点硬件单元。
同编号的256位向量寄存器的低半部分与128位寄存器共用,同编号的128位向量寄存器低半部分和浮点寄存器共用。

以下为向量寄存器的使用约定:

名称 用途 是否在过程间保存
$vr0 - $vr1 (128位) / $xr0 - $xr1 (256位) 参数寄存器/返回值寄存器
$vr2 - $vr7 (128位) / $xr2 - $xr7 (256位) 参数寄存器
$vr8 - $vr31 (128位) / $xr8 - $xr31 (256位) 临时寄存器

TODO:对于在过程间保存完整内容的寄存器(static register/callee-saved register),目前尚无明确最终方案,需要有效的性能测试手段来辅助判断。
目前在配合sleef向量数学库(还未提交社区)对x264、libjpeg-turbo进行性能测试的过程中,不同s/t寄存器的分配对性能没有产生明显影响。

向量调用约定

向量调用约定扩展是叠加于LP64D之上、使用128/256位向量寄存器,对向量参数和返回值进行传递的调用约定扩展。
可以通过以下的方式启用该调用约定:

  • 使用vecarg选项对编译模块进行编译。这会使编译模块内的所有使用了向量参数、向量返回值函数都遵循该调用约定。
  • 使用vecarg属性在源码中标记特定函数。被标记的函数会启用该调用约定。

为了使向量调用约定在函数、编译模块之间的行为保持一致,需要遵循以下的要求:

  • 如果使用vecarg选项构建一个编译模块,如果另一模块调用了该模块使用了向量参数、向量返回值的函数,该模块也应当使用vecarg选项进行编译。
  • 如果使用vecarg属性标记了一个函数,该函数的所有声明、定义都应当使用vecarg属性进行标记。
  • 对于所有利用了向量调用约定的编译模块,使用相同的向量长度指令集支持进行编译。

p.s.: 对于GCC当前的PoC实现,vecarg选项对应-mvecarg, vecarg属性对应于__attribute__ ((vecarg))

子程序调用流程

在以下的向量调用约定描述中,对于128/256位向量的传递描述中,我们都认为编译器开启了对应位宽的向量指令支持。

寄存器

VAR:0-7号向量寄存器按照编号依次用于向量参数的传递。同时,0-1号向量寄存器用于向量返回值的传递。向量参数传递时,总是会选择VRLEN等于向量参数位宽的VAR进行传递。

参数传递

在启用向量调用约定时,参数可能的传递形式如下:

  1. 一个参数寄存器。
  2. 一对编号连续的参数寄存器。
  3. 下面的任意一种不同类型参数寄存器的配对组合:
    • 一个GAR和一个FAR
    • 一个GAR和一个VAR
    • 一个FAR和一个VAR
  4. 一个在栈区域连续的内存块,该内存块具有由子程序调用者的$sp计算的偏移常量
  5. 1和4的组合。

单个向量参数的传递

  1. 128位向量

    • 如果存在至少1个VAR可用,则使用VAR进行传递。
    • 如果无VAR可用,至少2个编号相邻的GAR可用,则使用这一对GAR进行传递,低64位存储在编号靠前的GAR,高64位存储在编号靠后的GAR。
    • 其他情况,完全通过栈进行传递。
  2. 256位向量

    • 如果存在至少1个VAR可用,则使用VAR进行传递。
    • 如果无VAR可用,至少一个GAR可用,则将256位向量存储在调用者的栈空间,并且将存放位置对应的内存地址存放在GAR。
    • 如果无VAR和GAR可用,则完全通过栈进行传递。

带有向量成员的结构体的传递

无论何种场合,最多仅使用两个寄存器(所有使用的寄存器类型的数量之和)进行结构体的传递,否则从栈进行参数传递。

  • 如果结构体仅存在一个成员,并且该成员是向量,则参数传递规则与单个向量参数的传递行为相同。
  • 当结构体的成员为两个时:
    1. 如果两个成员均为向量,而且有至少两个编号连续的VAR可用,则使用这两个VAR进行传递。
    2. 如果结构体包含一个向量成员、一个浮点成员,在FAR、VAR有空闲的前提下,使用一个VAR、一个FAR对两个成员进行传递。
    3. 如果结构体包含一个向量成员、一个整型成员,在FAR、GAR有空闲的前提下,使用一个VAR、一个GAR对两个成员进行传递。
  • 其他情况下,如果至少有一个空闲的GAR,则进行引用传参,否则从栈进行传递。

p.s.:如果结构体成员包含0长度位域、0长度数组、空结构体或空组合体等成员,其处理规则与基础ABI中Other structures中所描述的处理方式相同。

可变长参数列表的传递

对于向量参数,不使用VAR/FAR进行传递。

对于128位向量,如果至少有两个GAR可用,并且首个GAR的编号为偶数,则使用这对GAR传递参数。

对于256位向量,根据向量位宽遵循现有基础ABI定义。

返回值

0-1号VAR用于返回值的传递,传递方式与参数列表中首个参数的传递逻辑相同。


This is a experimental vector calling convention impl. for LoongArch64 based on GCC. The ad-hoc implementation can be found in this pull request: #113.

Any discussions about this prototype calling convention are welcome! And please report any inconsistency between Chinese and English version. Thanks!

Keywords

  • VR: Vector Register
  • VAR: Vector Argument Register
  • VRLEN: Vector Register Length

Vector Types

A vector can be either 128 bits or 256 bits width, and always contains
multiple elements. Each member of vector consecutively occupies the vector
from lowest bits, and has index that starting from zero.

Elements of a vector always have same base scalar type from LP64 data model.

Vector Register

LoongArch machines that implements LA64 can optionally have 32 vector registers
may be either 128 or 256-bit, depending on the hardware implementation. double-precision
FPU is required for vector registers. Floating-point registers and vector registers that have same
index postfix follow the overlapping rules below:

  • Floating-point registers are overlapping the lower 64 bits of 128-bit and 256-bit vector registers.
  • 128-bit vector registers are overlapping the lower 128 bits of 256-bit vector registers.
Name Usage Preserved across calls
$vr0 - $vr1 (128-bit) / $xr0 - $xr1 (256-bit) Argument registers / return value registers No
$vr2 - $vr7 (128-bit) / $xr2 - $xr7 (256-bit) Argument registers No
$vr8 - $vr31 (128-bit) / $xr8 - $xr31 (256-bit) Temporary registers No

TODO: For "static register"/"callee-saved register", we didn't have a clear resolution for now, and we need effective performance measurements for definition.

In current performance test, when utilize different static/temp register allocation solutions with vector calling convention, x264/libjpeg-turbo's testing tool and sleef vector math library(loongarch support not released yet), we can't see significant difference in performance outputs.

Vector Calling Conventions

Vector calling convention extension is based on the LP64D, it utilizes 128-bit/256-bit vector register to pass vector argument and return value.It can be enabled via:

  • Use "vecarg option" to compile objects. This way will makes all functions that contain vector arguments or vector return values follow this calling convention.
  • Use "vecarg attribute" to mark specific function. The function that being marked will follow this calling convetion.

For consistent behavior between objects and functions, following rules should be considered while utilizing vector calling conventions:

  • When compiling object A with "vecarg option", if object B invokes functions that contain vector arguments or return value from this object, obejct B also need to be compiled with "vecarg option".
  • When marking function with "vecarg attribute",
  • All objects that utilize vector calling convention should be compiled with same SIMD instuction option(Keep same max vector length).

p.s.: For current GCC PoC implementation, "vecarg option" refers to -mvecarg, "vecarg attribute" refers to __attribute__ ((vecarg))

Subroutine Calling Sequence

In the following description of vector calling convention, we assume 128/256-bit vector insturction support is enabled in compiler while utilizing corresponding convention.

Registers

VAR: Number 0 - 7 vector register are preserved for vector argument passing, and number 0 - 1 vector are also used for vector return value.

Argument Passing

When vector calling convention is enabled, the possible passing method will be one of the following options:

  1. An argument register.
  2. A pair of argument registers with adjacent numbers.
  3. Any combination type of a pair of argument registers below:
  • a GAR and a FAR.
  • a GAR and a VAR.
  • a FAR and a VAR.
  1. A contiguous block of memory in the stack arguments region, with a constant offset from the caller's outgoing $sp.
  2. A combination of 1 and 4.

Passing Single Vector Argument

  1. 128-bit vector argument

    • If at least 1 VAR is available, then pass this argument via single VAR.
    • If no VAR is available and at least 2 GARs with adjacent numbers are available, then pass vector argument via them; the low 64-bit part of vector argument is stored inside first GAR, and high 64-bit part of vector argument inside second GAR.
    • For other condition, pass vector argument on stack.
  2. 256-bit vector argument

    • If at least 1 VAR is available, then pass this argument via single VAR.
    • If no VAR is available and at least 1 GAR is available, then store 256-bit vector argument on stack, then pass vector argument's address via GAR.
    • If no GAR and VAR is available, pass vector argument on stack.

Passing Struct with Vector Member

For all conditions, we only use at most 2 registers(sum of all register types) to pass a struct with vector member, otherwise pass structure on stack.

  • If struct only has 1 member and it's vector type, then we follow the single vector argument passing rule for this struct.
  • If struct has 2 members:
    1. If all of members are vector type, and 2 or more VARs with adjacent numbers are available, then pass struct via them; first vector member is stored inside first VAR, and second vector member inside second VAR.
    2. If struct contains 1 vector member and 1 float-point member, and VARs and FARs are sufficient, then use 1 VAR and 1 FAR to pass struct.
    3. If struct contains 1 vetcor member and 1 integer member, and VARs and GARs are sufficient, then use 1 VAR and 1 GAR to pass struct.
    4. For other conditions, If at least 1 GAR is available, then pass struct by reference, otherwise pass on stack.

p.s.: If struct contains zero-with bit field/zero-length array/empty struct/empty union, the passing rule is same as the description of "Other Structure" in base ABI document.

Variadic arguments

We don't use VAR/FAR to pass vector arguments.

For 128-bit vector argument, if at least 2 GARs are available, and first GAR's number is even, then use this pair of GARs to pass argument.

For 256-bit vector argument, it follows the current base ABI conventions with its data bit-width(256-bit).

Return Value

0 - 1 VARs are used for passing return value. The passing rule of return value is same as the first argument's method of argument list.

@ChenghuiPan
Copy link
Author

ChenghuiPan commented Jan 10, 2024

@xry111
Copy link

xry111 commented Jan 19, 2024

复数类型需要特殊考虑吗?

在目前的非向量调用惯例中复数直接当成有两个浮点成员的结构体,会用两个 FPR,但是 CPUCFG word 2 bit 8 预留了“复数向量指令”,而且好像 (?) 即使没有专用的复数向量指令而只用一般向量指令,用向量寄存器传复数也是有好处的 (至少加减法就简化成一条指令)。

另外可以学 RISC-V 在二进制文件中打标记以防止不小心把 ABI 不兼容的东西链接起来:https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-elf.adoc#dynamic-linking

@ChenghuiPan
Copy link
Author

ChenghuiPan commented Jan 22, 2024

复数类型需要特殊考虑吗?

在目前的非向量调用惯例中复数直接当成有两个浮点成员的结构体,会用两个 FPR,但是 CPUCFG word 2 bit 8 预留了“复数向量指令”,而且好像 (?) 即使没有专用的复数向量指令而只用一般向量指令,用向量寄存器传复数也是有好处的 (至少加减法就简化成一条指令)。

另外可以学 RISC-V 在二进制文件中打标记以防止不小心把 ABI 不兼容的东西链接起来:https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-elf.adoc#dynamic-linking

复数之前还没有考虑过,感觉可以试一试
ELF标记的话,目前主要考虑到系统大部分可能不会用vecarg来编译,所以暂时没有打标记,不过目前好像除非两个有向量调用的object,一边开vecarg、一边不开/向量开关对不上的时候会出问题,其他时候好像暂时没有遇到爆炸的情况

不过之前准备在代码里如果出现了“有向量参数,vecarg开了,向量没开”的情况下给出警告,因为代码需要改动的地方比较多还没有做出来

@xry111
Copy link

xry111 commented Jan 22, 2024

复数类型需要特殊考虑吗?
在目前的非向量调用惯例中复数直接当成有两个浮点成员的结构体,会用两个 FPR,但是 CPUCFG word 2 bit 8 预留了“复数向量指令”,而且好像 (?) 即使没有专用的复数向量指令而只用一般向量指令,用向量寄存器传复数也是有好处的 (至少加减法就简化成一条指令)。
另外可以学 RISC-V 在二进制文件中打标记以防止不小心把 ABI 不兼容的东西链接起来:https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-elf.adoc#dynamic-linking

复数之前还没有考虑过,感觉可以试一试 ELF标记的话,目前主要考虑到系统大部分可能不会用vecarg来编译,所以暂时没有打标记,不过目前好像除非两个有向量调用的object,一边开vecarg、一边不开/向量开关对不上的时候会出问题,其他时候好像暂时没有遇到爆炸的情况

不过之前准备在代码里如果出现了“有向量参数,vecarg开了,向量没开”的情况下给出警告,因为代码需要改动的地方比较多还没有做出来

对,就是在这种情况下让 ld 或者 ld.so 直接报错,不然运行到一半输出一堆乱码之类的很难调试。

@xen0n
Copy link

xen0n commented Jan 22, 2024

可能需要考虑同一编译单元内的混搭:例如用户可能希望综合使用 GNU ifunc、CPUCFG or HWCAP 等方式,来明确、自行控制一个公共的入口点之后的执行路径,使之在合适的软硬件组合上自动利用上 vecarg 能力。这种情况下可能不便以 ELF e_flags 或其他文件粒度的方式处理:如果一个文件虽然含有 vecarg 代码(因此它有了 vecarg ABI modifier 之类的标记),但其实这部分代码在不支持的系统上不会被跑到,那我们应该不想阻止用户运行。

@xen0n
Copy link

xen0n commented Jan 22, 2024

此外,考虑到这些研究成果在世界范围尚属新颖(如果没记错的话,RISC-V 的类似调用约定也未定稿),可能将这些内容翻译之后向 GCC、LLVM 等编译器社区发出 RFC 是不错的选择。这样也可以避免由于我们这些人菜,而未能提前预见,而造成又一次 rebuild world 之类的大量额外工作量。

@xry111
Copy link

xry111 commented Jan 22, 2024

可能需要考虑同一编译单元内的混搭:例如用户可能希望综合使用 GNU ifunc、CPUCFG or HWCAP 等方式,来明确、自行控制一个公共的入口点之后的执行路径,使之在合适的软硬件组合上自动利用上 vecarg 能力。这种情况下可能不便以 ELF e_flags 或其他文件粒度的方式处理:如果一个文件虽然含有 vecarg 代码(因此它有了 vecarg ABI modifier 之类的标记),但其实这部分代码在不支持的系统上不会被跑到,那我们应该不想阻止用户运行。

RISC-V 的 STO_RISCV_VARIANT_CC 是符号粒度的,可以每个符号不一样。

@xry111
Copy link

xry111 commented Jan 24, 2024

另外还有 __int128 要不要走向量寄存器,因为我们向量指令集自带 128 位加减法,虽然我还没算清楚用向量指令做 128 位乘除会不会比一般做法快……

@ChenghuiPan
Copy link
Author

另外还有 __int128 要不要走向量寄存器,因为我们向量指令集自带 128 位加减法,虽然我还没算清楚用向量指令做 128 位乘除会不会比一般做法快……

感觉可能要同时实现一下相关的128位操作,现在的运算依然还是用标量指令做的,如果走向量寄存器的话现在可能还涉及到取回标量寄存器去做运算的情况?

另外我发现之前的代码关于-mvecarg和vecarg attribute的实现有点问题(两者变成and的关系了,预想的是or的关系),稍微修复了一下。

@xen0n
Copy link

xen0n commented Jan 25, 2024

另外还有 __int128 要不要走向量寄存器,因为我们向量指令集自带 128 位加减法,虽然我还没算清楚用向量指令做 128 位乘除会不会比一般做法快……

可能不合适,这样的话会让 int128 的 ABI 取决于是否假定存在硬件向量扩展。另外,在做不受 LSX/LASX 直接支持的整数操作时,可能代价比倒腾两个整数寄存器更大。

@ChenghuiPan
Copy link
Author

ChenghuiPan commented Feb 5, 2024

复数类型需要特殊考虑吗?

在目前的非向量调用惯例中复数直接当成有两个浮点成员的结构体,会用两个 FPR,但是 CPUCFG word 2 bit 8 预留了“复数向量指令”,而且好像 (?) 即使没有专用的复数向量指令而只用一般向量指令,用向量寄存器传复数也是有好处的 (至少加减法就简化成一条指令)。

最近尝试了以下似乎不太好实现
如果在loongarch_get_arg_info对复数参数返回单个V2DF寄存器的话,GCC确实会尝试用vec_concat:DC来把参数放到寄存器里,但是寄存器的模式是V2DF,导致set表达式dest和src的模式不一致,结果在emit_move_insn里会导致ICE,gdb定位到的地方大概这样:

Breakpoint 1, emit_move_insn (x=0x7ffff6f74540, y=0x7ffff6f74648) at /home/panchenghui/upstream/gcc/gcc/expr.cc:4606
4606      gcc_assert (mode != BLKmode
(gdb) call debug_rtx(x)
(reg:V2DF 32 $f0)
(gdb) call debug_rtx(y)
(concat:DC (reg:DF 83)
    (reg:DF 84))
(gdb) l
4601      machine_mode mode = GET_MODE (x);
4602      rtx y_cst = NULL_RTX;
4603      rtx_insn *last_insn;
4604      rtx set;
4605
4606      gcc_assert (mode != BLKmode
4607                  && (GET_MODE (y) == mode || GET_MODE (y) == VOIDmode));

diff

diff --git a/gcc/config/loongarch/loongarch.cc b/gcc/config/loongarch/loongarch.cc
index d1946fb4c02..918069e80c6 100644
--- a/gcc/config/loongarch/loongarch.cc
+++ b/gcc/config/loongarch/loongarch.cc
@@ -669,10 +669,13 @@ loongarch_get_arg_info (struct loongarch_arg_info *info,
            return gen_rtx_REG (mode, fregno);

          case MODE_COMPLEX_FLOAT:
-           return loongarch_pass_fpr_pair (mode, fregno,
-                                           GET_MODE_INNER (mode), 0,
-                                           fregno + 1, GET_MODE_INNER (mode),
-                                           GET_MODE_UNIT_SIZE (mode));
+           if (pcs == LA_PCS_SIMD && ISA_HAS_LSX)
+             return gen_rtx_REG (E_V2DFmode, fregno);
+           else
+             return loongarch_pass_fpr_pair (mode, fregno,
+                                            GET_MODE_INNER (mode), 0,
+                                            fregno + 1, GET_MODE_INNER (mode),
+                                            GET_MODE_UNIT_SIZE (mode));

          default:
            gcc_unreachable ();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants