天行健 发表于 2024-12-23 20:09:02

C++反汇编入门21.单指令多数据流(SIMD)

# SIMD

SIMD是Single Instruction, Multiple Data的首字母。简单说就是单指令多数据流。

就像FPU,FPU看起来更像独立于x86处理器。

SIMD开始于MMX x86。8个新的64位寄存器MM0-MM7被添加。

每个MMX寄存器包含2个32-bit值/4个16-bit值/8字节。比如可以通过一次添加两个值到MMX寄存器来添加8个8-bit(字节)。

一个简单的例子就是图形编辑器,将图像表示为一个二维数组,当用户改变图像的亮度,编辑器必须添加每个像素的差值。为了简单起见,将每个像素定义为一个8位字节,就可以同时改变8个像素的亮度。

当使用MMX的时候,这些寄存器实际上位于FPU寄存器。所以可以同时使用FPU和MMX寄存器。有人可能会认为,intel基于晶体管保存,事实上,这种共生关系的原因是:老的操作系统不知道额外的CPU寄存器,上下文切换是不会保存这些寄存器,可以节省FPU寄存器。这样激活MMX的CPU+旧的操作系统+利用MMX特性的处理器=所有一起工作。

SSE-SIMD寄存器扩展至128bits,独立于FPU。

AVX-另一种256bits扩展。

实际应用还包括内存复制(memcpy)和内存比较(memcmp)等等。

一个例子是:DES加密算法需要64-bits block,56-bits key,加密块生成64位结果。DES算法可以认为是一个非常大的电子电路,带有网格和AND/OR/NOT门。

Bitslice DES2—可以同时处理块和密钥。比如说unsigned int类型变量在X86下可以容纳32位,因此,使用64+56 unsigned int类型的变量,可以同时存储32个blocks-keys对。

我写了一个爆破Oracle RDBMS密码/哈希(基于DES)的工具。稍微修改了DES算法(SSE2和AVX)现在可以同时加密128或256block-keys对。

