gpt4 book ai didi

loops - 时间性能 SIMD 汇编程序 : longer loop executes faster

转载 作者:行者123 更新时间:2023-12-02 03:16:39 24 4
gpt4 key购买 nike

我最近一直在学习 SIMD in assembly (x86_64),并且有一些意想不到的结果。归结为以下内容。

我有两个循环运行多次的程序。第一个程序包含一个执行 4 条 SIMD 指令的循环,第二个程序包含这个完全相同的循环和一个额外的指令。代码如下所示:

第一个程序:

section .bss
doublestorage: resb 8

section .text
global _start

_start:
mov rax, 0x0000000100000001
mov [doublestorage], rax
cvtpi2pd xmm1, [doublestorage]
cvtpi2pd xmm2, [doublestorage]
cvtpi2pd xmm3, [doublestorage]
cvtpi2pd xmm4, [doublestorage]
cvtpi2pd xmm5, [doublestorage]
cvtpi2pd xmm6, [doublestorage]
cvtpi2pd xmm7, [doublestorage]

mov rax, (1 << 31)
loop:
movupd xmm1, xmm3
movupd xmm2, xmm5
divpd xmm1, xmm2
addpd xmm4, xmm1
dec rax
jnz loop

mov rax, 60
mov rdi, 0
syscall

第二个程序:

section .bss
doublestorage: resb 8

section .text
global _start

_start:
mov rax, 0x0000000100000001
mov [doublestorage], rax
cvtpi2pd xmm1, [doublestorage]
cvtpi2pd xmm2, [doublestorage]
cvtpi2pd xmm3, [doublestorage]
cvtpi2pd xmm4, [doublestorage]
cvtpi2pd xmm5, [doublestorage]
cvtpi2pd xmm6, [doublestorage]
cvtpi2pd xmm7, [doublestorage]

mov rax, (1 << 31)
loop:
movupd xmm1, xmm3
movupd xmm2, xmm5
divpd xmm1, xmm2
addpd xmm4, xmm1
movupd xmm6, xmm7
dec rax
jnz loop

mov rax, 60
mov rdi, 0
syscall

现在,我的想法是:第二个程序有更多的指令要执行,因此执行时间会长得多。但是,如果我对两个程序都计时,那么第二个程序比第一个程序花费的时间更少。我总共运行了这两个程序 100 次,结果是:

Runtime first program: mean: 5.6129 s, standard deviation: 0.0156 s
Runtime second program: mean: 5.5056 s, standard deviation: 0.0147 s

我得出结论,第二个程序运行得相当。这些结果对我来说似乎违反直觉,所以我想知道造成这种行为的原因是什么。

为了完整起见,我正在运行 Ubuntu 15.10 和 NASM 编译器 (-elf64),并使用 Intel Core i7-5600。另外,我检查了反汇编,编译器没有进行任何优化:

第一个程序的Objdump:

exec/instr4:     file format elf64-x86-64


Disassembly of section .text:

00000000004000b0 <.text>:
4000b0: 48 b8 01 00 00 00 01 movabs $0x100000001,%rax
4000b7: 00 00 00
4000ba: 48 89 04 25 28 01 60 mov %rax,0x600128
4000c1: 00
4000c2: 66 0f 2a 0c 25 28 01 cvtpi2pd 0x600128,%xmm1
4000c9: 60 00
4000cb: 66 0f 2a 14 25 28 01 cvtpi2pd 0x600128,%xmm2
4000d2: 60 00
4000d4: 66 0f 2a 1c 25 28 01 cvtpi2pd 0x600128,%xmm3
4000db: 60 00
4000dd: 66 0f 2a 24 25 28 01 cvtpi2pd 0x600128,%xmm4
4000e4: 60 00
4000e6: 66 0f 2a 2c 25 28 01 cvtpi2pd 0x600128,%xmm5
4000ed: 60 00
4000ef: 66 0f 2a 34 25 28 01 cvtpi2pd 0x600128,%xmm6
4000f6: 60 00
4000f8: 66 0f 2a 3c 25 28 01 cvtpi2pd 0x600128,%xmm7
4000ff: 60 00
400101: b8 00 00 00 80 mov $0x80000000,%eax
400106: 66 0f 10 cb movupd %xmm3,%xmm1
40010a: 66 0f 10 d5 movupd %xmm5,%xmm2
40010e: 66 0f 5e ca divpd %xmm2,%xmm1
400112: 66 0f 58 e1 addpd %xmm1,%xmm4
400116: 48 ff c8 dec %rax
400119: 75 eb jne 0x400106
40011b: b8 3c 00 00 00 mov $0x3c,%eax
400120: bf 00 00 00 00 mov $0x0,%edi
400125: 0f 05 syscall

