おぺんcv

画像処理エンジニアのブログ

ARM NEONの使い方 減算編

年内に終わるかな?
今回は減算編です

減算 (通常の減算、符号拡張付き減算、飽和付き減算)

加算編で紹介したものとほぼ変わらないので、まとめて紹介

vsub[q]_<type>(va, vb)

64bit(qが付く場合は128bit)のベクタvaとvbの引き算

vsubl_<type>(va, vb)

64bitのベクタvaとvbの各レーンのbit幅を倍に拡張して引き算

vsubq[q]_<type>(va, vb)

64bit(qが付く場合は128bit)のベクタvaとvbの引き算
演算結果がオーバーフローする場合は最大値/最小値で飽和させます

サンプル

符号付き16bit整数のベクタvaとvbの引き算を、先ほど紹介した3つの方法でやってみます

#include <stdio.h>
#include <stdint.h>
#include <arm_neon.h>

int main()
{
	int16_t a[4] = { 1, -1, 1, -2 };
	int16_t b[4] = { 1, -1, -32767, 32767 };

	int16x4_t va = vld1_s16(a);
	int16x4_t vb = vld1_s16(b);

	int16x4_t vc = vsub_s16(va, vb);
	int32x4_t vcl = vsubl_s16(va, vb);
	int16x4_t vcq = vqsub_s16(va, vb);

	printf("vsub_s16\n");
	int16_t c[4];
	vst1_s16(c, vc);
	for (int i = 0; i < 4; i++)
		printf("c[%d]: %d\n", i, c[i]);

	printf("\nvsubl_s16\n");
	int32_t cl[4];
	vst1q_s32(cl, vcl);
	for (int i = 0; i < 4; i++)
		printf("cl[%d]: %d\n", i, cl[i]);

	printf("\nvqsub_s16\n");
	int16_t cq[4];
	vst1_s16(cq, vcq);
	for (int i = 0; i < 4; i++)
		printf("cq[%d]: %d\n", i, cq[i]);


	return 0;
}
実行結果
vsub_s16
c[0]: 0
c[1]: 0
c[2]: -32768
c[3]: 32767

vsubl_s16
cl[0]: 0
cl[1]: 0
cl[2]: 32768
cl[3]: -32769

vqsub_s16
cq[0]: 0
cq[1]: 0
cq[2]: 32767
cq[3]: -32768

3、4番目のレーンの引き算の結果がそれぞれ

  • vsub_s16()ではオーバーフロー
  • vsubl_s16()では32bitに拡張
  • vqsub_s16()では最大値/最小値で飽和

となっています

符号なしベクタの減算について

符号なし16bit整数のベクタvaとvbの減算を考えます
このときvsub_u16()を使ってしまうと、戻り値も符号なし16bitなので
va[i] >= vb[i]の場合は正しい結果が得られるのですが
va[i] < vb[i]の場合はオーバーフローが発生してしまいます

正しい結果を得るためには、ベクタを符号付き32bitに拡張する必要がありますが、
これを1回で実現してくれるNEON命令はなさそうです

そこで1つ思いついたのが、vsubl_u16()を使用して減算結果を符号なし32bitに拡張し
vreinterpretq_s32_u32()で符号付き32bitとして解釈する方法です

サンプル

符号なし16bit整数のベクタvaとvbの引き算を、vsub_u16()を使った方法と
vsubl_u16() & vreinterpretq_s32_u32()を使った方法でやってみます

#include <stdio.h>
#include <stdint.h>
#include <arm_neon.h>

int main()
{
	uint16_t a[4] = { 1, 1, 1, 1 };
	uint16_t b[4] = { 0, 1, 2, 65535 };

	uint16x4_t va = vld1_u16(a);
	uint16x4_t vb = vld1_u16(b);
	uint16x4_t vc = vsub_u16(va, vb);

	uint32x4_t vc_u32 = vsubl_u16(va, vb);
	int32x4_t vc_s32 = vreinterpretq_s32_u32(vc_u32);

	printf("vsub_u16\n");
	uint16_t c[4];
	vst1_u16(c, vc);
	for (int i = 0; i < 4; i++)
		printf("c[%d]: %d\n", i, c[i]);

	printf("\nvsubl_u16 and vreinterpretq_s32_u32\n");
	int c_s32[4];
	vst1q_s32(c_s32, vc_s32);
	for (int i = 0; i < 4; i++)
		printf("c_s32[%d]: %d\n", i, c_s32[i]);

	return 0;
}
vsub_u16
c[0]: 1
c[1]: 0
c[2]: 65535
c[3]: 2

vsubl_u16 and vreinterpretq_s32_u32
c_s32[0]: 1
c_s32[1]: 0
c_s32[2]: -1
c_s32[3]: -65534

vsubl_u16() & vreinterpretq_s32_u32()を使った方法で、一応正しい結果が得られました
やり方として良いのかわかりませんが…

次回

次回は乗算編です
ARM NEONの使い方 乗算編