项目记录:SSE/NEON快速指令集优化像素拷贝

SSE/NEON`快速指令集优化像素拷贝

项目需求:

  • 360全景图划分成 TILE.

  • 高纬度按 4:1 下采样. 中纬度按 2:1 下采样. 低纬度 1:1

  • 渲染时,高纬度按 1:4 还原. 中纬度按 1:2 还原.

做法:

SIMD,single instruction multiple data,单指令流多数据流,一次运算指令可以执行多个数据流.

使用指令集进行并行优化.

SSE指令集

SSE本质上类似于一个向量处理器,包括了4个主要部分

  • 单精确度浮点数运算指令
  • 整数运算指令(为MMX的延伸,并与MMX使用同样的暂存器)
  • Cache控制指令
  • 状态控制指令

Intrinsic使用的数据类型和其寄存器是相对应

  • 64位 MMX指令集使用
  • 128位 SSE指令集使用
  • 256位 AVX指令集使用

Intrinsic 函数的命名:

Intrinsic函数的命名也是有一定的规律的,一个Intrinsic通常由3部分构成,这个三个部分的具体含义如下:

  1. 第一部分为前缀_mm,表示是SSE指令集对应的Intrinsic函数。_mm256或_mm512是AVX,AVX-512指令集的Intrinsic函数前缀,这里只讨论SSE故略去不作说明。
  2. 第二部分为对应的指令的操作,如_add,_mul,_load等,有些操作可能会有修饰符,如loadu将未16位对齐的操作数加载到寄存器中。
  3. 第三部分为操作的对象名及数据类型,_ps packed操作所有的单精度浮点数;_pd packed操作所有的双精度浮点数;_pixx(xx为长度,可以是8,16,32,64)packed操作所有的xx位有符号整数,使用的寄存器长度为64位;_epixx(xx为长度)packed操作所有的xx位的有符号整数,使用的寄存器长度为128位;_epuxx packed操作所有的xx位的无符号整数;_ss操作第一个单精度浮点数。…

SSE浮点运算分为两大类, packedscalar

packed指令是一次对XMM暂存器中的四个浮点数(DATA0~DATA3)均进行计算,而scalar只对XMM暂存器中的DATA0进行计算,见下图。

项目记录:SSE/NEON快速指令集优化像素拷贝_第1张图片

NEON指令集

寄存器

ARMV7架构包含:

  • 16个通用寄存器(32bit),R0-R15

  • 16个NEON寄存器(128bit),Q0-Q15(同时也可以被视为32个64bit的寄存器,D0-D31)

  • 16个VFP寄存器(32bit),S0-S15

    NEON和VFP的区别在于VFP是加速浮点计算的硬件不具备数据并行能力,同时VFP更尽兴双精度浮点数(double)的计算,NEON只有单精度浮点计算能力。更多请参考stackoverflow:neon vs vfp

基本数据类型

64bit数据类型,映射至寄存器即为D0-D31

128bit数据类型,映射至寄存器即为Q0-Q15

结构化数据类型

下面这些数据类型是上述基本数据类型的组合而成的结构化数据类型,通常为被映射到多个寄存器中。

typedef struct int8x8x2_t
{
  int8x8_t val[2];
} int8x8x2_t;
...
//省略...
...
#ifdef __ARM_FEATURE_CRYPTO
typedef struct poly64x2x4_t
{
  poly64x2_t val[4];
} poly64x2x4_t;
#endif

基本指令:

NEON指令按照操作数类型可以分为正常指令、宽指令、窄指令、饱和指令、长指令。

  • 正常指令:生成大小相同且类型通常与操作数向量相同到结果向量。
  • 长指令:对双字向量操作数执行运算,生产四字向量到结果。所生成的元素一般是操作数元素宽度到两倍,并属于同一类型。L标记,如VMOVL。
  • 宽指令:一个双字向量操作数和一个四字向量操作数执行运算,生成四字向量结果。W标记,如VADDW。
  • 窄指令:四字向量操作数执行运算,并生成双字向量结果,所生成的元素一般是操作数元素宽度的一半。N标记,如VMOVN。
  • 饱和指令:当超过数据类型指定到范围则自动限制在该范围内。Q标记,如VQSHRUN

NEON指令按照作用可以分为:加载数据、存储数据、加减乘除运算、逻辑AND/OR/XOR运算、比较大小运算等

具体实现:

// 宏定义: 以空间换时间.. 小心使用..
// ---------PC端:SSE指令集----------------
// 高纬度的1:4 拷贝 4字节->16
#define set4(output_buf_y,input_buf_y) {\
	__m128i itmp1 = _mm_loadl_epi64((__m128i*)(input_buf_y));\
	__m128i itmp2 = _mm_unpacklo_epi8(itmp1, itmp1);\
	__m128i dst = _mm_unpacklo_epi8(itmp2, itmp2);\
	__m128i* otmp = (__m128i*)(output_buf_y);\
	_mm_store_si128(otmp, dst);\
}
// 中纬度的1:2 拷贝 8字节->16
#define set2(output_buf_y,input_buf_y) {\
	__m128i itmp =_mm_loadl_epi64((__m128i*)(input_buf_y));\
	__m128i* otmp = (__m128i*)(output_buf_y);\
	_mm_store_si128(otmp, _mm_unpacklo_epi8(itmp, itmp));\
}
// ---------Android端:NEON指令集----------------
#define NEON_INTRINSICS 1// 1:neon内联函数 0:汇编
#if NEON_INTRINSICS
//1:4 拷贝 8->32
#define set4(output_buf_y,input_buf_y) {\
        uint8x8x4_t v;\
        v.val[0] = vld1_u8(input_buf_y);\
        v.val[1] = v.val[0];\
        v.val[2] = v.val[0];\
        v.val[3] = v.val[0];\
        vst4_u8(output_buf_y,v);\
}
// 1:2 拷贝 8字节->16
#define set2(output_buf_y,input_buf_y) {\
        uint8x8x2_t v;\
        v.val[0] =vld1_u8(input_buf_y);\
        v.val[1] = v.val[0];\
        vst2_u8(output_buf_y,v);\
}
#else
// 汇编版本
void set4(unsigned char * output_buf_y,unsigned char * input_buf_y) {\
	__asm__(\
        "VLD1.8 {d0},[r1] \t\n"\
        "VMOV   d1,d0 \t\n"\
        "VMOV   d2,d0 \t\n"\
		"VMOV   d3,d0 \t\n"\
		"VST4.8 {d0, d1, d2, d3}, [r0]"\
	);\
}
void set2(unsigned char *output_buf_y,unsigned char *input_buf_y) {\
	__asm__(\
        "VLD1.8 {d0},[r1] \t\n"\
        "VMOV   d1,d0 \t\n"\
        "VST2.8 {d0, d1}, [r0]"\
	);\
}
#endif

备注

  • 使用宏定义.以空间换时间.

参考资料:

英特尔之SSE指令集指南

NEON各函数介绍

ARM NEON 编程系列

Android-NDK之Hello Neon

你可能感兴趣的:([研究生项目记录])