第二个程序的Objdump:

exec/instr5:     file format elf64-x86-64


Disassembly of section .text:

00000000004000b0 <.text>:
4000b0: 48 b8 01 00 00 00 01 movabs $0x100000001,%rax
4000b7: 00 00 00
4000ba: 48 89 04 25 2c 01 60 mov %rax,0x60012c
4000c1: 00
4000c2: 66 0f 2a 0c 25 2c 01 cvtpi2pd 0x60012c,%xmm1
4000c9: 60 00
4000cb: 66 0f 2a 14 25 2c 01 cvtpi2pd 0x60012c,%xmm2
4000d2: 60 00
4000d4: 66 0f 2a 1c 25 2c 01 cvtpi2pd 0x60012c,%xmm3
4000db: 60 00
4000dd: 66 0f 2a 24 25 2c 01 cvtpi2pd 0x60012c,%xmm4
4000e4: 60 00
4000e6: 66 0f 2a 2c 25 2c 01 cvtpi2pd 0x60012c,%xmm5
4000ed: 60 00
4000ef: 66 0f 2a 34 25 2c 01 cvtpi2pd 0x60012c,%xmm6
4000f6: 60 00
4000f8: 66 0f 2a 3c 25 2c 01 cvtpi2pd 0x60012c,%xmm7
4000ff: 60 00
400101: b8 00 00 00 80 mov $0x80000000,%eax
400106: 66 0f 10 cb movupd %xmm3,%xmm1
40010a: 66 0f 10 d5 movupd %xmm5,%xmm2
40010e: 66 0f 5e ca divpd %xmm2,%xmm1
400112: 66 0f 58 e1 addpd %xmm1,%xmm4
400116: 66 0f 10 f7 movupd %xmm7,%xmm6
40011a: 48 ff c8 dec %rax
40011d: 75 e7 jne 0x400106
40011f: b8 3c 00 00 00 mov $0x3c,%eax
400124: bf 00 00 00 00 mov $0x0,%edi
400129: 0f 05 syscall

最佳答案

no such thing as an "i7 5600" .我假设你的意思是 i7 5600U ,这是一款低功耗 (15W TDO) CPU,具有 2.6GHz 基础频率/3.2GHz 涡轮频率。

你能仔细检查一下这是否可重现吗?确保 CPU 时钟速度在两次测试中保持恒定,因为您的低功耗 CPU 可能无法在除法单元一直忙碌的情况下全速运行。

也许对于使用性能计数器(例如 perf stat ./a.out)进行测试也很有用,测量核心时钟周期。 (不是“引用”周期。您想计算时钟实际运行的实际周期。)


IACA最多只支持 Haswell。除了两个循环的每次迭代 14c 之外,它没有说明任何其他内容,这会成为除法器吞吐量的瓶颈。 (Agner Fog 对 divpd 的测量值为 one per 8-14c throughput on Haswell, one per 8c on Broadwell。)

a recent question about broadwell throughput ,但这是关于使前端饱和。


这个循环应该纯粹是 divpd 吞吐量 (one per 8c on Broadwell) 的瓶颈。如果效果是真实的,我想出的唯一解释是 movupd insns 之一并不总是被消除,而是从 divpd 中窃取 p0 用于有时一个循环。

循环中的三个未融合域微指令都在不同的端口上运行,因此它们不可能相互延迟。 (p0 上的 divpd,p1 上的 addpd,p6 上的 predicted-taken fused cmp/jcc)。