[http://conus.info/utils/ops_SIMD/](http://conus.info/utils/ops_SIMD/)

## 22.1 Vectorization

向量化3,例如循环用两个数组生成一个数组。循环体从输入数组中取值,处理后存储到另一个数组。重要的一点是操作了每一个元素。向量化—同时处理多个元素。

向量化并不是新的技术:本书的作者在1998年使用Cray Y-MP EL“lite”时从Cray Y-MP supercomputer line看到过。

例子:

```
for (i = 0; i < 1024; i++)
{
    C = A*B;
}
```

这段代码从A和B中取出元素,相乘,并把结果保存到C。

如果每个元素为32位int型,那么可以从A中加载4个元素到128bits XMM寄存器,B加载到另一个XMM寄存器,通过执行PMULID(Multiply Packed Signed Dword Integers and Store Low Result)和PMULHW(Multiply Packed Signed Integers and Store High Result),一次可以得到4个64位结果。

循环次数从1024变成1024/4,当然更快。

一些简单的情况下某些编译器可以自动向量化,Intel C++5.

函数如下:

```
int f (int sz, int *ar1, int *ar2, int *ar3)
{
      for (int i=0; i<sz; i++)
                ar3=ar1+ar2;
      return 0;
};
```

### 22.1.1 Intel C++

Intel C++ 11.1.051 win32下编译:

`icl intel.cpp /QaxSSE2 /Faintel.asm /Ox`

可以得到(IDA中):

```
; int __cdecl f(int, int *, int *, int *)
                public ?f@@YAHHPAH00@Z
?f@@YAHHPAH00@Z proc near

var_10          = dword ptr -10h
sz            = dword ptr 4
ar1             = dword ptr 8
ar2             = dword ptr 0Ch
ar3             = dword ptr 10h
                push    edi
                push    esi
                push    ebx
                push    esi
                mov   edx,
                test    edx, edx
                jle   loc_15B
                mov   eax,
                cmp   edx, 6
                jle   loc_143
                cmp   eax,
                jbe   short loc_36
                mov   esi,
                sub   esi, eax
                lea   ecx, ds:0
                neg   esi
                cmp   ecx, esi
                jbe   short loc_55

loc_36:                                       ; CODE XREF: f(int,int *,int *,int *)+21
                cmp   eax,
                jnb   loc_143
                mov   esi,
                sub   esi, eax
                lea   ecx, ds:0
                cmp   esi, ecx
                jb      loc_143
loc_55: ; CODE XREF: f(int,int *,int *,int *)+34
                cmp   eax,
                jbe   short loc_67
                mov   esi,
                sub   esi, eax
                neg   esi
                cmp   ecx, esi
                jbe   short loc_7F

loc_67:                                          ; CODE XREF: f(int,int *,int *,int *)+59
                cmp   eax,
                jnb   loc_143
                mov   esi,
                sub   esi, eax
                cmp   esi, ecx
                jb      loc_143

loc_7F:                                          ; CODE XREF: f(int,int *,int *,int *)+65
                mov   edi, eax ; edi = ar1
                and   edi, 0Fh ; is ar1 16-byte aligned?
                jz      short loc_9A ; yes
                test    edi, 3
                jnz   loc_162
                neg   edi
                add   edi, 10h
                shr   edi, 2

loc_9A:                                          ; CODE XREF: f(int,int *,int *,int *)+84
                lea   ecx,
                cmp   edx, ecx
                jl      loc_162
                mov   ecx, edx
                sub   ecx, edi
                and   ecx, 3
                neg   ecx
                add   ecx, edx
                test    edi, edi
                jbe   short loc_D6
                mov   ebx,
                mov   , ecx
                mov   ecx,
                xor   esi, esi

loc_C1:                                          ; CODE XREF: f(int,int *,int *,int *)+CD
                mov   edx,
                add   edx,
                mov   , edx
                inc   esi
                cmp   esi, edi
                jb      short loc_C1
                mov   ecx,
                mov   edx,

loc_D6:                                           ; CODE XREF: f(int,int *,int *,int *)+B2
                mov   esi,
                lea   esi, ; is ar2+i*4 16-byte aligned?
                test    esi, 0Fh
                jz      short loc_109 ; yes!
                mov   ebx,
                mov   esi,

loc_ED:                                           ; CODE XREF: f(int,int *,int *,int *)+105
                movdquxmm1, xmmword ptr
                movdquxmm0, xmmword ptr ; ar2+i*4 is not 16-byte aligned, so load
                it to   xmm0
                paddd   xmm1, xmm0
                movdqaxmmword ptr , xmm1
                add   edi, 4
                cmp   edi, ecx
                jb      short loc_ED
                jmp   short loc_127
; ---------------------------------------------------------------------------
loc_109:                                          ; CODE XREF: f(int,int *,int *,int *)+E3
                mov   ebx,
                mov   esi,
loc_111:                                          ; CODE XREF: f(int,int *,int *,int *)+125
                movdquxmm0, xmmword ptr
                paddd   xmm0, xmmword ptr
                movdqaxmmword ptr , xmm0
                add   edi, 4
                cmp   edi, ecx
                jb      short loc_111

loc_127:                                          ; CODE XREF: f(int,int *,int *,int *)+107
                                                ; f(int,int *,int *,int *)+164
                cmp   ecx, edx
                jnb   short loc_15B
                mov   esi,
                mov   edi,

loc_133: ; CODE XREF: f(int,int *,int *,int *)+13F
                mov   ebx,
                add   ebx,
                mov   , ebx
                inc   ecx
                cmp   ecx, edx
                jb      short loc_133
                jmp   short loc_15B
; ---------------------------------------------------------------------------
loc_143:                                          ; CODE XREF: f(int,int *,int *,int *)+17
                                                ; f(int,int *,int *,int *)+3A ...
                mov   esi,
                mov   edi,
                xor   ecx, ecx

loc_14D:                                          ; CODE XREF: f(int,int *,int *,int *)+159
                mov   ebx,
                add   ebx,
                mov   , ebx
                inc   ecx
                cmp   ecx, edx
                jb      short loc_14D

loc_15B:                                          ; CODE XREF: f(int,int *,int *,int *)+A
                                                ; f(int,int *,int *,int *)+129 ...
                xor   eax, eax
                pop   ecx
                pop   ebx
                pop   esi
                pop   edi
                retn
; ---------------------------------------------------------------------------
loc_162:                                           ; CODE XREF: f(int,int *,int *,int *)+8C
                                                   ; f(int,int *,int *,int *)+9F
                xor   ecx, ecx
                jmp   short loc_127
?f@@YAHHPAH00@Z endp
```

SSE2相关指令是:

```
MOVDQU (Move Unaligned Double Quadword)—仅仅从内存加载16个字节到XMM寄存器。
PADDD (Add Packed Integers)—把源存储器与目的寄存器按双字对齐无符号整数普通相加,结果送入目的寄存器,内存变量必须对齐内存16字节.
MOVDQA (Move Aligned Double Quadword)—把源存储器内容值送入目的寄存器,当有m128时,必须对齐内存16字节.
```

如果工作元素超过4对,并且指针ar3按照16字节对齐,SSE2指令将被执行: 如果ar2按照16字节对齐,则代码如下:

```
movdquxmm0, xmmword ptr ; ar1+i*4
paddd   xmm0, xmmword ptr ; ar2+i*4
movdqaxmmword ptr , xmm0 ; ar3+i*4
```

否则,ar2处的值将用MOVDQU加载到XMM0,它不需要对齐指针,代码如下:

```
movdquxmm1, xmmword ptr ; ar1+i*4
movdquxmm0, xmmword ptr ; ar2+i*4 is not 16-byte aligned, so load it to xmm0
paddd   xmm1, xmm0
movdqaxmmword ptr , xmm1 ; ar3+i*4
```

其他情况,将没有SSE2代码被执行。

### 22.1.2 GCC

gcc用-O3 选项同时打开SSE2支持: -msse2.

可以得到(GCC 4.4.1):

```
; f(int, int *, int *, int *)
                public _Z1fiPiS_S_
_Z1fiPiS_S_   proc near

var_18          = dword ptr -18h
var_14          = dword ptr -14h
var_10          = dword ptr -10h
arg_0         = dword ptr 8
arg_4         = dword ptr 0Ch
arg_8         = dword ptr 10h
arg_C         = dword ptr 14h
                push    ebp
                mov   ebp, esp
                push    edi
                push    esi
                push    ebx
                sub   esp, 0Ch
                mov   ecx,
                mov   esi,
                mov   edi,
                mov   ebx,
                test    ecx, ecx
                jle   short loc_80484D8
                cmp   ecx, 6
                lea   eax,
                ja      short loc_80484E8

loc_80484C1:                  ; CODE XREF: f(int,int *,int *,int *)+4B
                              ; f(int,int *,int *,int *)+61 ...
                xor   eax, eax
                nop
                lea   esi,
loc_80484C8:                  ; CODE XREF: f(int,int *,int *,int *)+36
                mov   edx,
                add   edx,
                mov   , edx
                add   eax, 1
                cmp   eax, ecx
                jnz   short loc_80484C8

loc_80484D8:                  ; CODE XREF: f(int,int *,int *,int *)+17
                              ; f(int,int *,int *,int *)+A5
                add   esp, 0Ch
                xor   eax, eax
                pop   ebx
                pop   esi
                pop   edi
                pop   ebp
                retn
; ---------------------------------------------------------------------------
                align 8
loc_80484E8:                  ; CODE XREF: f(int,int *,int *,int *)+1F
                test    bl, 0Fh
                jnz   short loc_80484C1
                lea   edx,
                cmp   ebx, edx
                jbe   loc_8048578

loc_80484F8:                  ; CODE XREF: f(int,int *,int *,int *)+E0
                lea   edx,
                cmp   ebx, edx
                ja      short loc_8048503
                cmp   edi, eax
                jbe   short loc_80484C1

loc_8048503:                  ; CODE XREF: f(int,int *,int *,int *)+5D
                mov   eax, ecx
                shr   eax, 2
                mov   , eax
                shl   eax, 2
                test    eax, eax
                mov   , eax
                jz      short loc_8048547
                mov   , ecx
                mov   ecx,
                xor   eax, eax
                xor   edx, edx
                nop

loc_8048520:                  ; CODE XREF: f(int,int *,int *,int *)+9B
                movdquxmm1, xmmword ptr
                movdquxmm0, xmmword ptr
                add   edx, 1
                paddd   xmm0, xmm1
                movdqaxmmword ptr , xmm0
                add   eax, 10h
                cmp   edx, ecx
                jb      short loc_8048520
                mov   ecx,
                mov   eax,
                cmp   ecx, eax
                jz      short loc_80484D8

loc_8048547:                  ; CODE XREF: f(int,int *,int *,int *)+73
                lea   edx, ds:0
                add   esi, edx
                add   edi, edx
                add   ebx, edx
                lea   esi,

loc_8048558:                  ; CODE XREF: f(int,int *,int *,int *)+CC
                mov   edx,
                add   eax, 1
                add   edi, 4
                add   edx,
                add   esi, 4
                mov   , edx
                add   ebx, 4
                cmp   ecx, eax
                jg      short loc_8048558
                add   esp, 0Ch
                xor   eax, eax
                pop   ebx
                pop   esi
                pop   edi
                pop   ebp
                retn
; ---------------------------------------------------------------------------
loc_8048578:                  ; CODE XREF: f(int,int *,int *,int *)+52
                cmp   eax, esi
                jnb   loc_80484C1
                jmp   loc_80484F8
_Z1fiPiS_S_   endp
```

几乎一样,但没有Intel的细致。

## 22.2 SIMD strlen() implementation

SIMD指令可能通过特殊的宏8插入到C/C++代码中。MSVC中他们被保存在intrin.h中。

Strlen()函数9的实现使用了SIMD指令,比常规的实现快了2-2.5倍。该函数将16个字符加载到一个XMM寄存器并检查是否为零

```
size_t strlen_sse2(const char *str)
{
      register size_t len = 0;
      const char *s=str;
      bool str_is_aligned=(((unsigned int)str)&0xFFFFFFF0) == (unsigned int)str;

      if (str_is_aligned==false)
                return strlen (str);

      __m128i xmm0 = _mm_setzero_si128();
      __m128i xmm1;
      int mask = 0;

      for (;;)
      {
                xmm1 = _mm_load_si128((__m128i *)s);
                xmm1 = _mm_cmpeq_epi8(xmm1, xmm0);
                if ((mask = _mm_movemask_epi8(xmm1)) != 0)
                {
                        unsigned long pos;
                        _BitScanForward(&pos, mask);
                        len += (size_t)pos;
                        break;
                }
                s += sizeof(__m128i);
                len += sizeof(__m128i);
      };

      return len;
}
```

(这里的例子基于源代码).

MSVC 2010 /Ox 编译选项:

```
_pos$75552 = -4               ; size = 4
_str$ = 8                     ; size = 4
?strlen_sse2@@YAIPBD@Z PROC   ; strlen_sse2

    push    ebp
    mov   ebp, esp
    and   esp, -16 ; fffffff0H
    mov   eax, DWORD PTR _str$
    sub   esp, 12 ; 0000000cH
    push    esi
    mov   esi, eax
    and   esi, -16 ; fffffff0H
    xor   edx, edx
    mov   ecx, eax
    cmp   esi, eax
    je      SHORT $LN4@strlen_sse
    lea   edx, DWORD PTR
    npad    3
$LL11@strlen_sse:
    mov   cl, BYTE PTR
    inc   eax
    test    cl, cl
    jne   SHORT $LL11@strlen_sse
    sub   eax, edx
    pop   esi
    mov   esp, ebp
    pop   ebp
    ret   0
$LN4@strlen_sse:
    movdqaxmm1, XMMWORD PTR
    pxor    xmm0, xmm0
    pcmpeqb         xmm1, xmm0
    pmovmskb      eax, xmm1
    test    eax, eax
    jne   SHORT $LN9@strlen_sse
$LL3@strlen_sse:
    movdqaxmm1, XMMWORD PTR
    add   ecx, 16 ; 00000010H
    pcmpeqb         xmm1, xmm0
    add   edx, 16 ; 00000010H
    pmovmskb      eax, xmm1
    test    eax, eax
    je      SHORT $LL3@strlen_sse
$LN9@strlen_sse:
    bsf   eax, eax
    mov   ecx, eax
    mov   DWORD PTR _pos$75552, eax
    lea   eax, DWORD PTR
    pop   esi
    mov   esp, ebp
    pop   ebp
    ret   0
?strlen_sse2@@YAIPBD@Z ENDP ; strlen_sse2
```

首先,检查str指针,如果不是按照16字节对齐则调用常规实现。

然后使用movdqa指令加载16个字节到xmm1.这里不使用movdqu的原因是如果指针不一致则从内存中加载的数据可能会不一致。

是的,它可能会以这种方式做,如果指针对齐,使用MOVDQA加载数据,否则使用比较慢的MOVDQU。

但是我们应该注意到这样的警告:

在windowsNT操作系统但不限于该操作系统,内存页按4kb对齐。每个win32进程独占4GB虚拟内存。事实上,只有部分地址空间与真实物理内存对应,如果进程访问的内存没有对应物理内存,将触发异常。这是虚拟内存的工作方式10.

一个函数一次加载16个字节,可能会跨内存分块访问。我们考虑这样一种情况,操作系统在x008c0000分配8192(0x2000)字节,因此块字节从地质0x008c0000到0x008c1fff。 内存块之后从0x008c2000什么都没有,操作系统没有分配任何内存。访问该地址将触发异常。

假如内存块包含的最后5个字符如下:

```
0x008c1ff8      ’h’
0x008c1ff9      ’e’
0x008c1ffa      ’l’
0x008c1ffb      ’l’
0x008c1ffc      ’o’
0x008c1ffd      ’x00’
0x008c1ffe      random noise
0x008c1fff      random noise
```

正常情况下,strlen()只会读取到”hello”。

如果我们使用MOVDQU读取16个字节,将会触发异常,应该避免这种情况。

因为我们要确保16字节对齐,保证我们不会读取未分配的内存。

让我们回到函数:

```
_mm_setzero_si128()—宏pxor xmm0, xmm0—清空XMM0寄存器。
_mm_load_si128()—宏 MOVDQA, 从内存加载16个字节到XMM寄存器。
_mm_cmpeq_epi8()—宏PCMPEQB,比较XMM寄存器的字节位,如果相等则为0xff否则为0。
```

比如:

```
XMM1: 11223344556677880000000000000000
XMM0: 11ab3444007877881111111111111111
```

执行pcmpeqb xmm1, xmm0之后,XMM1寄存器的值为:

`XMM1: ff0000ff0000ffff0000000000000000`

在本例中该指令比较每一个16字节块与16字节0字节块对比,XMM0通过pxor xmm0 xmm0置零。

接下来宏_mm_movemask_epi8() —这是PMOVMSKB指令。

`pmovmskb eax, xmm1`

pmovmskb创建源操作数每一个字节的自高位掩码,并保存结果到目的操作数的低byte。源操作数必须为MMX寄存器,目的操作数必须为32位通用寄存器。

比如:

`XMM1: 0000ff00000000000000ff0000000000`

对应的EAX:

`EAX=0010000000100000b`

之后bsf eax,eax被执行,eax值为5,意味着第一个是1的位置是5(从0开始)。

MSVC关于这个指令的宏是:_BitScanForward.

至此,找到结尾0的位置,然后程序返回长度计数。

整个过程大致就是这样。

顺便提一下,MSVC为了优化,使用了两个并排的循环。

SSE 4.2(英特尔core i7)提供了更多的指令,这些可能更容易字符串操作。

[http://www.strchr.com/strcmp_and_strlen_using_sse_4.2](http://www.strchr.com/strcmp_and_strlen_using_sse_4.2)
页: [1]
查看完整版本: C++反汇编入门21.单指令多数据流(SIMD)