优化汇编例程(14)

  1. 向量编程

因为微处理器的最大时钟频率存在技术限制,增加处理器吞吐率的趋势是并行处理多个数据。单指令多数据(SIMD)编程的原理是在一个大的寄存器中封装一个向量或一组数据,在一次操作中一起处理。有数以百计的SIMD指令可用。在《IA-32 Intel Architecture Software Developer’s Manual》卷2A及2B,以及《AMD64 Architecture Programmer’s Manual》卷4中列出这些指令。

多个数据可以下面的方式封装到64位MMX寄存器、128位XMM寄存器或256位YMM寄存器中:

数据类型

每封装数据

寄存器大小

指令集

8位整数

8

64位(MMX

MMX

16位整数

4

64位(MMX

MMX

32位整数

2

64位(MMX

MMX

64位整数

1

64位(MMX

SSE2

32位浮点

2

64位(MMX

3DNow(过时)

8位整数

16

128位(XMM

SSE2

16位整数

8

128位(XMM

SSE2

32位整数

4

128位(XMM

SSE2

64位整数

2

128位(XMM

SSE2

32位浮点

4

128位(XMM

SSE

64位浮点

2

128位(XMM

SSE2

8位整数

32

256位(YMM

AVX2

16位整数

16

256位(YMM

AVX2

32位整数

8

256位(YMM

AVX2

64位整数

4

256位(YMM

AVX2

32位浮点

8

256位(YMM

AVX

64位浮点

4

256位(YMM

AVX

8位整数

64

512位(ZMM

AVX-512BW

16位整数

32

512位(ZMM

AVX-512BW

32位整数

16

512位(ZMM

AVX-512

64位整数

8

512位(ZMM

AVX-512

32位浮点

16

512位(ZMM

AVX-512

64位浮点

8

512位(ZMM

AVX-512

表13.1. 向量类型

可以通过CUPID指令确定在一个特定微处理器上是否支持不同的指令集,如第143页所述。64位MMX寄存器不能与x87形式浮点寄存器一起使用。向量寄存器仅在操作系统支持时可用。如何检查操作系统是否启用向量寄存器,参考第144页。

要在一个向量寄存器中封装尽可能多的数据,选择合适目的的最小数据尺寸是有利的。数学计算可能要求双精度(64位)浮点,避免中间计算的精度损失,即使单精度对最终结果已足够。

在你选择使用向量指令前,你必须考虑结果代码是否比不使用向量的简单指令更快。使用向量代码,有时会在琐碎的事情上花费更多指令,比如移动数据到寄存器正确的位置,模拟条件移动。下面的例子13.8是这样的一个例子。在较旧的处理器上,向量指令相对较慢,但许多较新的处理器,在许多情形里,执行向量计算与标量(单)计算一样快。

对浮点计算,最好使用XMM寄存器,而不是x87寄存器,即使没有机会并行处理数据。XMM寄存器以一个比旧式x87寄存器栈更为直接的方式处理,某些CPU的x87指令性能很差。

没有VEX前缀XMM指令的内存操作数必须对齐到16。YMM指令的内存操作数最好对齐到32,ZMM对齐到64,但这不是必须的。如何对齐内存中数据,参考第81页。

大多数通用算术与逻辑操作可以在向量寄存器中进行。下面的例子展示了两个数组的相加:

; Example 13.1a. Adding two arrays using vectors

; float a[128], b[128], c[128];

; for (int i = 0; i < 128; i++) a[i] = b[i] + c[i];

; Assume that a, b and c are aligned by 32

      xor ecx, ecx                                                                   ; Loop counter i = 0

L: vmovaps ymm0, [b+rcx]                                               ; Load 8 elements from b

      vaddps ymm0, ymm0, [c+rcx]                                   ; Add 8 elements from c

      vmovaps [a+rcx], ymm0                                             ; Store result in a

      add ecx, 32                                                                   ; 8 elements * 4 bytes = 32

      cmp ecx, 512                                                                ; 128 elements * 4 bytes = 512

      jb L                                                                                 ; Loop

没有整数除法指令。一个常量除数的整数除法可以实现为乘法与偏移,使用第148页描述的方法,或http://www.agner.org/optimize/#vectorclass向量类库,www.agner.org/optimize/asmlib.zip汇编库。

    1. 使用AVX指令集与YMM寄存器

AVX指令集将128位XMM寄存器扩展为256位YMM寄存器,并支持256位向量中的浮点操作。AVX2指令集还支持YMM寄存器里的整数向量操作。AVX512指令集有512位向量可用,很可能在将来进一步扩展到1024位以及可能2048位。

每个XMM寄存器构成YMM寄存器的低半部。一个YMM寄存器可以保存一个8单精度向量或一个4双精度向量。使用AVX2,在一个YMM寄存器中,可以有4个64位整数,8个32位整数,16个16位整数或32个8位整数。

大多数AVX与AVX2指令允许3个操作数:一个目标与两个源操作数。这有输入寄存器不会被结果改写的好处。

在AVX指令集中,所有现存的XMM指令有两个版本。一个不改变目标YMM寄存器高半部(128 ~ 255比特)的遗留版本,以及将高半部置零以消除假依赖的VEX前缀版本。VEX前缀版本在名字中有一个V前缀,在大多数情形里有3个操作数:

; Example 13.2. Legacy and VEX versions of the same instruction

addps xmm1, xmm2                                           ; xmm1 = xmm1 + xmm2

vaddps xmm1, xmm2, xmm3                            ; xmm1 = xmm2 + xmm3

如果代码序列同时包含128位与256位指令,使用128位指令的VEX版本是重要的。混用256位指令与没有VEX前缀遗留128位指令,将导致某些Intel处理器在不同状态间切换寄存器文件,这将花费许多时钟周期。这适用于Intel Sandy Bridge,Ivy Bridge,Haswell与Broadwell处理器,但不适用Skylake与更新的处理器以及AMD处理器。

为了与现存代码兼容,在Sandy Bridge,Ivy Bridge与Broadwell处理器上,寄存器组有三个不同的状态:

  1. (干净状态)。YMM寄存器的高半部未使用,且已知为零。
  2. (修改状态)。至少一个YMM寄存器的高半部被使用且包含数据。
  3. (保存状态)。所有的YMM寄存器分解为两个。低半部由不改变高半部的遗留XMM指令使用。所有高半部保存在一个暂存器里。如果需要,通过转换到状态B,每个寄存器的两部分将被再次合并。

为了状态间的快速转换,有两条指令可用。将所有YMM寄存器置零的VZEROALL,及将所有YMM寄存器高半部置零的VZEROUPPER。这两条指令将处理器维持在状态A。下面的状态转换表展示了状态的转换:

当前状态 à

A

B

B

指令 ↓

VZEROALL / UPPER

A

A

A

XMM

A

C

C

VEX / XMM

A

B

B

YMM

B

B

B

表13.2. YMM状态转换

状态A是中性的初始状态。在使用完整的YMM寄存器时,需要状态B。在从状态B调用时,需要状态C使遗留XMM代码快速且没有假依赖。从状态B到状态C的转换代价高,因为所有的YMM寄存器必须被分解为分别保存的两部分、从状态C到状态B的转换同样代价高,因为所有的寄存器必须被再次合并。从状态C到A的转换也是高代价的,因为保存寄存器高半部的暂存器不支持乱序访问,且必须保护不受可能误预测分支推测执行的影响。转换BàC,CàB及CàA,在某些Intel处理器上非常耗时,因为它们必须等待所有寄存器回收。转换AàB与BàA是快的,最多需要1时钟周期。C应该被视为不期望的状态,转换AàC是不可能的。在Intel Sandy Bridge,Ivy Bridge,Haswell与Broadwell处理器上,根据我的测量,不期望的转换需要大约70时钟周期。在AMD处理器与Intel Skylake及更新的处理器上,这个问题不再存在,这些转换不需要额外时间。

下面的例子展示了状态转换:

; Example 13.3a. Transition between YMM states

vaddps ymm0, ymm1, ymm2                                                             ; State B

addss xmm3, xmm4                                                                             ; State C

vmulps ymm0, ymm0, ymm5                                                             ; State B

例子13.3a有两个昂贵的状态转换,从B到C,然后回到状态B。通过VEX版本的VADDSS替换遗留ADDSS指令,可以避免状态转换:

; Example 13.3b. Transition between YMM states avoided

vaddps ymm0, ymm1, ymm2                                                             ; State B

vaddss xmm3, xmm3, xmm4                                                              ; State B

vmulps ymm0, ymm0, ymm5                                                             ; State B

在调用使用XMM指令的库函数时,不能使用这个方法。这里的解决方案是保存任何使用的YMM寄存器,进入状态A:

; Example 13.3c. Transition to state A

vaddps ymm0, ymm1, ymm2                                                             ; State B

vmovaps [mem], ymm0                                                                       ; Save ymm0

vzeroupper                                                                                             ; State A

call XMM_Function                                                                               ; Legacy function

vmovaps ymm0, [mem]                                                                       ; Restore ymm0

vmulps ymm0, ymm0, ymm5                                                             ; State B

vzeroupper                                                                                             ; Go to state A before returning

ret

...

XMM_Function proc near

addss xmm3, xmm4                                                                              ; State A

ret

混用XMM与YMM代码的程序应该遵循特定指引,以避免高代价的自/至状态C的转换:

  • 使用YMM指令的函数应该在返回前发布一条VZEROALL或VZEROUPPER,如果存在返回到XMM代码的可能性。
  • 在调用任何可能包含XMM代码的函数前,使用YMM指令的函数应该保存使用的YMM寄存器,发布一条VZEROALL或VZEROUPPER。
  • 具有一个CPU分派器,如果可以选择YMM代码,否则XMM代码的函数应该在离开YMM部分前,应该发布一条VZEROALL或VZEROUPPER。

换而言之:这个建议允许任何函数使用XMM或YMM寄存器,但在调用任何具有未知VEX状态的函数后,或返回到任何具有未知VEX状态的函数前,使用YMM寄存器的函数必须将寄存器保留在状态A。

显然,这不适用于使用完整YMM寄存器来传递参数或返回的函数。将YMM寄存器用于参数传递的函数,在入口处可以假定状态B。将YMM寄存器(YMM0)用于返回值的函数在返回时仅可以在状态B,总是应该避免状态C。

在大多数处理器上,指令VZEROUPPER比VZEROALL快。因此,建议使用VZEROUPPER,而不是VZEROALL,除非你希望完全的初始化。VZEROALL不能用在64位Windows中,因为ABI声明XMM6 ~ XMM15有被调用者保存状态。换句话说,调用函数可以假设寄存器XMM6 ~ XMM15在返回后不改变,但YMM寄存器的高半部不是。在32位Windows或任何Unix系统(Linux,BSD,Mac)中,没有XMM或YMM寄存器有被调用者保存状态。因此,在如64位Linux里使用VZEROALL是可以的。显然,如果任何YMM寄存器包含函数参数或返回值,不能使用VZEROALL。在这些情形里,必须使用VZEROUPPER。

在Intel Sandy Bridge,Ivy Bridge,Haswell与Broadwell处理器上,混用VEX与非VEX代码有性能损失。Intel Skylake处理器对混用VEX与非VEX代码没有性能损失,AMD处理器没有这样的性能损失,在所有具有高代价状态转换的处理器上,VZEROUPPER与VZEROALL指令都是快的。

不过,在Knights Landing处理器(第一个支持AVX512的处理器)上,VZEROUPPER与VZEROALL指令代价很高。在Knights Landing上不建议使用这些指令。何时使用VZEROUPPER以及何时不使用的问题,目前在https://software.intel.com/en-us/forums/intel-isa-extensions/topic/704023上讨论。这个讨论尚未解决,但可以遵循得出的初步结论:

  • 尽可能避免混用VEX与非VEX代码
  • 在支持AVX但不支持AVX512的处理器上:在调用具有未知VEX状态的函数前,以及在返回到具有未知VEX状态的函数前,使用VZEROUPPER。
  • 在支持AVX512的处理器上:不要使用VZEROUPPER(仅寄存器zmm0 ~ zmm15受VZEROUPPER/VZEROALL影响,zmm16 ~ zmm31没有)。
  • 在通用函数库中:使CPU分配到依赖支持指令集的多个分支中。仅在支持AVX但不支持AVX512的处理器分支里使用VZEROUPPER。

混用VEX与非VEX代码是一个常见的错误,很容易犯,但很难检测。代码仍然工作,但在某些处理器性能下降。强烈建议再三检测代码中混用的VEX与非VEX指令。

预热时间

某些高端处理器能在不用时关闭256位执行单元的高128位来节省电能。在一个空闲期间后,启动高半部需要大约14 μs。在预热期间,256位向量指令的吞吐率要低得多,因为执行一个256位操作,处理器需要使用低半部128位单元两次。在需要256位单元之前,通过在合适时间执行一条伪256位指令,预先预热256位单元是可能的。在没有256位指令的大约675 μs后,256位单元的高半部将再次关闭。这个现象在手册3《Intel,AMD与VIA CPU微架构》中描述。

操作系统支持

使用YMM寄存器或VEX编码指令的代码仅能运行在支持它的操作系统上,因为在任务切换时,操作系统必须保存YMM寄存器。下面的操作系统版本支持AVX与XMM寄存器:Windows 7,Windows server 2008 SP2,Linux内核2.6.30或更新版本。

YMM与系统代码

状态B与C之间的转换必定发生的一个情形是,YMM代码被中断,且中断处理句柄包含保存XMM寄存器,但不保存完整YMM寄存器的遗留XMM代码。事实上,状态C的发明就是为了在这个情形里保留YMM寄存器的高半部。

在编写可能被中断句柄调用的设备驱动与其他代码时,遵循某些规则是非常重要的。如果在系统代码中一条YMM指令或VEX-XMM指令修改了YMM寄存器,首先使用XSAVE保存整个寄存器暂停,在返回前使用XRESTORE恢复它是必要的。独立保存YMM寄存器是不够的,因为未来的处理器可能进一步扩展YMM寄存器为512位ZMM寄存器(或者将来的称谓),在执行YMM指令时将零扩展YMM寄存器到ZMM,因而破坏了ZMM寄存器的最高部分。XSAVE / XRESTORE是仅有的、与未来超过256位扩展兼容的、保存这些寄存器的方式。未来的扩展将不使用每条YMM指令两个版本的复杂方法。

如果一个设备驱动不使用XSAVE / XRESTORE,存在不经意使用YMM寄存器的风险,即使程序员不打算这么做。不是为系统代码准备的编译器可能插入对库函数,如memset与memcpy的隐式调用。通常这些函数有自己的CPU分派器,可能选择最大的可用寄存器。因此,使用为制作系统代码准备的编译器与函数库是必要的。

这些规则在手册5《调用惯例》中进一步描述。

使用非破坏性三操作数指令

定义YMM指令的AVX指令集,通过将现有的前缀与转义码替换为新的VEX前缀,还定义了所有现存XMM指令的另一个编码。VEX前缀有定义了一个额外寄存器操作数的进一步好处。几乎所有之前有两个操作数的XMM指令,在使用VEX前缀时,都有三个操作数。

两操作数版本的指令通常将同一个寄存器用作目标以及其中一个源操作数:

; Example 13.4a. Two-operand instruction

addsd xmm0, xmm2                                              ; xmm0 = xmm0 + xmm2

这有结果改写其中一个源操作数的坏处。在使用三操作数加法时,可以避免例子13.4a里的移动指令:

; Example 13.4b. Three-operand instruction

vaddsd xmm0, xmm1, xmm2                               ; xmm0 = xmm1 + xmm2

这里,没有源操作数被破坏,因为结果保存在另一个目标寄存器里。这有帮于避免寄存器-寄存器移动。例子13.4a与13.4b中的addsd与vaddsd指令具有相同的长度。因此,使用三操作数版本没有性能损失。名字以ps结尾的指令(封装单精度),如果目标寄存器不是xmm8 ~ xmm15,两操作数版本比三操作数版本要短1字节。在少数情形里,三操作数版本比两操作数版本短。在大多数情形里,两操作数与三操作数版本有相同的长度。

只要寄存器集在状态A,在代码中混用两操作数与三操作数指令是可以的。但如果寄存器集不管什么原因,恰好在状态C,那么混用VEX与非VEX的XMM指令,在每次指令类型改变时,会导致高代价的状态改变。因此,仅使用VEX版本或非VEX版本更好。如果使用了YMM寄存器(状态B),所有XMM指令应该仅使用VEX前缀版本,直到这个VEX段以VZEROALL或VZEROUPPER结束。

64位MMX指令与大多数通用寄存器指令没有三操作数版本。混用MMX与VEX指令没有性能损失。少数通用寄存器指令也有三操作数。

非对齐内存访问

所有带有一个内存操作数、VEX编码的XMM及YMM指令允许非对齐内存访问,除了显式要求对齐的指令VMOVAPS,VMOVAPD,VMOVDQA,VMOVNTPS,VMOVNTPD,VMOVNTDQ。因此,将YMM操作数保存在栈上,无需保持栈对齐到32是可以的。

编译器支持

Microsoft,Intel与Gnu编译器支持VEX指令集。

编译器将对所有XMM指令使用VEX前缀的版本,如果编译AVX指令集,包括固有函数。在从使用AVX编译的模块迁移到不使用AVX编译的模块或库之前,发布一条VZEROUPPER指令,是程序员的责任。

融合乘加指令

在仅能执行一次乘法的时间里,融合乘加(FMA)指令可以进行一次浮点乘法,后跟一次浮点加法或减法。FMA操作具有形式:

d = a * b + c

FMA指令有称为FMA3与FMA4的两个变种。FMA4指令可将4个不同的寄存器用于操作数a,b,c及d。FMA3仅能有3个操作数,其中目标d必须使用与输入操作数a,b,c其中一个相同的寄存器,一开始Intel设计FMA4指令集,但当前仅支持FMA3。最新的AMD处理器同时支持FMA3与FMA4(参考en.wikipedia.org/wiki/FMA_instruction_set#History)。

例子

第95页的例子12.6f展示了对一个DAXPY计算使用AVX指令集。第107页的例子12.12e展示了对一个泰勒序列展开使用AVX指令集。这两个例子说明了3操作数指令以及256位向量的使用。例子12.6h与12.6g分别展示了FMA3与FMA4指令的使用。

    1. 使用AVX512指令集与ZMM寄存器

伴随AVX512指令集,向量寄存器被扩展到512比特。在64位模式中,向量寄存器数增加到32个,在32位模式中,仅有8个向量寄存器。

有8个新的称为k0 ~ k7的掩码寄存器。掩码寄存器k1 ~ k7可用于每个向量元素上的条件操作,如第124页所述。在数组大小不能被向量寄存器大小整除时,条件操作对数组循环特别有用。这在第100页解释了。

对AVX512有几个额外的扩展。所有支持AVX512的处理器基于这些扩展的某些,但目前为止(2016年)没有处理器具有所有。对AVX512已知且已计划的扩展有这些:

  • AVX512F。基础(Foundation)。所有AVX512处理器都有这。包括在512位向量里的32位与64位整数、浮点及双精度数上的操作,以及掩蔽操作。
  • AVX512L。在128位与256位向量上包含相同的操作,包括掩蔽操作与32个向量寄存器。
  • AVX512BW。在512位向量里8位与16位整数上的操作。
  • AVX512DQ。带有64位整数的乘法与转换指令。在浮点与双精度上的其他各种指令。
  • AVX512ER。快速倒数、平方根倒数与指数函数。单精度上精确,双精度上近似。
  • AVX512CD。冲突检测。在一个向量中找出重复的元素。
  • AVX512PF。使用gather/scatter逻辑预取指令。
  • AVX512VBMI。8位粒度的排列(permutation)与偏移(shift)。
  • AVX512IFMA。52位整数上的融合乘加。
  • AVX512_4VNNIW。16位整数上的迭代点积。
  • AVX512_4FMAPS。单精度的,迭代融合乘加。
    1. Xmm与ymm寄存器里的条件移动

考虑在4对值里查找最大值的这个C++代码:

// Example 13.5a. Loop to find maximums

float a[4], b[4], c[4];

for (int i = 0; i < 4; i++) {

      c[i] = a[i] > b[i] ? a[i] : b[i];

}

如果我们希望使用XMM寄存器实现这个代码,那么不能对循环里的分支使用条件跳转,因为对所有4个元素分支条件不是相同的。

幸好,有一条做这件事的maximum指令:

; Example 13.5b. Maximum in XMM

movaps xmm0, [a]                                                                                     ; Load a vector

maxps xmm0, [b]                                                                                       ; max(a,b)

movaps [c], xmm0                                                                                     ; c = a > b ? a : b

对单精度与双精度浮点、8位与16位整数,都存在minimum与maximum向量指令。浮点向量元素的绝对值可以通过AND掉符号位来计算,如第134页例子13.16所示。用于整数向量元素绝对值的指令存在于“补充SSE3(Supplementary SSE3)”指令集中。整数饱和加法向量指令(如PADDSW)也可用于查找最大、最小值,或把值限定在一个指定区域。

不过,这些方法不是太通用。在向量寄存器中进行条件移动的最通用方式是使用布尔向量(Boolean vector)指令。下面的例子是上面例子的一个修改,其中我们不能使用MAXPS指令:

// Example 13.6a. Branch in loop

float a[4], b[4], c[4], x[4], y[4];

for (int i = 0; i < 4; i++) {

      c[i] = x[i] > y[i] ? a[i] : b[i];

}

通过制作一个掩码,在条件成立时包含全1,在条件不成立时包含全0,可以完成必须的条件移动。a[i]与这个掩码AND,b[i]与掩码的取反AND:

; Example 13.6b. Conditional move in XMM registers

movaps xmm1, [y]                                                                   ; Load y vector

cmpltps xmm1, [x]                                                                   ; Compare with x. xmm1 = mask for y < x

movaps xmm0, [a]                                                                   ; Load a vector

andps xmm0, xmm1                                                                ; a AND mask

andnps xmm1, [b]                                                                    ; b AND NOT mask

orps xmm0, xmm1                                                                   ; (a AND mask) OR (b AND NOT mask)

movaps [c], xmm0                                                                    ; c = x > y ? a : b

制作这个条件的向量(例子13.6b中的x与y)以及被选择的向量(例子13.6b中的a与b)不需要是相同的类型。例如,x与y可以是整数。但它们应该有相同的元素数。如果a与b是double,每个向量2个元素,x与y是32位整数,每个向量4个元素。那么我们必须复制x与y里的每个元素,以得到正确大小的掩码(参考下面的例子13.8b)。

注意,AND-NOT指令(andnps,andnpd,pandn)取反目标操作数,不是源操作数。这意味着它破坏了掩码。因此,在例子13.6b中,我们必须在andnps前有andps。如果支持SSE4.1,我们可以使用BLENDVPS指令:

; Example 13.6c. Conditional move in XMM registers, using SSE4.1

movaps xmm0, [y]                                                             ; Load y vector

cmpltps xmm0, [x]                                                             ; Compare with x. xmm0 = mask for y < x

movaps xmm1, [a]                                                             ; Load a vector

blendvps xmm1, [b], xmm0                                             ; Blend a and b

movaps [c], xmm0                                                             ; c = x > y ? a : b

如果多次需要这个掩码, a与b的XOR和掩码AND更高效。这在下一个例子中展示,它进行a和b条件交换:

// Example 13.7a. Conditional swapping in loop

float a[4], b[4], x[4], y[4], temp;

for (int i = 0; i < 4; i++) {

     if (x[i] > y[i]) {

          temp = a[i]; // Swap a[i] and b[i] if x[i] > y[i]

          a[i] = b[i];

          b[i] = temp;

     }

}

现在使用XMM向量的汇编代码:

; Example 13.7b. Conditional swapping in XMM registers, SSE

movaps xmm2, [y]                                                                  ; Load y vector

cmpltps xmm2, [x]                                                                  ; Compare with x. xmm2 = mask for y < x

movaps xmm0, [a]                                                                  ; Load a vector

movaps xmm1, [b]                                                                  ; Load b vector

xorps xmm0, xmm1                                                                ; a XOR b

andps xmm2, xmm0                                                               ; (a XOR b) AND mask

xorps xmm1, xmm2                                                                ; b XOR ((a XOR b) AND mask)

xorps xmm2, [a]                                                                      ; a XOR ((a XOR b) AND mask)

movaps [b], xmm1                                                                  ; (x[i] > y[i]) ? a[i] : b[i]

movaps [a], xmm2                                                                  ; (x[i] > y[i]) ? b[i] : a[i]

指令xorps xmm0, xmm1产生a与b间比特差异的一个模式。这个比特模式与掩码AND,如果a与b应该交换,使xmm2包含需要改变的比特,如果不需要交换,包含零。最后两条xorps指令翻动,如果a与b应该交换,必须改变的比特,如果不应该,则不改变。

也可以通过使用算术右移指令psrad,将符号位拷贝到所有比特位置,产生用于条件移动的掩码。这在下一个例子中展示,其中向量元素有不相同的整数指数。我们使用第112页例子12.16a中的方法计算指数。

// Example 13.8a. Raise vector elements to different integer powers

double x[2], y[2]; unsigned int n[2];

for (int i = 0; i < 2; i++) {

    y[i] = pow(x[i],n[i]);

}

如果n的元素都相等,那么最简单的解决方案是使用一个分支。但如果指数不同,我们必须使用条件移动:

; Example 13.8b. Raise vector to power, using integer mask, SSE2

.data                                                                                     ; Data segment

align 16                                                                                ; Must be aligned

ONE DQ 1.0, 1.0                                                                 ; Make constant 1.0

X DQ ?, ?                                                                              ; x[0], x[1]

Y DQ ?, ?                                                                              ; y[0], y[1]

N DD ?, ?                                                                              ; n[0], n[1]

 

.code

; register use:

; xmm0 = xp

; xmm1 = power

; xmm2 = i (i0 and i1 each stored twice as DWORD integers)

; xmm3 = 1.0 if not(i & 1)

; xmm4 = xp if (i & 1)

 

      movq xmm2, [N]                                                         ; Load n0, n1

      punpckldq xmm2, xmm2                                           ; Copy to get n0, n0, n1, n1

      movapd xmm0, [X]                                                     ; Load x0, x1

      movapd xmm1, [one]                                                 ; power initialized to 1.0

      mov eax, [N]                                                                 ; n0

      or eax, [N+4]                                                                ; n0 OR n1 to get highest significant bit

      xor ecx, ecx                                                                   ; 0 if n0 and n1 are both zero

      bsr ecx, eax                                                                   ; Compute repeat count for max(n0,n1)

 

L1: movdqa xmm3, xmm2                                                ; Copy i

      pslld xmm3, 31                                                             ; Get least significant bit of i

      psrad xmm3, 31                                                            ; Copy to all bit positions to make mask

      psrld xmm2, 1                                                               ; i >>= 1

      movapd xmm4, xmm0                                                ; Copy of xp

      andpd xmm4, xmm3                                                   ; xp if bit = 1

      andnpd xmm3, [one]                                                   ; 1.0 if bit = 0

      orpd xmm3, xmm4                                                       ; (i & 1) ? xp : 1.0

      mulpd xmm1, xmm3                                                    ; power *= (i & 1) ? xp : 1.0

      mulpd xmm0, xmm0                                                    ; xp *= xp

      sub ecx, 1                                                                       ; Loop counter

      jns L1                                                                               ; Repeat ecx+1 times

      movapd [Y], xmm1                                                       ; Store result

循环的重复次数在循环外单独计算,以减少循环内的指令数。

在P4E上例子13.8b的时间分析:有4条连续的依赖链:xmm0:7时钟周期,xmm1:7时钟周期,xmm2:4时钟周期,ecx:1时钟周期。不同执行单元的吞吐率:MMX-SHIFT:3 μop,6时钟周期。MMX-ALU:3 μop,6时钟周期。FP-MUL:2 μop,4时钟周期。端口1的吞吐率:8 μop,8时钟周期。因此,循环看起来受限于端口1的吞吐率。我们可以期望的最好时间是每迭代8时钟周期,这是必须去往端口1的μop数。不过,3条连续依赖链由两个被破坏但相当长的,涉及xmm3与xmm4,分别需时23与29时钟周期的依赖链互联。这有阻碍μop最优重排的趋势。测得时间大约是每迭代10 μop。这个时序实际上要求相当令人印象深刻的重排能力,考虑几次迭代必须重叠且几条依赖链相互交织,以满足所有端口与执行单元的限制。

在通用寄存器里的条件移动使用CMOVcc,使用FCMOVcc的浮点寄存器不会被XMM寄存器快。

在具有SSE4.1指令集的处理器上,我们可以使用PBLENDVB,BLENDVPS或BLENDVPD指令,替代AND/OR操作,就像上面的例子13.6c中那样。在具有XOP指令集的AMD处理器上,我们可以相同的方式使用VPCMOV指令作为替代。

    1. 使用AVX512的条件移动

AVX512指令集支持由一个掩码寄存器控制的掩蔽操作。一条掩蔽向量指令将仅在掩码寄存器对应比特为1的向量元素上执行操作。我们可以重写例子13.6来展示这:

// Example 13.9a. Branch in loop using AVX512

float a[16], b[16], c[16], x[16], y[16];

for (int i = 0; i < 16; i++) {

     c[i] = x[i] > y[i] ? a[i] : b[i];

}

使用AVX512实现这是相当高效的:

; Example 13.9b. Conditional move in ZMM registers

vmovaps zmm1, [y]                                                       ; Load vector y

vcmpltps k1, zmm1, [x]                                                 ; Compare with x. k1 = mask for y < x

vmovaps zmm0, [b]                                                       ; Load vector b

vmovaps zmm0{k1}, [a]                                                ; Load vector a for elements with mask bit 1

vmovaps [c], zmm0                                                       ; c = x > y ? a : b

在AVX512下,大多数向量指令可以被掩蔽,例如:

// Example 13.10a. Conditional addition using AVX512

float a[16], b[16];

for (int i = 0; i < 16; i++) {

    if (a[i] < 0) {

         a[i] += b[i];

    }

}

这可以使用一个掩蔽加法实现:

; Example 13.10b. Conditional add in ZMM registers

vmovaps zmm1, [a]                                                              ; Load vector a

vpxord zmm0, zmm0, zmm0                                              ; Make zero

vcmpltps k1, zmm1, zmm0                                                 ; Compare with zero. k1 = mask for a < 0

vaddps zmm1{k1}, zmm1, [b]                                             ; Masked addition

vmovaps [a], zmm1                                                              ; Store result

掩蔽指令也可以用在更小的xmm与ymm寄存器上,如果支持指令集扩展AVX512VL。如果支持AVX512DQ,可以vpxord指令替换vxorps。

在上面的例子中,掩码比特为0的向量元素不改变。将禁用的元素置零,而不是使它们不改变是可能的,通过声明{z}选项:

; Example 13.11. Masking and zeroing

mov eax, 000FH                                                                     ; make constant

kmovw k1, eax                                                                       ; copy constant to mask register

vmovaps zmm1{k1}, [a]                                                        ; conditional move

vmovaps zmm2{k1}{z}, [b]                                                   ; conditional move with zeroing

在这个例子中,数组a的前4个元素被读入zmm1,zmm1余下的元素不改变。数组b的前4元素读入zmm2,zmm2余下的元素置为零。置零选项是有用的,因为它消除了zmm2对zmm2之前值上的依赖。建议尽可能使用置零选项,因为它使得掩蔽指令的乱序执行更高效。

掩蔽是免费的,就大多数指令而言,有没有掩蔽,不管掩码的值为何,时延与吞吐率都是相同的。另一方面,你可能把时间浪费在掩码是全零的计算上,如果掩码是全零,跳过耗时的条件计算是有利的。例如:

// Example 13.12a. Skip calculations if mask is all zeroes

float a[16], b[16];

for (int i = 0; i < 16; i++) {

     if (a[i] >= 0) {

          b[i] = sqrt(a[i]);

     }

}

如果数组a所有的元素经常是负数,我们可以跳过耗时的平方根:

; Example 13.12b. Skip calculations if mask is all zeroes

vmovaps zmm1, [a]                                                               ; Load vector a

vpxord zmm0, zmm0, zmm0                                               ; Make zero

vcmpltps k1, zmm1, zmm0                                                  ; Compare with zero. k1 = mask for a < 0

kortestw k1, k1                                                                      ; test bits of k1

jz L1                                                                                          ; jump if all zero

vmovaps zmm2, [b]                                                               ; Load vector b

vsqrtps zmm2{k1}, zmm1                                                     ; square root of non-negative elements

vmovaps [b], zmm2                                                               ; Store modified b

L1:

一条掩蔽指令通常可以替换两条指令,因为它进行两件事。甚至有一个掩码寄存器的比较指令也可以被掩蔽。带有一个掩码寄存器作为输出,并施行另一个掩码的比较与测试指令,总是有一个隐含的置零选项,使输出掩码为零,而不是不改变对应输入掩码为零的比特。结果是,比较结果与输入掩码的逻辑AND合并。这意味着AND合并可以比OR合并更高效地实现。这展示在下面的例子中:

// Example 13.13a. Use of masks

float a[16], b[16], c[16];

for (int i = 0; i < 16; i++) {

     if (a[i] > 0 || b[i] > 0) {

          c[i] = a[i] + b[i];

     }

     else {

          c[i] = a[i] - b[i];

     }

}

在这个例子中,根据公式:a || b = !(!a && !b),通过取反所有输入与输出,可以将OR(||)转换为AND(&&)。这将代码改为:

// Example 13.13b. Use of masks, convert OR to AND

float a[16], b[16], c[16];

for (int i = 0; i < 16; i++) {

     if (a[i] <= 0 && b[i] <= 0) {

          c[i] = a[i] - b[i];

     }

     else {

          c[i] = a[i] + b[i];

     }

}

这是有用的,因为可以使用第一个的结果掩蔽第二条比较指令,优化AND操作:

; Example 13.13c. Efficient use of masks

vmovaps zmm1, [a]                                                          ; Load vector a

vmovaps zmm2, [b]                                                          ; Load vector b

vpxord zmm0, zmm0, zmm0                                           ; Make zero

vcmpleps k1, zmm1, zmm0                                             ; k1 = mask for a <= 0

vcmpleps k2{k1}, zmm2, zmm0                                      ; k2 = mask for a <= 0 && b <= 0

vaddps zmm0, zmm1, zmm2                                           ; a + b

vsubps zmm0{k2}, zmm1, zmm2                                    ; a - b if mask

vmovaps [c], zmm0                                                           ; Store result

    1. 以意图以外的其他类型使用向量指令

大多数XMM、YMM与ZMM指令是有类型的,在于它们意图用于特定的数据类型。例如,使用一条指令把整数加上浮点数是不合理的。但仅移动数据的指令将对任何数据类型都有效,即使它们专门用于一个特定类型的数据。如果你拥有的数据类型不存在等效的指令,或者另一个数据类型指令更高效,这是有用的。

所有移动、混排(shuffle)、混合(blend)或偏移(shift)数据的向量指令,以及布尔指令可用于它们目标类型以外的其他数据类型。但执行任何算术操作、类型转换或精度转换的指令,仅能用于它的目标类型。例如,指令FLD不仅移动浮点数据,它还转换到另一个精度。如果你尝试使用FLD与FSTP来移动整数数据,那么在整数恰好不能表示一个正常的浮点数的情形下,你将得到次正规操作数的异常。在某些情形里,这条指令甚至可能改变数据的值。但指令MOVAPS,它也是意图用于移动浮点数据,不会转换精度或别的。它只是移动数据。因此,使用MOVAPS移动整数数据没有问题。

如果你怀疑一条特定的指令是否对任何数据类型有效,检查Intel或AMD的软件手册。如果指令可以产生任何种类的“浮点异常”,那么它不应该用于目标类型以外的类型。在某些处理器上,使用错误的类型会有性能损失。这是因为对整数与浮点数据,处理器可能有不同的数据总线或执行单元。在整数与浮点执行单元间移动数据,取决于处理器,需要1个或多个时钟周期,如表13.3列出的那样。

处理器

旁路时延,时钟周期

Intel Core 2及更早的处理器

1

Intel Nehalem

2

Intel Sandy Bridge与更新的处理器

0-1

Intel Atom

0

AMD

2

VIA Nano

2-3

表13.3. 整数与浮点执行单元间数据旁路时延

在Intel Core2与更早的Intel 处理器上,某些浮点指令在整数单元中执行。这包括XMM移动指令,布尔及某些混排与封装指令。在与使用浮点单元的指令混合时,这些指令有一个旁路时延。在大多数其他处理器上,根据指令名使用执行单元,比如MOVAPS XMM1, XMM2使用浮点单元,MOVDQA XMM1, XMM2使用整数单元。读或写内存的指令使用一个单独的单元。在某些处理器上,从内存单元到浮点单元的旁路时延可能比到整数单元长,但它不依赖于指令的类型。因此,MOVAPS XMM0, [MEM]与MOVDQA XMM0, [MEM]的时延没有差别,但不保证在将来的处理器上会有差别。

不同处理器执行单元的更多细节可以在手册3《Intel,AMD与VIA CPU微架构》中找到。手册4《指令表》有所有指令的列表,指出它们使用哪些执行单元。

在没有旁路时延以及吞吐率比时延重要的情形下,使用错误类型的指令会是一个优势。某些情形描述如下。

使用最短的指令

封装单精度浮点数、名字以PS结尾的指令,比用于双精度或整数的等效指令短。例如,可使用MOVAPS替换MOVAPD或MOVDQA自/至内存或寄存器间移动数据。在一些处理器上,在使用MOVAPS移动整数指令的结果到另一个寄存器时,有旁路时延,但自/至内存移动数据时不会。

使用更高效的指令

将向量寄存器设置为零的一个有效方式是PXOR XMM0, XMM0。许多处理器将这条指令识别位与XMM0的先前值无关,但不是所有的处理器同样识别XOPRS与XORPD。因此,将寄存器置零,优先使用XOR指令。

布尔向量指令(PAND,PANDN,POR,PXOR)的整数版本,在某些AMD与Intel处理器上,可以比等效的浮点指令(ANDPS等)使用更多不同的执行单元。

使用其他数据类型不可用的指令

有许多情形,对目标类型以外的类型使用指令是有利的,只是因为对你所有的数据类型,不存在等效的指令。用于单精度浮点向量的指令在SSE指令集中可用,而双精度与整数的等效指令要求SSE2指令集。使用MOVAPS替换MOVAPD或MOVDQA移动数据,使代码与支持SSE,但不支持SSE2的处理器兼容。

许多用于数据混排与混合的指令仅对一种数据类型可用。这些指令很容易用于它们目标类型以外的类型。旁路时延,如果有,可能小于替代方案的代价。数据混排指令在下一段落列出。

    1. 混排数据

向量化代码有时需要大量的指令来交换、拷贝向量元素,把数据放入向量正确的位置。这些额外指令的需求,降低了使用向量操作的优势。使用目标类型是你的类型以外的混排指令是可能的,如前面章节里解释的那样。一些对数据混排有用的指令列在下面。

在128位通道内排列数据

指令

数据块大小,比特

描述

指令集

PSHUFD

32

一般排列

SSE2

PSHUFLW

16

仅排列寄存器低半部

SSE2

PSHUFHW

16

仅排列寄存器高半部

SSE2

SHUFPS

32

排列

SSE

SHUFPD

64

排列

SSE2

PSLLDQ

8

偏移到另一个位置,把原来位置置零

SSE2

PSHUFB

8

一般排列

Suppl. SSE3

PALIGNR

8

旋转816字节的向量

Suppl. SSE3

PINSRB

8

向向量插入字节

SSE4.1

PINSRW

16

向向量插入字

SSE

PINSRD

32

向向量插入双字

SSE4.1

PINSRQ

64

向向量插入四字

SSE4.1

INSERTPS

32

向向量插入双字

SSE4.1

VPERMILPS

32

可变选择子(selector)排列

AVX

VPERMILPD

64

可变选择子(selector)排列

AVX

VPPERM

8

可变选择子(selector)排列

AMD XOP

表13.4. 排列指令

跨128位通道排列数据

指令

数据块大小,比特

描述

指令集

VPERMW

16

可变选择子(selector)排列

AVX512BW

VPERMD

32

可变选择子(selector)排列

AVX2

VPERMQ

64

可变选择子(selector)排列

AVX2

VPERMPD

32

可变选择子(selector)排列

AVX2

VPERMPD

64

可变选择子(selector)排列

AVX2

VPERMI/T2B

8

从两个源排列

AVX512VBMI

VPERMI/T2W

16

从两个源排列

AVX512BW

VPERMI/T2D

32

从两个源排列

AVX512

VPERMI/T2Q

64

从两个源排列

AVX512

VPERMI/T2PS

32

从两个源排列

AVX512

VPERMI/T2PD

64

从两个源排列

AVX512

VPERM2I128

128

从两个源排列

AVX2

VPERM2F128

128

从两个源排列

AVX

表13.5. 完全排列的指令

从两个不同源合并数据

指令

数据块大小,比特

描述

指令集

SHUFPS

32

目标任意位置低2个双字,源任意位置高2个双字

SSE

SHUFPD

64

目标任意位置低四字,源任意位置高四字

SSE2

MOVLPS/D

64

内存低四字,高四字不变

SSE/SSE2

MOVHPS/D

64

内存高四字,低四字不变

SSE/SSE2

MOVLHPS

64

源低半部的高四字,低四字不变

SSE

MOVHLPS

64

源高半部的高四字,低四字不变

SSE

MOVSS

32

源最低双字(仅寄存器),比特32 ~ 127不变

SSE

MOVSD

64

源低四字(仅寄存器),高四字不变

SSE2

PUNPCKLBW

8

源与目标的低8字节交织

SSE2

PUNPCKLWD

16

源与目标的低4字交织

SSE2

PUNPCKLDQ

32

源与目标的低2双字交织

SSE2

PUNPCKLQDQ

64

源低半部的高四字,低四字不变

SSE2

PUNPCKHBW

8

源与目标的高8字节交织

SSE2

PUNPCKHWD

16

源与目标的高4字交织

SSE2

PUNPCKHDQ

32

源与目标的高2双字交织

SSE2

PUNPCKHQDQ

64

目标高半部低四字,源高半部高四字

SSE2

PACKUSWB

8

目标8字的低8字节,源8字的高8字节。通过无符号饱和转换。

SSE2

PACKSSWB

8

目标8字的低8字节,源8字的高8字节。通过有符号饱和转换。

SSE2

PACKSSDW

16

目标4双字的低4字,源4双字的高4字。通过有符号饱和转换。

SSE2

PINSRB

8

向量中插入字节

SSE4.1

PINSRW

16

向量中插入字

SSE

PINSRD

32

向量中插入双字

SSE4.1

INSERTPS

32

向量中插入双字

SSE4.1

PINSRQ

64

向量中插入四字

SSE4.1

VINSERTF128

128

I向量中插入xmmword

AVX

PALIGNR

8

双精度偏移(类似于SHRD

Suppl. SSE3

PBLENDW

16

从两个不同的源混合

SSE4.1

BLENDPS

32

从两个不同的源混合

SSE4.1

BLENDPD

64

从两个不同的源混合

SSE4.1

PBLENDVB

8

多路选择器(Multiplexer

SSE4.1

BLENDVPS

32

多路选择器

SSE4.1

BLENDVPD

64

多路选择器

SSE4.1

VPCMOV

1

多路选择器

AMD XOP

VPBLENDD

32

多路选择器

AVX2

VPBLENDMB

8

使用掩码作为选择子的多路选择器

AVX512BW

VPBLENDMW

16

使用掩码作为选择子的多路选择器

AVX512BW

VPBLENDMD

32

使用掩码作为选择子的多路选择器

AVX512

VPBLENDMQ

64

使用掩码作为选择子的多路选择器

AVX512

VBLENDMPS

32

使用掩码作为选择子的混合

AVX512

VBLENDMPD

64

使用掩码作为选择子的混合

AVX512

表13.6. 合并数据

向一个寄存器的所有元素广播数据

指令

数据块大小,比特

描述

指令集

PSHUFD xmm2, xmm1, 0

32

广播双字

SSE2

PSHUFD xmm2, xmm1, 0EEH

64

广播四字

SSE2

MOVDDUP

64

广播四字

SSE3

MOVSLDUP

32

双字022个拷贝

SSE3

MOVSHDUP

32

双字132个拷贝

SSE3

VBROADCASTSS

32

从内存广播双字

AVX

VBROADCASTSS

32

从寄存器广播双字

AVX512

VBROADCASTSD

64

从内存广播四字

AVX

VBROADCASTSD

64

从寄存器广播四字

AVX512

VBROADCASTF128

128

从内存广播16字节

AVX

VPBROADCASTB

8

从寄存器或内存广播字节

AVX2

VPBROADCASTW

16

从寄存器或内存广播字

AVX2

VPBROADCASTD

32

从寄存器或内存广播双字

AVX2

VPBROADCASTQ

64

从寄存器或内存广播四字

AVX2

VBROADCASTF32X2

64

从寄存器或内存广播四字

AVX512DQ

VBROADCASTF32X4

128

从内存广播16字节

AVX512

VPBROADCASTMB2Q

8

向向量广播掩码寄存器

AVX512CD

VPBROADCASTMW2D

16

向向量广播掩码寄存器

AVX512CD

表13.7. 移动与广播数据

除了这些指令,许多AVX512指令有一个选项来使用广播内存操作数。

从不同内存位置合并数据到一个向量

指令

数据块大小,比特

描述

指令集

VPGATHERDD

32

使用双字索引收集双字

AVX2

VPGATHERQD

32

使用四字索引收集双字

AVX2

VPGATHERDQ

64

使用双字索引收集四字

AVX2

VPGATHERQQ

64

使用四字索引收集四字

AVX2

VGATHERDPS

32

使用双字索引收集双字

AVX2

VGATHERQPS

32

使用四字索引收集双字

AVX2

VGATHERDPD

64

使用双字索引收集四字

AVX2

VGATHERQPD

64

使用四字索引收集四字

AVX2

表13.8. 收集指令

向量表查找

如果整个表可以包含在一个或几个向量寄存器里,排列指令可用于表查找。如果表太大,你必须使用更慢的收集指令进行表查找。

例子:水平加法

下面的例子展示了如何相加一个向量的所有元素

; Example 13.14a. Add 16 elements in vector of 8-bit unsigned integers

; (SSE2)

movaps xmm1, [source]                                                  ; Source vector, 16 8-bit unsigned integers

pxor xmm0, xmm0                                                           ; 0

psadbw xmm1, xmm0                                                     ; Sum of 8 differences

pshufd xmm0, xmm1, 0EH                                              ; Get bit 64-127 from xmm1

paddd xmm0, xmm1                                                        ; Sum

movd [sum], xmm0                                                          ; Store sum

 

; Example 13.14b. Add eight elements in vector of 16-bit integers

; (SUPPL. SSE3)

movaps xmm0, [source]                                                  ; Source vector, 8 16-bit integers

phaddw xmm0, xmm0

phaddw xmm0, xmm0

phaddw xmm0, xmm0

movq [sum], xmm0                                                          ; Store sum

 

; Example 13.14c. Add eight elements in vector of 32-bit integers

; (AVX)

vmovaps ymm0, [source]                                                ; Source vector, 8 32-bit floats

vextractf128 xmm1, ymm0, 1                                         ; Get upper half

vaddps xmm0, xmm0, xmm1                                          ; Add

vhaddps xmm0, xmm0, xmm0

vhaddps xmm0, xmm0, xmm0

vmovsd [sum], xmm0                                                      ; Store sum

    1. 产生常量

没有将常量移入一个XMM寄存器的指令。将常量放入XMM寄存器的缺省方式是从内存常量读入。如果缓存不命中罕见,这也是最高效的方式。但如果缓存不命中频繁,我们可能要寻找替代方案。

一个替代方案是将常量从静态内存位置拷贝到最里层循环外的栈,在最里层循环里使用栈拷贝。栈上内存位置导致缓存不命中的可能性小于常量数据段的内存位置。不过,在库函数中,这个选项也许是不可能的。

第二个替代方案是使用整数指令将常量保存到栈内存,然后从栈内存将值读入XMM寄存器。

第三个替代方案是通过聪明地使用各种指令产生常量。这不使用数据缓存,但在代码缓存中占据更多空间。代码缓存不太可能产生缓存不命中,因为代码是连续的。

常量可以重用许多次,只要别的什么不需要这个寄存器。

下面表13.9展示如何在XMM寄存器中产生各种整数常量。在向量所有元素中产生相同值。

在XMM寄存器中产生整数向量常量

8

16

32

64

0

pxor xmm0,xmm0

pxor xmm0,xmm0

pxor xmm0,xmm0

pxor xmm0,xmm0

1

pcmpeqw xmm0,xmm0 pabsb xmm0,xmm0

pcmpeqw xmm0,xmm0 psrlw xmm0,15

pcmpeqd xmm0,xmm0 psrld xmm0,31

pcmpeqw xmm0,xmm0 psrlq xmm0,63

2

pcmpeqw xmm0,xmm0

pabsb xmm0,xmm0

paddb xmm0,xmm0

pcmpeqw xmm0,xmm0 psrlw xmm0,15

psllw xmm0,1

pcmpeqd xmm0,xmm0 psrld xmm0,31

pslld xmm0,1

pcmpeqw xmm0,xmm0 psrlq xmm0,63

psllq xmm0,1

3

pcmpeqw xmm0,xmm0 psrlw xmm0,14

packuswb xmm0,xmm0

pcmpeqw xmm0,xmm0 psrlw xmm0,14

pcmpeqd xmm0,xmm0 psrld xmm0,30

pcmpeqw xmm0,xmm0 psrlq xmm0,62

4

pcmpeqw xmm0,xmm0

pabsb xmm0,xmm0

psllw xmm0,2

pcmpeqw xmm0,xmm0 psrlw xmm0,15

psllw xmm0,2

pcmpeqd xmm0,xmm0 psrld xmm0,31

pslld xmm0,2

pcmpeqw xmm0,xmm0 psrlq xmm0,63

psllq xmm0,2

-1

pcmpeqw xmm0,xmm0

pcmpeqw xmm0,xmm0

pcmpeqd xmm0,xmm0

pcmpeqw xmm0,xmm0

-2

pcmpeqw xmm0,xmm0

paddb xmm0,xmm0

pcmpeqw xmm0,xmm0

psllw xmm0,1

pcmpeqd xmm0,xmm0

pslld xmm0,1

pcmpeqw xmm0,xmm0

psllq xmm0,1

其他值

mov eax, value*01010101H

movd xmm0,eax

pshufd xmm0,xmm0,0

mov eax, value*10001H movd xmm0,eax

pshufd xmm0,xmm0,0

mov eax,value

movd xmm0,eax

pshufd xmm0,xmm0,0

mov rax,value

movq xmm0,rax punpcklqdq xmm0,xmm0(仅64位模式)

表13.9. 产生整数向量常量

下面表13.10展示如何在XMM寄存器中产生各种浮点常量。在向量所有元素中产生相同值:

在XMM寄存器中产生浮点常量

单精度标量

双精度标量

单精度向量

双精度向量

0.0

pxor xmm0,xmm0

pxor xmm0,xmm0

pxor xmm0,xmm0

pxor xmm0,xmm0

0.5

pcmpeqw xmm0,xmm0

pslld xmm0,26

psrld xmm0,2

pcmpeqw xmm0,xmm0

psllq xmm0,55

psrlq xmm0,2

pcmpeqw xmm0,xmm0

pslld xmm0,26

psrld xmm0,2

pcmpeqw xmm0,xmm0

psllq xmm0,55

psrlq xmm0,2

1.0

pcmpeqw xmm0,xmm0

pslld xmm0,25

psrld xmm0,2

pcmpeqw xmm0,xmm0 psllq xmm0,54

psrlq xmm0,2

pcmpeqw xmm0,xmm0 pslld xmm0,25

psrld xmm0,2

pcmpeqw xmm0,xmm0

psllq xmm0,54

psrlq xmm0,2

1.5

pcmpeqw xmm0,xmm0

pslld xmm0,24

psrld xmm0,2

pcmpeqw xmm0,xmm0

psllq xmm0,53

psrlq xmm0,2

pcmpeqw xmm0,xmm0

pslld xmm0,24

psrld xmm0,2

pcmpeqw xmm0,xmm0

psllq xmm0,53

psrlq xmm0,2

2.0

pcmpeqw xmm0,xmm0

pslld xmm0,31

psrld xmm0,1

pcmpeqw xmm0,xmm0

psllq xmm0,63

psrlq xmm0,1

pcmpeqw xmm0,xmm0

pslld xmm0,31

psrld xmm0,1

pcmpeqw xmm0,xmm0

psllq xmm0,63

psrlq xmm0,1

-2.0

pcmpeqw xmm0,xmm0

pslld xmm0,30

pcmpeqw xmm0,xmm0

psllq xmm0,62

pcmpeqw xmm0,xmm0 pslld xmm0,30

pcmpeqw xmm0,xmm0

psllq xmm0,62

符号位

pcmpeqw xmm0,xmm0

pslld xmm0,31

pcmpeqw xmm0,xmm0

psllq xmm0,63

pcmpeqw xmm0,xmm0

pslld xmm0,31

pcmpeqw xmm0,xmm0

psllq xmm0,63

非符号位

pcmpeqw xmm0,xmm0

psrld xmm0,1

pcmpeqw xmm0,xmm0

psrlq xmm0,1

pcmpeqw xmm0,xmm0

psrld xmm0,1

pcmpeqw xmm0,xmm0

psrlq xmm0,1

其他值(32位模式)

mov eax, value

movd xmm0,eax

mov eax, value>>32

movd xmm0,eax

psllq xmm0,32

mov eax, value

movd xmm0,eax

shufps xmm0,xmm0,0

mov eax, value>>32 movd xmm0,eax

pshufd xmm0,xmm0,22H

其他值(64位模式)

mov eax, value

movd xmm0,eax

mov rax, value

movq xmm0,rax

mov eax, value

movd xmm0,eax

shufps xmm0,xmm0,0

mov rax, value

movq xmm0,rax

shufpd xmm0,xmm0,0

表13.10. 产生浮点向量常量

“符号位”是一个带有符号位、其他比特都是零的值。这用于改变变量的符号设置。例如在xmm0里改变一个2*双精度向量的符号:

; Example 13.15. Change sign of 2*double vector

pcmpeqw xmm7, xmm7                                                                   ; All 1's

psllq xmm7, 63                                                                                   ; Shift out the lower 63 1's

xorpd xmm0, xmm7                                                                          ; Flip sign bit of xmm0

“非符号位”是“符号位”的取反值。它的符号位是0,其他位是1。这用于获取变量的绝对值。例如获取xmm0中一个2*双精度向量的绝对值:

; Example 13.16. Absolute value of 2*double vector

pcmpeqw xmm6, xmm6                                                                   ; All 1's

psrlq xmm6, 1                                                                                     ; Shift out the highest bit

andpd xmm0, xmm6                                                                          ; Set sign bit to 0

在32位模式中产生一个任意双精度值更复杂。表13.10中的方法仅用于值64位表示的高32位,假定该值较低二进制数都是0,或者一个可接受的近似。例如,要产生双精度值9.25,我们首先使用编译器或汇编器找出9.25的16字节表示是4022800000000000H。低32位可以忽略,因此我们可以这样做:

; Example 13.17a. Set 2*double vector to arbitrary value (32 bit mode)

mov eax, 40228000H                                                                         ; High 32 bits of 9.25

movd xmm0, eax                                                                                ; Move to xmm0

pshufd xmm0, xmm0, 22H                                                                ; Get value into dword 1 and 3

在64位模式中,我们可以使用64位整数寄存器:

; Example 13.17b. Set 2*double vector to arbitrary value (64 bit mode)

mov rax, 4022800000000000H                                                        ; Full representation of 9.25

movq xmm0, rax                                                                                 ; Move to xmm0

shufpd xmm0, xmm0, 0                                                                     ; Broadcast

注意,对在通用寄存器与XMM寄存器间移动64比特的指令,某些汇编器使用非常误导的名字movd,而不是movq。

    1. 访问非对齐数据与向量局部

所有使用向量寄存器读写的数据最好对齐到向量大小。如何对齐数据,参考第78页。

较旧的处理器对访问非对齐数据有性能损失,而现代处理器处理非对齐数据几乎与对齐数据一样快。

存在对齐是不可能的情形,例如,一个库函数接收一个数组指针,未知该数组是否对齐。

下面的方法可用于读非对齐向量:

使用非对齐读指令

指令MOVDQU,MOVUPS,MOVUPD与LDDQU都能读非对齐向量。在P4E与PM处理器上,LDDQU比其他都快,但在更新的处理器上不是。在较旧的Intel处理器及Intel Atom上,非对齐读指令相对慢,但在Nehalem与更新的Intel处理器,以及AMD与VIA处理器上快。

; Example 13.18. Unaligned vector read

; esi contains pointer to unaligned array

movdqu xmm0, [esi]                                                        ; Read vector unaligned

在当代处理器上,如果数据实际上是对齐的,使用非对齐指令MOVDQU,而不是MOVDQA,并没有性能损失。因此,如果你不确定数据是否对齐,使用MOVDQU是方便的。

使用VEX前缀指令

带有VEX前缀的指令允许非对齐内存操作数,而没有VEX前缀的相同指令将产生一个异常,如果操作数没有正确对齐:

; Example 13.19. VEX versus non-VEX access to unaligned data

movups xmm1, [esi]                                                         ; unaligned operand must be read first

addps xmm0, xmm1

; VEX prefix allows unaligned operand

vaddps xmm0, [esi]                                                           ; unaligned operand allowed here

如果使用了256位YMM寄存器,不应该混用VEX与非VEX代码。不过,混用AVX与AVX512指令是可以的。参考章节13.1。

将非对齐读分解为两个

读不超过8字节的指令没有对齐要求,通常相当快,除非这个读跨了缓存边界。例如:

; Example 13.20. Unaligned 16 bytes read split in two

; esi contains pointer to unaligned array

movq xmm0, qword ptr [esi]                                          ; Read lower half of vector

movhps xmm0, qword ptr [esi+8]                                  ; Read upper half of vector

在较旧的Intel处理器上,这个方法比使用非对齐读指令要快,但在Intel Nehalem及更新的Intel处理器是不是。没有理由在任何较新的处理器上使用这个方法。

部分重叠读

在非对齐地址进行第一个读,在后面最接近的16字节边界处进行下一个读。因此,前两个读将可能重叠:

; Example 13.21. First unaligned read overlaps next aligned read

; esi contains pointer to unaligned array

movdqu xmm1, [esi]                                                         ; Read vector unaligned

add esi, 10H

and esi, -10H                                                                      ; = nearest following 16B boundary

movdqa xmm2, [esi]                                                         ; Read next vector aligned

这里,在xmm1与xmm2里的数据将部分重叠,如果esi不能被16整除。当然,这仅后面的算法允许重复数据才可用。最后的向量读也会与倒数第二重叠,如果数组末尾没有对齐。

从前面最接近的16字节边界读

从一个非对齐数组前面最接近的16字节边界开始读是可能的。无关的数据将放入向量寄存器的部分,这些数据必须被忽略。类似的,最后的读可能超出数组末尾,直到下一个16字节边界:

; Example 13.22. Reading from nearest preceding 16-bytes boundary

; esi contains pointer to unaligned array

mov eax, esi ; Copy pointer

and esi, -10H ; Round down to value divisible by 16

and eax, 0FH ; Array is misaligned by this value

movdqa xmm1, [esi] ; Read from preceding 16B boundary

movdqa xmm2, [esi+10H] ; Read next block

在上面的例子中,xmm1包含eax个垃圾字节,后跟(16 – eax)个有用的字节。Xmm2包含向量剩下的eax个字节,后跟要么是垃圾字节、要么属于下一个向量的(16 – eax)个字节。因此,eax中的值应该用于掩蔽或忽略寄存器包含垃圾数据的部分。

这里,我们利用向量大小、缓存行大小与内存页大小总是2的指数这个事实。因此,缓存行边界与内存页边界将与向量边界吻合。当我们使用这个方法读某些无关数据时,我们将不载入任何不必要的缓存行,因为缓存行总是16的某个倍数。因此,读无关数据没有代价。更重要的,我们不会从读不存在的内存地址得到一个读失败,因为总是以4096(=212)个或更多字节的页分配数据内存,因此对齐的向量读不会跨页边界。

这个方法的使用展示在www.agner.org/optimize/asmexamples.zip附录的strlenSSE2.asm例子中。

将两个非对齐向量部分合并为一个对齐向量

可以通过将两个寄存器的有效部分合并为一整个寄存器,扩展上面的方法。如果例子13.22中的xmm1右移非对齐量(eax),xmm2左移(16 – eax),那么这两个寄存器可以合并为一个仅包含有效数据的寄存器。

不幸的是,没有指令根据一个变量计数器,将整个向量寄存器向左或向右偏移(类似于shr eax, cl),因此我们必须使用一条混排指令。下一个例子使用一个字节混排指令,PSHUFB,通过合适的掩码值左移或右移一个向量寄存器:

; Example 13.23. Combining two unaligned parts into one vector.

; (Supplementary-SSE3 instruction set required)

; This example takes the squareroot of n floats in an unaligned

; array src and stores the result in an aligned array dest.

; C++ code:

; const int n = 100;

; float * src;

; float dest[n];

; for (int i=0; i<n; i++) dest[i] = sqrt(src[i]);

 

; Define masks for using PSHUFB instruction as shift instruction:

; The 16 bytes from SMask[16+a] will shift right a bytes

; The 16 bytes from SMask[16-a] will shift left a bytes

.data

SMask label xmmword

      DB -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1

      DB 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15

      DB -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1

 

.code

      mov esi, src                                                                         ; Unaligned pointer src

      lea edi, dest                                                                        ; Aligned array dest

      mov eax, esi

      and eax, 0FH                                                                       ; Get misalignment, a

      movdqu xmm4, [SMask+10H+eax]                                 ; Mask for shift right by a

      movdqu xmm5, [SMask+eax]                                          ; Mask for shift left by 16-a

      and esi, -10H                                                                       ; Nearest preceding 16B boundary

      xor ecx, ecx                                                                         ; Loop counter i = 0

 

L: ; Loop

      movdqa xmm1, [esi+ecx]                                                  ; Read from preceding boundary

      movdqa xmm2, [esi+ecx+10H]                                         ; Read next block

      pshufb xmm1, xmm4                                                         ; shift right by a

      pshufb xmm2, xmm5                                                         ; shift left by 16-a

      por xmm1, xmm2                                                               ; combine blocks

      sqrtps xmm1, xmm1                                                          ; compute four squareroots

      movaps [edi+ecx], xmm1                                                  ; Save result aligned

      add ecx, 10H                                                                        ; Loop to next four values

      cmp ecx, 400 ; 4*n

      jb L                                                                                         ; Loop

尽可能将SMask对齐到64,避免跨缓存行边界的非对齐读。

如果非对齐量(eax)已知为常量,这个方法变得更容易。我们可以使用PSRLDQ与PSLLDQ,而不是PSHUFB,将寄存器右移及左移。PSRLDQ与PSLLDQ属于SSE2指令集,而PSHUFB要求SSSE3。在偏移值是常量时,通过使用PALIGNR指令(SSSE3),指令数可以减少。在非对齐量是4,8或12时,如下面例子13.24b所示,使用MOVSS或MOVSD的某些技巧是可能的。

最快的实现有16个代码分支,eax中每个可能对齐值一个。参考www.agner.org/optimize/asmexamples.zip附录中memcpy与memset的例子。

合并同一个数组的非对齐向量部分

当在同一个数组的两个元素间执行一个操作,且这两个元素间的距离不是向量大小的倍数,非对齐是不可避免的。在下面的C++例子里,每个数组元素被加到前面的元素:

// Example 13.24a. Add each array element to the preceding element

float x[100];

for (int i = 0; i < 99; i++) x[i] += x[i+1];

这里x[i+1]不可避免地不对齐于x[i]。我们仍然可以使用合并两个对齐向量部分的方法,利用非对齐量是一个已知常量4的事实。

; Example 13.24b. Add each array element to the preceding element

      xor ecx, ecx                                                                         ; Loop counter i = 0

      lea esi, x                                                                               ; Pointer to aligned array

      movaps xmm0, [esi]                                                          ; x3,x2,x1,x0

L:                                                                                                 ; Loop

      movaps xmm2, xmm0                                                       ; x3,x2,x1,x0

      movaps xmm1, [esi+ecx+10H]                                         ; x7,x6,x5,x4

      ; Use trick with movss to combine parts of two vectors

      movss xmm2, xmm1                                                          ; x3,x2,x1,x4

      ; Rotate right by 4 bytes

      shufps xmm2, xmm2, 00111001B                                   ; x4,x3,x2,x1

      addps xmm0, xmm2                                                          ; x3+x4, x2+x3, x1+x2, x0+x1

      movaps [esi+ecx], xmm0                                                  ; save aligned

      movaps xmm0, xmm1                                                       ; Save for next iteration

      add ecx, 16                                                                          ; i += 4

      cmp ecx, 400-16                                                                 ; Repeat for 96 values

      jb L

 

      ; Do the last odd one:

L: movaps xmm2, xmm0                                                          ; x99,x98,x97,x96

      xorps xmm1, xmm1                                                            ; 0, 0, 0, 0

      movss xmm2, xmm1                                                          ; x99, x98, x97, 0

      ; Rotate right by 4 bytes

      shufps xmm2, xmm2, 00111001B                                   ; 0, x99, x98, x97

      addps xmm0, xmm2                                                          ; x99+0, x98+x99, x97+x98, x96+x97

      movaps [esi+ecx], xmm0                                                  ; save aligned

现在,我们已经讨论了读非对齐向量的几个方法。当然,我们还必须讨论如何写非对齐地址的向量。某些方法是大同小异的。

一次写8个字节

写不超过8字节的指令没有对齐要求,通常相当快,除非写跨了缓存行边界。例如:

; Example 13.26. Unaligned vector read split in two

; edi contains pointer to unaligned array

movq qword ptr [edi], xmm0                                                 ; Write lower half of vector

movhps qword ptr [edi+8], xmm0                                        ; Write upper half of vector

在较旧的Intel处理器上,这个方法比使用MOVDQU快,但在Nehalem及更新的Intel处理器或现代AMD与VIA Intel处理器上不是。

部分重叠写

在非对齐地址进行第一个写,在后面最接近的16字节边界处进行下一个写。因此,前两个读将可能重叠:

; Example 13.27. First unaligned write overlaps next aligned write

; edi contains pointer to unaligned array

movdqu [edi], xmm1                                                              ; Write vector unaligned

add edi, 10H

and edi, 0FH                                                                             ; = nearest following 16B boundary

movdqa [edi], xmm2                                                              ; Write next vector aligned

这里,在xmm1与xmm2里的数据将部分重叠,如果esi不能被16整除。当然,这仅后面的算法允许重复数据才可用。最后的向量读也会与倒数第二重叠,如果数组末尾没有对齐。参考在www.agner.org/optimize/asmexamples.zip附录中的memset例子。

分开写开头与末尾

使用非向量指令写一个非对齐数组的开头,直到第一个16字节边界,然后从最后的16字节边界写到数组末尾。参考www.agner.org/optimize/asmexamples.zip附录里的memcpy例子。

使用非对齐写指令

指令movdqu,movups与movupd都能写非对齐向量。在较旧的Intel处理器上非对齐写指令相对慢,但在Nehalem与更新的Intel处理器,以及现代的AMD与VIA处理器上快。

; Example 13.25. Unaligned vector write

; edi contains pointer to unaligned array

movdqu [edi], xmm0                                                               ; Write vector unaligned

使用掩蔽写

指令VPMASKMOVD(AVX2)与VMASKMOVPS(AVX)等可用于写一个非对齐数组的第一部分,直到第一个16字节边界,以及最后16字节边界后的最后部分。

非VEX版本的掩蔽移动指令极慢,比如MASKMOVDQU,因为它们绕过缓存,写入主内存(所谓的非临时写)。在Intel处理器上VEX版本比非VEX版本快,但在AMD Bulldozer及Piledriver处理器上不是。

这些指令绝对应该避免。

    1. 通用寄存器中的向量操作

有时,在32位或64位通用寄存器中处理封装数据是可能的。你可以在整数操作比向量操作快的处理器上,或者没有合适的向量操作时,使用这个方法。

一个64位寄存器可以保存2个32位整数,4个16位整数,8个8位整数,或64个布尔值。当在32位或64位寄存器中的封装整数上进行计算时,如果溢出可能发生,你必须特别小心避免一个操作数的进位进入下一个操作数。如果所有操作数都是正数且很小不会发生溢出,进位就不会出现。例如,如果确定不会发生溢出,你可以把4个正16位整数封装入RAX,使用ADD RAX, RBX,而不是PADDW MM0, MM1。如果不能排除进位,那么你必须屏蔽掉最高位,如下面的例子,它将2加到EAX中的所有4个字节上:

; Example 13.28. Byte vector addition in 32-bit register

mov eax, [esi]                                                                           ; read 4-bytes operand

mov ebx, eax                                                                            ; copy into ebx

and eax, 7f7f7f7fh                                                                   ; get lower 7 bits of each byte in eax

xor ebx, eax                                                                              ; get the highest bit of each byte

add eax, 02020202h                                                               ; add desired value to all four bytes

xor eax, ebx                                                                              ; combine bits again

mov [edi], eax                                                                          ; store result

这里,每个字节的最高位被屏蔽了,以避免每个字节在加1时可能的进位。代码使用XOR,而不是ADD,来放回最高位,以避免进位。如果第二个加数有可能设置了最高位,它也必须被屏蔽。如果两个加数都没有最高位,不需要屏蔽。

查找一个特定字节也是可能的。这个C代码通过检查一个32位整数是否至少有一个字节为零,展示了这个原理:

// Example 13.29. Return nonzero if dword contains null byte

inline int dword_has_nullbyte(int w) {

return ((w - 0x01010101) & ~w & 0x80808080);}

如果w的所有4个字节都是非零,输出是0。输出将在第一个为零字节处有0x80。如果超过一个字节为零,无需表示后续字节。(这个方法由Alan Mycroft发明并在1987年发布。我在1996年在本手册的第一版中发布了相同的方法,不知道Mycroft在我之前做了相同的发明)。

这个原理可用于通过查找第一个零字节,找出一个零结尾字符串的长度。它比使用REPNE SCASB快:

; Example 13.30, optimized strlen procedure (32-bit):

_strlen PROC NEAR

; extern "C" int strlen (const char * s);

      push ebx                                                                                           ; ebx must be saved

      mov ecx, [esp+8]                                                                             ; get pointer to string

      mov eax, ecx                                                                                    ; copy pointer

      and ecx, 3                                                                                         ; lower 2 bits, check alignment

      jz L2                                                                                                    ; s is aligned by 4. Go to loop

      and eax, -4                                                                                        ; align pointer by 4

      mov ebx, [eax]                                                                                 ; read from preceding boundary

      shl ecx, 3                                                                                           ; *8 = displacement in bits

      mov edx, -1

      shl edx, cl ; make byte mask

      not edx                                                                                              ; mask = 0FFH for false bytes

      or ebx, edx                                                                                        ; mask out false bytes

      ; check first four bytes for zero

      lea ecx, [ebx-01010101H]                                                              ; subtract 1 from each byte

      not ebx                                                                                              ; invert all bytes

      and ecx, ebx ; and these two

      and ecx, 80808080H                                                                       ; test all sign bits

      jnz L3                                                                                                 ; zero-byte found

 

      ; Main loop, read 4 bytes aligned

L1: add eax, 4                                                                                         ; increment pointer

L2: mov ebx, [eax]                                                                                 ; read 4 bytes of string

      lea ecx, [ebx-01010101H]                                                              ; subtract 1 from each byte

      not ebx                                                                                              ; invert all bytes

      and ecx, ebx ; and these two

      and ecx, 80808080H                                                                       ; test all sign bits

      jz L1                                                                                                    ; no zero bytes, continue loop

 

L3: bsf ecx, ecx                                                                                       ; find right-most 1-bit

      shr ecx, 3                                                                                           ; divide by 8 = byte index

      sub eax, [esp+8]                                                                               ; subtract start address

      add eax, ecx                                                                                      ; add index to byte

      pop ebx                                                                                              ; restore ebx

      ret                                                                                                       ; return value in eax

_strlen ENDP

对齐检查确保我们仅从对齐到4的地址读。这个函数可能在这个字符串之前和之后进行读,但因为所有的读都是对齐的,我们不会不必要地跨任何缓存行边界或页边界。最重要的,读超出分配的内存时,不会有任何页失败,因为页边界总是对齐到212或更大。

如果SSE2指令集可用,那么它可能比使用具有相同对齐技巧的XMM指令更快。例子13.30以及类似使用XMM寄存器的函数,在www.agner.org/optimize/asmexamples.zip的附录里。

其他查找特殊字节的普通函数,比如strcpy,strchr,memchr可用使用同样的技巧。要查找非零字节,只要与期望的字节XOR,如这个C代码所示:

// Example 13.31. Return nonzero if byte b contained in dword w

inline int dword_has_byte(int w, unsigned char b) {

      w ^= b * 0x01010101;

      return ((w - 0x01010101) & ~w & 0x80808080);}

注意,如果回退查找(比如strrchr),在b在w中多次出现的情形下,上面的方法将表示第一次出现的位置。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
混合编程是指在一个程序中同时使用汇编语言和C语言编写代码。这样做的好处是可以充分发挥汇编语言的高效性能,同时又可以利用C语言的易读性和可移植性。 下面是一个简单的例程,展示了如何在C语言中调用汇编语言函数: ```c #include <stdio.h> extern int asm_add(int a, int b); int main() { int a = 10, b = 20, sum; sum = asm_add(a, b); printf("sum = %d\n", sum); return 0; } ``` 上面的代码中,`asm_add`是一个汇编语言函数,在C语言中通过`extern`关键字声明。在`main`函数中,我们调用了`asm_add`函数,并将结果打印出来。 下面是`asm_add`函数的汇编语言实现: ```asm section .text global asm_add asm_add: mov eax, edi add eax, esi ret ``` 上面的代码中,`asm_add`函数接收两个参数,分别存放在寄存器`edi`和`esi`中。函数实现将这两个参数相加,并将结果返回。 对于上面的例程,我们需要将C语言和汇编语言代码分别保存为`.c`和`.asm`文件,并使用汇编器和编译器编译链接: ```bash nasm -f elf64 -o asm_add.o asm_add.asm gcc -c main.c gcc -o main main.o asm_add.o ``` 上述命令中,`-f elf64`参数指定了汇编语言程序的目标平台为x86-64,`-c`参数表示只编译不链接,最后一个命令将编译好的目标文件链接成可执行文件。 以上就是一个简单的混合编程例程,通过在C语言中调用汇编语言函数,实现了高效的计算。实际应用中,混合编程可以用于底层的系统编程和优化性能要求较高的算法实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值