实际上,即使是该理论也站不住脚。未消除的 movaps xmm,xmm 使用 Broadwell 上的端口 5。我假设 movupd xmm,xmm 的奇怪选择也解码为 port5 uop。 (Agner Fog 甚至没有列出 movups/movupd 的 reg-reg 形式的条目,因为每个人总是使用 movaps。或者 movapd 如果他们喜欢将 insn 类型与数据匹配,即使它长一个字节并且现有的 uarch 不关心 sd 的旁路延迟,只有 movaps 用于 float/double 和 movdqa 用于整数。)


有趣的是,我的 2.4GHz E6600(Conroe/merom 微架构)在 4.5 秒内运行您的循环。 Agner Fog 的表格将 Merom 上的 divpd 列为 5-31c 中的一个。 1.0/1.0 大概发生在 5c 中。 Sandybridge 的 best-case divide 比 Nehalem 慢得多。只有使用 Skylake 时,最佳情况下的吞吐量才能像 Merom 一样快速下降。 (对于 128b divpd,吞吐量固定为每 4c 一个)。


顺便说一句,这是您的代码版本,它使用一些更常规的方法在 regs 中设置 FP 数据:

    default REL              ;  use RIP-relative for access to globals by default, so you don't have to write `[rel my_global]`

section .rodata
; ALIGN 16 ; only desirable if we're doing a 128b load instead of a 64b broadcast-load
one_dp: dq 1.0

section .text

global _start
_start:
mov rax, 0x0000000100000001
mov [rsp-16], rax ; if you're going to store/reload, use the stack for scratch space, not a static location that will probably cache-miss.
cvtpi2pd xmm1, [rsp-16] ; this is the form with xmm, mm/m64 args. Interestingly, for the memory operand form, this should perform the same but saves a byte in the encoding.
cvtdq2pd xmm8, [rsp-16] ; this is the "normal" insn, with xmm, xmm/m64 args.
movq xmm9, rax
cvtdq2pd xmm9, xmm9
; Fun fact: 64bit int <-> FP is only available for scalar until AVX-512 introduces packed conversions for qword integer vectors.

;mov eax, 1 ; still 1 from earlier
cvtsi2sd xmm2, eax
unpcklpd xmm2, xmm2 ; broadcast

pcmpeqw xmm3,xmm3 ; generate the constant on the fly, from Agner Fog's asm guide
psllq xmm3, 54
psrlq xmm3, 2 ; (double)1.0 in both halves.

movaps xmm4, xmm3 ; duplicate the data instead of re-doing the conversion. Shorter and cheaper.
movaps xmm5, xmm3
movaps xmm6, xmm3

;movaps xmm7, [ones] ; load 128b constant
movddup xmm7, [one_dp] ; broadcast-load

mov eax, (1 << 31) ; 1<<31 fits in a 32bit reg just fine.
;IACA_start
.loop:
;movupd xmm1, xmm3
;movupd xmm2, xmm5
movaps xmm1, xmm3 ; prob. no perf diff, but weird to see movupd used for reg-reg moves
movaps xmm2, xmm5
divpd xmm1, xmm2
addpd xmm4, xmm1
;movaps xmm6, xmm7
dec eax
jnz .loop
;IACA_end

mov eax, 60
xor edi,edi
syscall ; exit(0)

对于 IACA,我使用了:

%macro  IACA_start 0
mov ebx, 111
db 0x64, 0x67, 0x90
%endmacro
%macro IACA_end 0
mov ebx, 222
db 0x64, 0x67, 0x90
%endmacro

您是否尝试过进一步简化循环?

.loop:
movapd xmm1, xmm3 ; replace the only operand that div writes
divpd xmm1, xmm2
addpd xmm4, xmm1
dec eax
jnz .loop

然后循环只有 4 个融合域微指令。应该使差异为零,因为 divpd 吞吐量应该仍然是唯一的瓶颈。

或使用 AVX:

    vdivpd    xmm0, xmm1, xmm2
vaddpd xmm4, xmm4, xmm0
cmp/jcc

关于loops - 时间性能 SIMD 汇编程序 : longer loop executes faster,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36652462/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com