BitTigerio 2018-03-15
单指令多数据流,即SIMD(Single Instruction, Multiple Data)指一类能够在单个指令周期内同时处理多个数据元素的指令集,利用的是数据级并行来提高运行效率,典型的代表由Intel的MMX和SSE指令系列。这类指令的使用环境是对多个数据进行同一种处理,因此典型的应用场景就是多媒体领域,特别是在其中的编解码流程中。
MMX指令有8个64位寄存器(MM0~MM7),但MMX实际上并没有硬件支持的新寄存器,它使用浮点寄存器来模拟MMX指令寄存器。
当使用MMX指令的时候,一个叫做FP(Floating Point) Tag 的Word(2字节)被用来映射浮点寄存器到MMX寄存器。这样浮点寄存器就成了MMX寄存器的容器,用来执行计算。从浮点指令切换到MMX指令实在处理器内部完成的,不需要人为的操作;相反,从MMX切换到浮点指令时,需要手动调用emms或者**__mm_empty()**Intrinsics。
MMX指令与x86指令类似可以分为几类,具体使用及介绍可以参考Oracle的手册,这里不再重复介绍:
我们可以用通常的汇编嵌入方式在C/C++代码中调用mmx指令,但是这样一来C/C++开发者可能不是很习惯,尤其是它们没有接触过汇编语言的情况下;Intel提供了另一种方式来供开发者选择----Compiler Intrinsics。
Compiler Intrinsics是内建在编译器里的函数,Intrinsics通常会以汇编代码的形式被内联到代码中且具有较高的执行效率,因为编译器知道intrinsics的表现,相比内嵌汇编代码编译器能做更多的优化。
同时,Intrinsics的使用方式是停留在宿主语言层的,所以C语言(通常情况下)相比汇编语言拥有的所有优点,Intrinsics都有(比如我可以对Intrinsics数据类型做类型单位的递增递减)。
我们这里分别简单测试C++、Intrinsics(使用MMX)、Asm(使用MMX)三种形式代码的执行效率,示例中我们分别对内存中的100 000 000个字节进行加
算数运算:
void calculateUsingCpp(char* data, unsigned size) { assert(size % 8 == 0); unsigned step = 10; for (unsigned i = 0; i < size; ++i) { *data++ += step; } }
Intrinsics代码中,我们每次执行mmx Intrinsics时都打包8个字节的数据并执行加操作,执行完mmx指令后我们需要调用**_mm_empty()** Intrinsics来取消mmx指令对浮点寄存器的别名映射:
void calculateUsingIntrinsics(char* data, unsigned size) { assert(size % 8 == 0); __m64 step = _mm_set_pi8(10, 10, 10, 10, 10, 10, 10, 10); __m64* dst = reinterpret_cast<__m64*>(data); for (unsigned i = 0; i < size; i += 8) { auto sum = _mm_adds_pi8(step, *dst); *dst++ = sum; } _mm_empty(); }
Intel汇编语法在嵌入到高级语言代码中时可以直接使用上下文中的变量,这一点非常方便:
void calculateUsingAsm(char* data, unsigned size) { assert(size % 8 == 0); unsigned loopCount = size / 8; __int64 value = 0x0a0a0a0a0a0a0a0a; __asm { push eax push ecx mov eax, data mov ecx, loopCount movq mm1, value startLoop: movq mm0, [eax] paddb mm0, mm1 movq [eax], mm0 add eax, 8 dec ecx jnz startLoop emms pop ecx pop eax } }
可以看出运行时间比是 8 : 1.5 : 1左右,完整代码见链接。