ARM 中的 NEON 实现
Posted
技术标签:
【中文标题】ARM 中的 NEON 实现【英文标题】:NEON implementation in ARM 【发布时间】:2018-03-13 05:11:43 【问题描述】:我是 NEON 的初学者,想优化以下代码,但是当它编译并产生与期望相同的输出时,我没有看到任何改进。 AFAIK NEON 有助于对连续数据块进行操作,所以我希望在执行时间和周期方面有所改进。我做错了什么?
我正在 Ubuntu 12.04 上使用 -03 级别优化的 gcc
普通的c实现
for(i= 0;i<9215;i++)
Z[i] = (L[i]>0)?0:1;
霓虹灯形式
for(i=0;i<9215;i+=4)
int32x4_t l_N = vld1q_s32(&L[i]);
uint32x4_t mask_n=vcltq_s32(l_N,zero_N);
int32x4_t z_n = vbslq_s32(mask_n,one_N,zero_N);
vst1q_s32(&Z[i],z_n);
【问题讨论】:
你的例程没有引起异常几乎是一个奇迹。 9215 不是 4 的倍数。 而且计算部分效率也不高:vbsl
是一条相当昂贵的指令。你可以做一个饱和减法(vqsub
),类型转换为无符号,然后移位 31(vshr_u32
)。
【参考方案1】:
问题:
您正在使用一种非常低效的算法进行循环内的计算 您的例程遭受了繁重的流水线互锁,逐条指令void isNonNatural(int32_t * pDst, int32_t *pSrc, int n)
int32x4_t vec;
const int32x4_t one = vdupq_n_s32(1);
int32_t a;
unsigned int i;
if (n >= 4)
n -= 4;
while (1)
do
n -= 4;
vec = vld1q_s32(pSrc++);
vec = vqsubq_s32(vec, one);
vec = (int32x4_t) vshrq_n_u32((uint32x4_t) vec, 31);
vst1q_s32(pDst++, vec);
while (n >= 0);
if (n <= -4) return;
// dealing with residuals
pSrc += n; // rewind pointers
pDst += n;
// iterate for one last time
for (i = 0; i < n; ++i)
a = *pSrc++;
if (a > 0) a = 0; else a = 1;
*pDst++ = a;
上面的这个函数应该比你的实现要快一些。
执行饱和减法 1,使 0 变为 -1,而 0x80000000 仍为 0x80000000 元素被移动了 31 位,因此只保留了符号位。 我可以使用 0xffffffff 而不是 1,您可以省略类型转换并改用vshrq_n_s32
。不过也不会更快。
注意余量管理。
对 NEON 进行编程就像驾驶一辆大卡车。你不应该像开紧凑型车一样驾驶它。
虽然 NEON 可以一次计算多个数据,主要是在一个周期内,但它具有更高的指令延迟,通常为 3~4 个周期。换句话说,在上面的实现中,每条指令都必须等待上一条指令返回结果。
实际上,避免这种情况的唯一方法是展开,深入的。
void isNonNatural_unroll(int32_t * pDst, int32_t *pSrc, int n)
int32x4_t vec1, vec2, vec3, vec4;
const int32x4_t one = vdupq_n_s32(1);
int32_t a;
unsigned int i;
if (n >= 16)
n -= 16;
while (1)
do
n -= 16;
vec1 = vld1q_s32(pSrc++);
vec2 = vld1q_s32(pSrc++);
vec3 = vld1q_s32(pSrc++);
vec4 = vld1q_s32(pSrc++);
vec1 = vqsubq_s32(vec1, one);
vec2 = vqsubq_s32(vec2, one);
vec3 = vqsubq_s32(vec3, one);
vec4 = vqsubq_s32(vec4, one);
vec1 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec1, 31);
vec2 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec2, 31);
vec3 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec3, 31);
vec4 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec4, 31);
vst1q_s32(pDst++, vec1);
vst1q_s32(pDst++, vec2);
vst1q_s32(pDst++, vec3);
vst1q_s32(pDst++, vec4);
while (n >= 0);
if (n <= -16) return;
// dealing with residuals
pSrc += n; // rewind pointers
pDst += n;
// iterate for one last time
if (n & 8)
vec1 = vld1q_s32(pSrc++);
vec2 = vld1q_s32(pSrc++);
vec1 = vqsubq_s32(vec1, one);
vec2 = vqsubq_s32(vec2, one);
vec1 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec1, 31);
vec2 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec2, 31);
vst1q_s32(pDst++, vec1);
vst1q_s32(pDst++, vec2);
if (n & 4)
vec1 = vld1q_s32(pSrc++);
vec1 = vqsubq_s32(vec1, one);
vec1 = (int32x4_t) vshrq_n_u32((uint32x4_t) vec1, 31);
vst1q_s32(pDst++, vec1);
n &= 3;
for (i = 0; i < n; ++i)
a = *pSrc++;
if (a > 0) a = 0; else a = 1;
*pDst++ = a;
现在这个应该比以前的快很多,因为几乎所有的延迟都被隐藏了(快四倍多),前提是可怜的编译器不会把它搞砸。
【讨论】:
非常感谢您的意见,我花了 2 个小时来理解您的实现,但最后我确实理解并在我的代码中实现了它并进行了更改,但它大大增加了周期,我很抱歉给索引为 9215,实际上是 9216 加 1 用于承认编译器如何搞砸完美的内在函数 :) 是的,@BitBank 我认为你是对的,但现在我无法更改我的编译器。 @Skynet 我愿意为aarch32
和aarch64
提供完全优化的汇编版本(这是我的专长)。你会发布基准测试结果吗?
@Jake'Alquimista'LEE 非常感谢你的帮助,但我目前受到一些限制,我无法使用汇编语言,但我尝试了内联汇编,它适用于非常简单的事情喜欢浮动到固定的转换,所以是的,我肯定会给出基准。以上是关于ARM 中的 NEON 实现的主要内容,如果未能解决你的问题,请参考以下文章