ARM64-SVE: `TestFirstTrue()` can be optimised away when using `CreateTrueMask()`

Open
#108,392 4 comments 0 reactions 1 assignee View on GitHub

@a74nh is already working on this.

Since May 22, 2025.

Assessment

This issue has not been assessed yet.

Description

arch-arm64 area-CodeGen-coreclr arm-sve Priority:2

Consider the C++ code:

int16_t mutiplyadd(int16_t *a, int16_t *b, int length)
{
  svint16_t res_vec = svdup_s16(0);
  svbool_t ploop;
  for (int i = 0; svptest_first(svptrue_b16(), ploop = svwhilelt_b16(i, length)); i+= (int)svcnth())
  {
    svint16_t a_vec = svld1(ploop, a + i);
    svint16_t b_vec = svld1(ploop, b + i);
    res_vec = svmla_m(ploop, res_vec, a_vec, b_vec);
  }

  return svaddv(svptrue_b16(), res_vec);
}

Compiled with gcc -O3, this gives:

0000000000000000 <_Z10mutiplyaddPsS_i>:
   0:	256207e2 	whilelt	p2.h, wzr, w2
   4:	2558e3e1 	ptrue	p1.h
   8:	25824840 	mov	p0.b, p2.b
   c:	2550c440 	ptest	p1, p2.b
  10:	54000265 	b.pl	5c <_Z10mutiplyaddPsS_i+0x5c>  // b.nfrst
  14:	0460e3e5 	cnth	x5
  18:	52800003 	mov	w3, #0x0                   	// #0
  1c:	937f7ca4 	sbfiz	x4, x5, #1, #32
  20:	2538c000 	mov	z0.b, #0
  24:	d503201f 	nop
  28:	a4a0a002 	ld1h	{z2.h}, p0/z, [x0]
  2c:	a4a0a021 	ld1h	{z1.h}, p0/z, [x1]
  30:	0b050063 	add	w3, w3, w5
  34:	04414040 	mla	z0.h, p0/m, z2.h, z1.h
  38:	25620461 	whilelt	p1.h, w3, w2
  3c:	8b040000 	add	x0, x0, x4
  40:	8b040021 	add	x1, x1, x4
  44:	25814420 	mov	p0.b, p1.b
  48:	54ffff04 	b.mi	28 <_Z10mutiplyaddPsS_i+0x28>  // b.first
  4c:	2518e3e0 	ptrue	p0.b
  50:	04402000 	saddv	d0, p0, z0.h
  54:	9e660000 	fmov	x0, d0
  58:	d65f03c0 	ret
  5c:	2518e3e0 	ptrue	p0.b
  60:	2538c000 	mov	z0.b, #0
  64:	04402000 	saddv	d0, p0, z0.h
  68:	9e660000 	fmov	x0, d0
  6c:	d65f03c0 	ret

Note the C++ has a pfirst instruction but it is not in the assembly. It has been optimised away. whilelt sets the flags and then there is a branch via b.mi (same as b.first).

Consider the same in C#:

public static unsafe long mutiplyadd(ref ushort* a, ref ushort* b, int length)
{
  Vector<ushort> res = Vector<ushort>.Zero;
  Vector<ushort> ploop;

  for (int i = 0; Sve.TestFirstTrue(Sve.CreateTrueMaskInt16(), ploop = (Vector<ushort>)Sve.CreateWhileLessThanMask16Bit(i, length)); i+= (int)Sve.Count16BitElements())
  {
    Vector<ushort> a_vec = Sve.LoadVector((Vector<ushort>)ploop, a+i);
    Vector<ushort> b_vec = Sve.LoadVector((Vector<ushort>)ploop, b+i);
    res = Sve.ConditionalSelect((Vector<ushort>)ploop, Sve.MultiplyAdd(res, a_vec, b_vec), res);
  }

  return Sve.AddAcross(res).ToScalar();
}
G_M65150_IG01:  ;; offset=0x0000
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
						;; size=8 bbWeight=1 PerfScore 1.50
G_M65150_IG02:  ;; offset=0x0008
            movi    v16.4s, #0
            mov     w3, wzr
            mov     w4, wzr
            whilelt p0.h, w4, w2
            mov     z17.h, p0/z, #1
            ptrue   p0.h
            ptrue   p1.h
            cmpne   p1.h, p1/z, z17.h, #0
            ptest   p0, p1.b
            bge     G_M65150_IG05
						;; size=40 bbWeight=1 PerfScore 15.50
G_M65150_IG03:  ;; offset=0x0030
            ldr     x0, [x0]
            cnth    x4, all
            align   [4 bytes for IG04]
            align   [4 bytes]
            align   [0 bytes]
            align   [0 bytes]
						;; size=16 bbWeight=0.25 PerfScore 1.50
G_M65150_IG04:  ;; offset=0x0040
            ptrue   p1.h
            cmpne   p1.h, p1/z, z17.h, #0
            sbfiz   x5, x3, #1, #32
            add     x6, x0, x5
            ld1h    { z17.h }, p1/z, [x6]
            ldr     x6, [x1]
            add     x5, x6, x5
            ld1h    { z18.h }, p1/z, [x5]
            mla     z16.h, p1/m, z17.h, z18.h
            add     w3, w3, w4
            whilelt p1.h, w3, w2
            mov     z17.h, p1/z, #1
            ptrue   p1.h
            cmpne   p1.h, p1/z, z17.h, #0
            ptest   p0, p1.b
            blt     G_M65150_IG04
						;; size=64 bbWeight=4 PerfScore 170.00
G_M65150_IG05:  ;; offset=0x0080
            ptrue   p0.h
            saddv   d16, p0, z16.h
            umov    x0, v16.d[0]
						;; size=12 bbWeight=1 PerfScore 6.00
G_M65150_IG06:  ;; offset=0x008C
            ldp     fp, lr, [sp], #0x10
            ret     lr
						;; size=8 bbWeight=1 PerfScore 2.00

As expected there is a pfirst instruction. This could be folded into the whilelt the same as C++.

(Issues to fix the extra mask conversions and the load addressing modes have already been raised.)

Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/runtime

All issues in dotnet/runtime

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.