riscv-non-isa/rvv-intrinsic-doc

[Proposal] Support for C operators on RVV types

Opened this issue ยท 12 comments

Now the only way to do calculation on RVV sizeless type is using the intrinsic. By example, if we want to add 2 whole scalable vector, we should use the following code:

vint16m1_t op1;
vint16m1_t op2;
vint16m1_t op3 = __riscv_vadd(op1, op2, -1);

My proposal is that we support using C operators (such as: arithmetic, bitwise and subscript) on RVV sizeless types. Then we could change to the following code:

vint16m1_t op1;
vint16m1_t op2;
vint16m1_t op3 = op1 + op2;

I think that using operators instead of builtin is more clear and would enable more ir/backend optimizations.

The C operators that should be supported includes the following classes:
arithmetic operators: +, -, *, /, %;
bitwise operators: &, |, ^;
compare operators: ==, !=, >, >=, <, <=;
shift operators: <<, >>;
subscript operators: [].

Now clang already supports some C operators for SVE sizeless types. I have a patch to support C operators for RVV sizeless types, https://reviews.llvm.org/D158259.

But how to implement C operators with mask?

But how to implement C operators with mask?

... And how to control mask policy?

Or how to use flexible VL? And how to control tail policy?

The proposed semantics are unmasked, with VL = VLMAX, which resolves these questions, but requires users who need those features to use intrinsics.

If a ? b : c was supported (as gcc and clang do for fixed-length vectors, ...though only in C++), or some similar thing for a blend/merge, __riscv_vadd_vv_i32m1_mu(mask, vd, op1, op2, -1) could be mask ? op1+op2 : vd. (and fwiw, clang already can convert a __riscv_vmerge to masking a source operand)

For arithmetic operations (i.e. all things proposed here), tail-agnostic and mask-agnostic aren't really strictly necessary; the compiler could infer a smaller VL from uses/argument sources. e.g. these are valid transformations:

__riscv_vse32(ptr, __riscv_vadd(a, b, VLMAX), 2); // == proposed __riscv_vse32(ptr, a+b, 2);
โ†“
__riscv_vse32(ptr, __riscv_vadd(a, b, 2), 2);

vint32m1_t vec = __riscv_vadd(__riscv_vle32_v_i32m1(ptr, 2), b, VLMAX); // == proposed __riscv_vle32_v_i32m1(ptr, 2) + b
โ†“
vint32m1_t vec = __riscv_vadd(__riscv_vle32_v_i32m1(ptr, 2), b, 2);

and mask-agnostic afaik isn't really useful for arithmetic at all other than a hint (ignoring FP exceptions/vxsat, which should be rarely needed and isn't even supported by existing intrinsics yet anyway), and thus are replaceable with like mask ? op1+op2 : __riscv_vundefined_i32m1() if really desired for whatever reason.

There'll still be some cases where VL control is required (tail-undisturbed is a thing that's not easily replaced; I guess there's like __riscv_vmv_v(vd, a+b, vl)?), but it could still be worth having the C operators for when that's not the case.

The backend of compiler (not sure about gcc) has ability to transform mask ? op1+op2 : vd into masked instruction. So with implemention of the conditional expression, I think we could handle the masked cases.

For vl, I also agree with @dzaima. One of motivation of this proposal is simplifying the syntax of vector calculation when vl=vlmax. For the cases that still demand vl, I think we could keep using intrinsics.

@jacquesguan thanks for raising this, this topic has raise long time ago, however it has stop for a while since we've discuss around explicitly VL or implicitly VL at that moment, but now we are settle down with the explicitly VL for a while so it's kind of obviously to define those operator as VLMAX semantics for now.

Anyway I want to express I am support this in general, and here is few comments around different things:

  • Unary operators like -, ! and ~. ++ and -- may just ignore.
  • How about vbool*_t? I guess we could support bitwise operators, also ! and/or ~.

If a ? b : c was supported (as gcc and clang do for fixed-length vectors, ...though only in C++),

I support this, it's customized type, so I think we are not necessary has same limitation, I mean we could support that for both C and C++.


Also I would like to add few more syntax sugar around tuple type:

  1. Brace initialization for tuple type (syntax sugar for vcreate)
vint32m1_t x, y;
vint32m1x2_t a = {x, y};
  1. subscript operators (syntax sugar for vget/vset)
vint32m1_t x, y;
vint32m1x2_t a;
a[1] = x;
y = a[1];

I've tried a similar approach using C++ template classes and operators, here is some example I achieved:

All that required is to enable rvv intrinsic types to be contained in regular C++ class/struct (which is not true for now, but I tried all the code below with modified riscv g++ from gcc 12) , then rest of the work is all done by C++ grammar features, no further compiler frontend support needed.

