I have this (and similar) pieces of code where I expect umull2 or pmull2 instructions to be emitted. Compiler clang-11.0.1, options -O3 -std=c++11 -target aarch64
#include "stdint.h"
#include "arm_neon.h"
inline poly16x8_t vmull_low_p8(poly8x16_t a, poly8x16_t b) {
return vmull_p8(vget_low_p8(a), vget_low_p8(b));
}
// evaluates polynomials of length `len > 0` using Horner's method
// for 16 points `x`, `X = x 2^m mod n`
poly16x8x2_t p(const uint8_t *input, int len, poly8x16_t x, poly8x16_t X) {
auto ptr = input + len;
auto L = vdupq_n_p16(*--ptr), H = L;
while (ptr > input) {
auto s = vuzpq_p8(vreinterpretq_p8_p16(L), vreinterpretq_p8_p16(H));
auto a = vmull_low_p8(s.val[0], x);
auto b = vmull_high_p8(s.val[0], x);
auto A = vmull_low_p8(s.val[1], X);
auto B = vmull_high_p8(s.val[1], X);
auto C = vdupq_n_p16(*--ptr);
L = C ^ a ^ A;
H = C ^ b ^ B;
}
return {L,H};
}
Instead of having code like
ldrb w9, [x8, #-1]!
uzp1 v6.16b, v2.16b, v3.16b
uzp2 v2.16b, v2.16b, v3.16b
pmull v3.8h, v6.8b, v0.8b
pmull2 v7.8h, v6.8b, v0.8b
pmull v6.8h, v6.8b, v4.8b
pmull2 v2.8h, v2.8b, v4.8b
the compiler has chosen to
ldrb w9, [x8, #-1]!
uzp1 v6.16b, v2.16b, v3.16b
uzp2 v2.16b, v2.16b, v3.16b
pmull v3.8h, v6.8b, v0.8b
ext v6.16b, v6.16b, v6.16b, #8 // aligns top 8 bytes over bottom 8 bytes
pmull v7.8h, v2.8b, v1.8b // uses a copy of v0.8 also shifted by 8 bytes
ext v2.16b, v2.16b, v2.16b, #8
pmull v6.8h, v6.8b, v4.8b
pmull v2.8h, v2.8b, v5.8b
Working on uint8x16_t or uint16x8_t does not matter -- and this seems to be a recurring theme of clang avoiding the top bits of a register to be accessed. I haven't found a -mtune=cortex-73 kind of an option that would force a different code generation, so I could at least assess if this is generally less performant (which I expect it to be on little cores). ARM64 gcc works as expected.
This may not matter (except for size wise and having less registers allocated) if the ext instructions are known to double-issue with pmull where as two pmulls would not.