basic types

rvv_vector<uint32_t, m1> v0;
rvv_vector<uint64_t, m4> v1;
rvv_vector<uint16_t, mf2> v2;
rvv_vector<uint8_t, mf8> v3;
rvv_mask<8> vm1;
rvv_mask<64> vm2;

vlen setting:

size_t vl = rvv_vector<uint16_t, m2>::vsetvlmax(); // set max vl according to vtype and lmul
set_global_vl(13); // set global vl, all operators below will use it, until set vl again or specified vl in specific op
v3.store(data, vl); // certain functions can have specified vl
//most operators can only use global vl, since there is not enough slot

load store

uint16_t data[512] = {...}; //
rvv_vector<uint16_t, m2> v1, v2, v3;
v1.load(data);
vl = 15;
v1.load(data, vl);
v1.load_index_od(data, index, vl); // variations of load instrinsics
v1 = 12;
v3.store(data, vl);

arithmetic

rvv_vector<uint32_t, m1> vindex, v0, v1, v2, v3, v4, v5;
vindex.index(); // index operator, v[i] = i
v1 = 1; // v1[i] = C for all i = {0, 1, 2, ...}
v2 = vindex % 2; // v2[i] = vindex[i] % 2
v3 = vindex + v2; 
v3 += 3;
v4 = v2*v3;
v5 *= v4<<1; 
v6 = v2 & v5; // v6[i] = v2[i] & v5[i] for i = {0, 1, 2, 3, ...}
v6 = v1+v2-v3*v4; // conbinations

logical and mask operations, commonly used syntax in math libs like numpy

rvv_vector<uint32_t, m1> v0, v1, v2, v3;
vindex.index(); // vindex = {0,1,2,3, ... }
rvv_mask<8> vm1 = vindex==1; //  vm1[i] = 1 if i==1, else 0
auto vm2 = vindex%1!=0; // vm2[i] = 1 if i = 1,3,5,...
auto vm3 = vindex>5; // vm3[i]=1 if i>5
auto vm4 = v1&&v2 || vm3; // logic operations
v2 = v1[vm1]; // masked assignment
v2[vm1] = v1; // same effect as above
v2 = v1[vm4] + v0; // v2[i] = v1[i]+v2[i] if vm4[i]==1, else v2[i]
v3 = v1[vindex>1 && vindex<5 || vindex>8] * v2; // combinations

widen/narrow op:

rvv_vector<uint8_t, m1> v0, v1, v2;
auto v1 = v0.widen_op() + 1 // rvv_vector<uint16_t, m2>
auto v2 = v1.narrow_op(); // rvv_vector<uint8_t, m1>
rvv_vector<uint64_t, m8> v4;
auto v5 = v4.narrow_op() + 1; // error, uint8 can't be narrowed
auto v6 = v5.widen_op() * 1; // error, m8 can't be widdend

Just a quick +1 from Highway, we'd love to have operators. Currently we're requiring user code to write Div() or MaskedDivOr(), it would be nice to avoid that.

I agree with dzaima, _mu is the only thing we'd use (why use masks otherwise?). And VLMAX is also fine, one can also use masks for tail/remainder handling.

With @kito-cheng 's comment, let me conclude the operators we would support for RVV:

For RVV sizeless vector type:
unary operators:
-, +
!, ~
++, --

binary operators:
arithmetic operators: +, -, *, /, %;
bitwise operators: &, |, ^;
compare operators: ==, !=, >, >=, <, <=;
shift operators: <<, >>;

subscript operators: [].

ternary operatros : ?

For RVV sizeless mask vector type:
Only support:
!, ~, &, |, ^;

For RVV sizeless vector tuple type:
brace initialization {}
subscript operators []

I see subscript operators for non-tuple types too, do you intend to propose declaration for an array of RVV non-tuple type?

I see subscript operators for non-tuple types too, do you intend to propose declaration for an array of RVV non-tuple type?

Similar to fixed length vectors, subscript operator for sizeless vectors means getting the i th element from the vector. By example, v[i] will lower to extractelement <vscale x n x ty> v, i64 i in LLVM.

As I mentioned in #13, if you use the architecture-independent vector extension in Clang then you can use C operators for the bulk of your code and dip into intrinsics when you need to do something esoteric.

I'll just repeat that here, because I don't want existing functionality to be overlooked and duplicated.

Trouble is, that extension forces you to specify the size of the vector at compile time, and it picks RVV types which it thinks can do the job; setting VL appropriately for portable code. That's where I think an extension would be needed -- the ability to make that vector type sizeless.

And also it doesn't work in GCC.