Assembly X86

关于X86 与 arm的寄存器的区别写在了arm那篇下

IDA analysis

word/ dword/ qword

In x86 terminology/documentation, a “word” is 16 bits

x86 word = 2 bytes

x86 dword = 4 bytes (double word)

x86 qword = 8 bytes (quad word)

x86 double-quad or xmmword = 16 bytes, e.g. movdqa xmm0, [rdi].

常见X86汇编

https://en.wikipedia.org/wiki/X86_instruction_listings

https://www.felixcloutier.com/x86/

https://officedaytime.com/simd512e/

官方手册第一个4800页

1
2
3
4
5
6
7
SHR	    # Shift right (unsigned shift right)
SAL # Shift Arithmetically left (signed shift left)
lea # Load Effective Address, like mov but not change Flags, can store in any register, three opts
imul # Signed multiply
movslq # Move doubleword to quadword with sign-extension.
movl $0x46dd0bfe, 0x804a1dc #将数值0x46dd0bfe放入0x804a1dc的地址中
movl 0x46dd0bfe, 0x804a1dc #将0x46dd0bfe地址里的内容放入0x804a1dc地址中

lea & leaq

1
2
lea    -0xc(%ebp),%eax
mov %eax,0x8(%esp) #常见于scanf第三个参数,lea传结果写入地址
1
2
3
// x is %rdi, result is %rax 就是计算地址,没有寻址操作
lea 0x0(,%rdi,8),%rax //result = x * 8;
lea 0x4b(,%rdi),%rax //result = x + 0x4b;

call & ret

  • Call 地址:返回地址入栈(等价于“Push %eip,mov 地址,%eip”;注意eip指向下一条尚未执行的指令)
  • ret:从栈中弹出地址,并跳到那个地址(pop %eip

leave

leave:使栈做好返回准备,等价于

1
2
mov %ebp,%esp
pop %ebp

compare order

1
2
cmpl   $0x5,$0x1
jle 8048bc5 # Jump if Less or Equal 会触发,前面的 1<=5

X86 load store

X86 不像 ARM有专门的ldrstr指令。是通过mov实现的

movswl (%rdi), %eax sign-extending load from word (w) to dword (l). Intel movsx eax, word [rdi]

AVX

https://docs.oracle.com/cd/E36784_01/html/E36859/gntbd.html

1
2
3
4
5
6
7
8
vxorpd   XORPD
Bitwise Logical XOR for Double-Precision Floating-Point Values

vxorps XORPS
Bitwise Logical XOR for Single-Precision Floating-Point Values

vmovaps MOVAPS
Move Aligned Packed Single-Precision Floating-Point Values

test & jump

1
2
test    al, al
jne 0x1000bffcc

The test instruction performs a logical and of the two operands and sets the CPU flags register according to the result (which is not stored anywhere). If al is zero, the anded result is zero and that sets the Z flag. If al is nonzero, it clears the Z flag. (Other flags, such as Carry, oVerflow, Sign, Parity, etc. are affected too, but this code has no instruction testing them.)

The jne instruction alters EIP if the Z flag is not set. There is another mnemonic for the same operation called jnz.

1
2
test   %eax,%eax
jg <phase_4+0x35> # eax & eax > 0 jump

注意 cmp不等于 test

The TEST operation sets the flags CF and OF to zero.

The SF is set to the MSB(most significant bit) of the result of the AND.

If the result of the AND is 0, the ZF is set to 1, otherwise set to 0.

kinds of jump

AT&T syntax jmpq *0x402390(,%rax,8) into INTEL-syntax: jmp [RAX*8 + 0x402390].

ja VS jg

JUMP IF ABOVE AND JUMP IF GREATER

ja jumps if CF = 0 and ZF = 0 (unsigned Above: no carry and not equal)

jg jumps if SF = OF and ZF = 0 (signed Greater, excluding equal)

FLAGS

cmp performs a sub (but does not keep the result).

cmp eax, ebx

Let’s do the same by hand:

1
2
3
4
5
6
reg     hex value   binary value  

eax = 0xdeadc0de ‭11011110101011011100000011011110‬
ebx = 0x1337ca5e ‭00010011001101111100101001011110‬
- ----------
res 0xCB75F680 11001011011101011111011010000000

The flags are set as follows:

1
2
3
4
5
6
OF (overflow) : did bit 31 change      -> no
SF (sign) : is bit 31 set -> yes
CF (carry) : is abs(ebx) < abs(eax) -> no
ZF (zero) : is result zero -> no
PF (parity) : is parity of LSB even -> no (archaic)
AF (Adjust) : overflow in bits 0123 -> archaic, for BCD only.

Carry Flag

Carry Flag is a flag set when:

a) two unsigned numbers were added and the result is larger than “capacity” of register where it is saved.

Ex: we wanna add two 8 bit numbers and save result in 8 bit register. In your example: 255 + 9 = 264 which is more that 8 bit register can store. So the value “8” will be saved there (264 & 255 = 8) and CF flag will be set.

b) two unsigned numbers were subtracted and we subtracted the bigger one from the smaller one.

Ex: 1-2 will give you 255 in result and CF flag will be set.

Auxiliary Flag is used as CF but when working with BCD. So AF will be set when we have overflow or underflow on in BCD calculations. For example: considering 8 bit ALU unit, Auxiliary flag is set when there is carry from 3rd bit to 4th bit i.e. carry from lower nibble to higher nibble. (Wiki link)

Overflow Flag is used as CF but when we work on signed numbers.

Ex we wanna add two 8 bit signed numbers: 127 + 2. the result is 129 but it is too much for 8bit signed number, so OF will be set.

Similar when the result is too small like -128 - 1 = -129 which is out of scope for 8 bit signed numbers.

register signed & unsigned

Positive or negative
The CPU does not know (or care) whether a number is positive or negative. The only person who knows is you. If you test SF and OF, then you treat the number as signed. If you only test CF then you treat the number as unsigned.
In order to help you the processor keeps track of all flags at once. You decide which flags to test and by doing so, you decide how to interpret the numbers.

register multiply

The computer makes use of binary multiplication(AND), followed by bit shift (in the direction in which the multiplication proceeds), followed by binary addition(OR).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1100100
0110111
=======
0000000
-1100100
--1100100
---0000000
----1100100
-----1100100
------1100100
==============
1010101111100

100 = 1.1001 * 2^6
55 = 1.10111* 2^5
100 * 55 -> 1.1001 * 1.10111 * 2^(6+5)

for more:

How computer multiplies 2 numbers?
And:
Binary multiplier - Wikipedia

Memory and Addressing Modes

声明静态代码区域

DB, DW, and DD can be used to declare one, two, and four byte data locations,

1
2
3
4
5
6
7
# 基本例子
.DATA
var DB 64 ; Declare a byte, referred to as location var, containing the value 64.
var2 DB ? ; Declare an uninitialized byte, referred to as location var2.
DB 10 ; Declare a byte with no label, containing the value 10. Its location is var2 + 1.
X DW ? ; Declare a 2-byte uninitialized value, referred to as location X.
Y DD 30000 ; Declare a 4-byte value, referred to as location Y, initialized to 30000.

数组的声明,The DUP directive tells the assembler to duplicate an expression a given number of times. For example, 4 DUP(2) is equivalent to 2, 2, 2, 2.

1
2
3
4
5

Z DD 1, 2, 3 ; Declare three 4-byte values, initialized to 1, 2, and 3. The value of location Z + 8 will be 3.
bytes DB 10 DUP(?) ; Declare 10 uninitialized bytes starting at location bytes.
arr DD 100 DUP(0) ; Declare 100 4-byte words starting at location arr, all initialized to 0
str DB 'hello',0 ; Declare 6 bytes starting at the address str, initialized to the ASCII character values for hello and the null (0) byte.

寻址

32位X86机器寻址支持

  1. 最多支持32位寄存器和32位有符号常数相加
  2. 其中一个寄存器可以再乘上 2,4,8
1
2
3
4
5
6
7
8
9
10
# right
mov eax, [ebx] ; Move the 4 bytes in memory at the address contained in EBX into EAX
mov [var], ebx ; Move the contents of EBX into the 4 bytes at memory address var. (Note, var is a 32-bit constant).
mov eax, [esi-4] ; Move 4 bytes at memory address ESI + (-4) into EAX
mov [esi+eax], cl ; Move the contents of CL into the byte at address ESI+EAX
mov edx, [esi+4*ebx] ; Move the 4 bytes of data at address ESI+4*EBX into EDX

# wrong and reason
mov eax, [ebx-ecx] ; Can only add register values
mov [eax+esi+edi], ebx ; At most 2 registers in address computation

指定存储在地址的数据大小

1
2
3
mov BYTE PTR [ebx], 2	; Move 2 into the single byte at the address stored in EBX.
mov WORD PTR [ebx], 2 ; Move the 16-bit integer representation of 2 into the 2 bytes starting at the address in EBX.
mov DWORD PTR [ebx], 2 ; Move the 32-bit integer representation of 2 into the 4 bytes starting at the address in EBX.

汇编寄存器顺序,作用方向

这和汇编器语法有关:

X86 instructions

For instructions with two operands, the first (lefthand) operand is the source operand, and the second (righthand) operand is the destination operand (that is, source->destination).

1
2
mov eax, ebx — copy the value in ebx into eax
add eax, 10 — EAX ← EAX + 10

AT&T syntax

AT&T Syntax is an assembly syntax used in UNIX environments, that originates from AT&T Bell Labs. It is descended from the MIPS assembly syntax. (AT&T, American Telephone & Telegraph)

AT&T Syntax is an assembly syntax used mostly in UNIX environments or by tools like gcc that originated in that environment.

语法特点:https://stackoverflow.com/tags/att/info

需要注意的:

  1. Operands are in destination-last order
  2. Register names are prefixed with %, and immediates are prefixed with $
    1. sub $24, %rsp reserves 24 bytes on the stack.
  3. Operand-size is indicated with a b/w/l/q suffix on the mnemonic
    1. addb $1, byte_table(%rdi) increment a byte in a static table.
    2. The mov suffix (b, w, l, or q) indicates how many bytes are being copied (1, 2, 4, or 8 respectively)
  4. imul $13, 16(%rdi, %rcx, 4), %eax 32-bit load from rdi + rcx<<2 + 16, multiply that by 13, put the result in %eax. Intel imul eax, [16 + rdi + rcx*4], 13.
  5. movswl (%rdi), %eax sign-extending load from word (w) to dword (l). Intel movsx eax, word [rdi].

Intel syntax (used in Intel/AMD manuals).

The Intel assembler(icc,icpc我猜) uses the opposite order (destination<-source) for operands.

语法特点: https://stackoverflow.com/tags/intel-syntax/info

RISC-V

1
2
3
beq rs1, rs2, Label #RISC-V
SW rs2, imm(rs1) # Mem[rs1+imm]=rs2 ,汇编将访存放在最后
add rd, rs1, rs2 # rd = rs1 + rs2

反汇编器

但是这个语法不是很重要,因为decompiler有选项控制语法

objdump has -Mintel flag, gdb has set disassembly-flavor intel option.

gcc -masm=intel -S or objdump -drwC -Mintel.

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

https://www.cs.virginia.edu/~evans/cs216/guides/x86.html

Vtune Assembly Analysis

超算机器用vtune的命令行文件分析

  1. 首先找到vtune程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    > module load intel/2022.1                                    
    > which icc
    /public1/soft/oneAPI/2022.1/compiler/latest/linux/bin/intel64/icc
    > cd /public1/soft/oneAPI/2022.1
    > find . -executable -type f -name "*vtune*"
    ./vtune/2022.0.0/bin64/vtune-worker-crash-reporter
    ./vtune/2022.0.0/bin64/vtune-gui.desktop
    ./vtune/2022.0.0/bin64/vtune-gui
    ./vtune/2022.0.0/bin64/vtune-agent
    ./vtune/2022.0.0/bin64/vtune-self-checker.sh
    ./vtune/2022.0.0/bin64/vtune-backend
    ./vtune/2022.0.0/bin64/vtune-worker
    ./vtune/2022.0.0/bin64/vtune
    ./vtune/2022.0.0/bin64/vtune-set-perf-caps.sh
  2. vtune-gui获取可执行命令

    1
    /opt/intel/oneapi/vtune/2021.1.1/bin64/vtune -collect hotspots -knob enable-stack-collection=true -knob stack-size=4096 -data-limit=1024000 -app-working-dir /home/shaojiemike/github/IPCC2022first/build/bin -- /home/shaojiemike/github/IPCC2022first/build/bin/pivot /home/shaojiemike/github/IPCC2022first/src/uniformvector-2dim-5h.txt
  3. 编写sbatch_vtune.sh

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #!/bin/bash
    #SBATCH -o ./slurmlog/job_%j_rank%t_%N_%n.out
    #SBATCH -p IPCC
    #SBATCH -t 15:00
    #SBATCH --nodes=1
    #SBATCH --exclude=
    #SBATCH --cpus-per-task=64
    #SBATCH --mail-type=FAIL
    #SBATCH [email protected]

    source /public1/soft/modules/module.sh
    module purge

    module load intel/2022.1

    logname=vtune
    export OMP_PROC_BIND=close; export OMP_PLACES=cores
    # ./pivot |tee ./log/$logname
    /public1/soft/oneAPI/2022.1/vtune/2022.0.0/bin64/vtune -collect hotspots -knob enable-stack-collection=true -knob stack-size=4096 -data-limit=1024000 -app-working-dir /public1/home/ipcc22_0029/shaojiemike/slurm -- /public1/home/ipcc22_0029/shaojiemike/slurm/pivot /public1/home/ipcc22_0029/shaojiemike/slurm/uniformvector-2dim-5h.txt |tee ./log/$logname
  4. log文件如下,但是将生成的trace文件r000hs导入识别不了AMD

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    > cat log/vtune
    dim = 2, n = 500, k = 2
    Using time : 452.232000 ms
    max : 143 351 58880.823709
    min : 83 226 21884.924801
    Elapsed Time: 0.486s
    CPU Time: 3.540s
    Effective Time: 3.540s
    Spin Time: 0s
    Overhead Time: 0s
    Total Thread Count: 8
    Paused Time: 0s

    Top Hotspots
    Function Module CPU Time % of CPU Time(%)
    --------------- ------ -------- ----------------
    SumDistance pivot 0.940s 26.6%
    _mm256_add_pd pivot 0.540s 15.3%
    _mm256_and_pd pivot 0.320s 9.0%
    _mm256_loadu_pd pivot 0.300s 8.5%
    Combination pivot 0.250s 7.1%
    [Others] N/A 1.190s 33.6%

汇编

1
2
objdump -Sd ../build/bin/pivot > pivot1.s
gcc -S -O3 -fverbose-asm ../src/pivot.c -o pivot_O1.s

汇编分析技巧

https://blog.csdn.net/thisinnocence/article/details/80767776

如何设置GNU和Intel汇编语法


vtune汇编实例

(没有开O3,默认值)


偏移 -64 是k

-50 是ki


CDQE复制EAX寄存器双字的符号位(bit 31)到RAX的高32位。

这里的movsdq的q在intel里的64位,相当于使用了128位的寄存器,做了64位的事情,并没有自动向量化。

生成带代码注释的O3汇编代码

如果想把 C 语言变量的名称作为汇编语言语句中的注释,可以加上 -fverbose-asm 选项:

1
gcc -S -O3 -fverbose-asm ../src/pivot.c -o pivot_O1.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.L15:
# ../src/pivot.c:38: double dis = fabs(rebuiltCoordFirst - rebuiltCoordSecond);
movsd (%rax), %xmm0 # MEM[base: _15, offset: 0B], MEM[base: _15, offset: 0B]
subsd (%rax,%rdx,8), %xmm0 # MEM[base: _15, index: _21, step: 8, offset: 0B], tmp226
addq $8, %rax #, ivtmp.66
# ../src/pivot.c:38: double dis = fabs(rebuiltCoordFirst - rebuiltCoordSecond);
andpd %xmm2, %xmm0 # tmp235, dis
maxsd %xmm1, %xmm0 # chebyshev, dis
movapd %xmm0, %xmm1 # dis, chebyshev
# ../src/pivot.c:35: for(ki=0; ki<k; ki++){
cmpq %rax, %rcx # ivtmp.66, _115
jne .L15 #,
.L19:
# ../src/pivot.c:32: for(j=i+1; j<n; j++){
addl $1, %esi #, j
# ../src/pivot.c:41: chebyshevSum += chebyshev;
addsd %xmm1, %xmm4 # chebyshev, <retval>
addl %r14d, %edi # k, ivtmp.75
# ../src/pivot.c:32: for(j=i+1; j<n; j++){
cmpl %esi, %r15d # j, n
jg .L13 #,
# ../src/pivot.c:32: for(j=i+1; j<n; j++){
addl $1, %r10d #, j
# ../src/pivot.c:32: for(j=i+1; j<n; j++){
cmpl %r10d, %r15d # j, n
jne .L16 #,

vtune O3汇编分析

原本以为O3是看不了原代码与汇编的对应关系的,但实际可以-g -O3 是不冲突的。

指令的精简合并

  1. 访存指令的合并
    1. r9 mov到 rax里,
      1. leaq (%r12,%r8,8), %r9。其中r12rebuiltCoord,所以r8原本存储的是[i*k]的值
      2. raxrebuiltCoord+[i*k]的地址,由于和i有关,index的计算在外层就计算好了。
    2. rdx的值减去r8存储在rdx
      1. rdx原本存储的是[j*k]的地址
      2. r8原本存储的是[i*k]的值
      3. rdx之后存储的是[(j-i)*k]的地址
    3. data16 nop是为了对齐插入的nop
    1. 值得注意的是取最大值操作,这里变成了maxsd
    2. xmm0缓存值
    3. xmm1chebyshev
    4. xmm2fabs的掩码
    5. xmm4chebyshevSum

自动循环展开形成流水

    1. r14d存储k的值,所以edi存储j*k
    2. Block22后的指令验证了rdx原本存储的是[j*k]的地址
    1. 最外层循环
    2. 因为r14d存储k的值,r8r11d存储了i*k的值

从汇编看不出有该操作,需要开启编译选项

自动向量化

从汇编看不出有该操作,需要开启编译选项

自动数据预取

从汇编看不出有该操作,需要开启编译选项

问题

为什么求和耗时这么多

添加向量化选项

gcc

Baseline

-mavx2 -march=core-avx2

  1. 阅读文档, 虽然全部变成了vmov,vadd的操作,但是实际还是64位的工作。
    1. 这点add rax, 0x8没有变成add rax, 0x16可以体现
    2. 但是avx2不是256位的向量化吗?用的还是xmm0这类的寄存器。
1
2
3
4
5
6
7
8
VADDSD (VEX.128 encoded version)
DEST[63:0] := SRC1[63:0] + SRC2[63:0]
DEST[127:64] := SRC1[127:64]
DEST[MAXVL-1:128] := 0

ADDSD (128-bit Legacy SSE version)
DEST[63:0] := DEST[63:0] + SRC[63:0]
DEST[MAXVL-1:64] (Unmodified)

-march=skylake-avx512

汇编代码表面没变,但是快了10s(49s - 39s)

下图是avx2的

下图是avx512的

猜测注意原因是

  1. nop指令导致代码没对齐
  2. 不太可能和红框里的代码顺序有关

添加数据预取选项

判断机器是否支持

1
2
lscpu|grep pref
3dnowprefetch //3DNow prefetch instructions

应该是支持的

汇编分析

虽然时间基本没变,主要是对主体循环没有进行预取操作,对其余循环(热点占比少的)有重新调整。如下图增加了预取指令

添加循环展开选项

变慢很多(39s -> 55s)

-funroll-loops

汇编实现,在最内层循环根据k的值直接跳转到对应的展开块,这里k是2。

默认是展开了8层,这应该和xmm寄存器总数有关

分析原因

  1. 循环展开的核心是形成计算和访存的流水
    1. 不是简单的少几个跳转指令
    2. 这种简单堆叠循环核心的循环展开,并不能形成流水。所以时间不会减少
  2. 但是完全无法解释循环控制的时间增加
    1. 比如图中cmp的次数应该减半了,时间反而翻倍了

手动分块

由于数据L1能全部存储下,没有提升

手动数据预取

并没有形成想象中预取的流水。每512位取,还有重复。


每次预取一个Cache Line,后面两条指令预取的数据还有重复部分(导致时间增加 39s->61s)


想预取全部,循环每次预取了512位=64字节

手动向量化

avx2

(能便于编译器自动展开来使用所有的向量寄存器,avx2

39s -> 10s -> 8.4s 编译器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
for(i=0; i<n-blockSize; i+=blockSize){
for(j=i+blockSize; j<n-blockSize; j+=blockSize){
for(ii=i; ii<i+blockSize; ii++){
__m256d vi1 = _mm256_broadcast_sd(&rebuiltCoord[0*n+ii]);
__m256d vi2 = _mm256_broadcast_sd(&rebuiltCoord[1*n+ii]);

__m256d vj11 = _mm256_loadu_pd(&rebuiltCoord[0*n+j]); //读取4个点
__m256d vj12 = _mm256_loadu_pd(&rebuiltCoord[1*n+j]);

__m256d vj21 = _mm256_loadu_pd(&rebuiltCoord[0*n+j+4]); //读取4个点
__m256d vj22 = _mm256_loadu_pd(&rebuiltCoord[1*n+j+4]);

vj11 = _mm256_and_pd(_mm256_sub_pd(vi1,vj11), vDP_SIGN_Mask);
vj12 = _mm256_and_pd(_mm256_sub_pd(vi2,vj12), vDP_SIGN_Mask);

vj21 = _mm256_and_pd(_mm256_sub_pd(vi1,vj21), vDP_SIGN_Mask);
vj22 = _mm256_and_pd(_mm256_sub_pd(vi2,vj22), vDP_SIGN_Mask);

__m256d tmp = _mm256_add_pd(_mm256_max_pd(vj11,vj12), _mm256_max_pd(vj21,vj22));
_mm256_storeu_pd(vchebyshev1, tmp);

chebyshevSum += vchebyshev1[0] + vchebyshev1[1] + vchebyshev1[2] + vchebyshev1[3];

// for(jj=j; jj<j+blockSize; jj++){
// double chebyshev = 0;
// int ki;
// for(ki=0; ki<k; ki++){
// double dis = fabs(rebuiltCoord[ki*n + ii] - rebuiltCoord[ki*n + jj]);
// chebyshev = dis>chebyshev ? dis : chebyshev;
// }
// chebyshevSum += chebyshev;
// }
}
}
}

明明展开了一次,但是编译器继续展开了,总共8次。用满了YMM 16个向量寄存器。

下图是avx512,都出现寄存器ymm26了。

vhaddpd是水平的向量内加法指令

avx512

当在avx512的情况下展开4次,形成了相当工整的代码。

  1. 向量用到了寄存器ymm18,估计只能展开到6次了。
    1. avx2 应该寄存器不够

最后求和的处理,编译器首先识别出了,不需要实际store。还是在寄存器层面完成了计算。并且通过三次add和两次数据 移动指令自动实现了二叉树型求和。

avx2 寄存器不够会出现下面的情况。

avx求和的更快速归约

假如硬件存在四个一起归约的就好了,但是对于底层元件可能过于复杂了。


1
2
__m256d _mm256_hadd_pd (__m256d a, __m256d b);
VEXTRACTF128 __m128d _mm256_extractf128_pd (__m256d a, int offset);


如果可以实现会节约一次数据移动和一次数据add。没有分析两种情况的寄存器依赖。可能依赖长度是一样的,导致优化后时间反而增加一点。

对于int还有这种实现

将横向归约全部提取到外面

并且将j的循环展开变成i的循环展开

手动向量化+手动循环展开?

支持的理由:打破了循环间的壁垒,编译器会识别出无效中间变量,在for的jump指令划出的基本块内指令会乱序执行,并通过寄存器重命名来形成最密集的计算访存流水。

不支持的理由:如果编译器为了形成某一指令的流水,占用了太多资源。导致需要缓存其他结果(比如,向量寄存器不够,反而需要额外的指令来写回,和产生延迟。

理想的平衡: 在不会达到资源瓶颈的情况下展开。

支持的分析例子

手动展开后,识别出来了连续的访存应该在一起进行,并自动调度。将+1的偏移编译器提前计算了。

如果写成macro define,可以发现编译器自动重排了汇编。

不支持的分析例子

avx2可以看出有写回的操作,把值从内存读出来压入栈中。

寄存器足够时没有这种问题

寻找理想的展开次数

由于不同代码对向量寄存器的使用次数不同,不同机器的向量寄存器个数和其他资源数不同。汇编也难以分析。在写好单次循环之后,最佳的展开次数需要手动测量。如下图,6次应该是在不会达到资源瓶颈的情况下展开来获得最大流水。

1
2
3
4
5
6
for(j=beginJ; j<n-jBlockSize; j+=jBlockSize){  /
//展开jBlockSize次
}
for(jj=j; jj<n; jj++){ //j初始值继承自上面的循环
//正常单次
}

由于基本块内乱序执行,代码的顺序也不重要。
加上寄存器重命名来形成流水的存在,寄存器名也不重要。当然数据依赖还是要正确。

对于两层循环的双层手动展开

思路: 外层多load数据到寄存器,但是运行的任何时候也不要超过寄存器数量的上限(特别注意在内层循环运行一遍到末尾时)。

左图外层load了8个寄存器,但是右边只有2个。

特别注意在内层循环运行一遍到末尾时:

如图,黄框就有16个了。

注意load的速度也有区别

所以内层调用次数多,尽量用快的

1
2
_mm256_loadu_ps >> _mm256_broadcast_ss > _mm256_set_epi16
0.04 >> 0.5
1
2
3
4
5
6
7
vsub  vmax    ps 0.02      Latency 4
vand Latency 1

vadd ps 0.80 Throughput 0.5
vhadd Latency 7
vcvtps2pd 2.00 Latency 7
vextractf128 0.50 Latency 3

|指令|精度|时间(吞吐延迟和实际依赖导致)|Latency|Throughput
|-|-|-|-|-|-|
|_mm256_loadu_ps /_mm256_broadcast_ss|||7|0.5
|vsub vmax | ps| 0.02 | 4|0.5
vand ||0.02| 1|0.33
vadd |ps |0.80 |4| 0.5
vhadd ||0.8| 7|2
vcvtps2pd || 2.00 | 7|1
vextractf128 || 0.50 | 3|1

向量化double变单精度没有提升

17条avx计算 5load 2cvt 2extract

单位时间 avx计算 load cvt extract
2.33 3.68 12.875 4.1

可见类型转换相当耗费时间,最好在循环外,精度不够,每几次循环做一次转换。

GCC编译器优化

-march=skylake-avx512是一条指令

-mavx2 是两条指令

1
2
vmovupd xmm7, xmmword ptr [rdx+rsi*8]
vinsertf128 ymm1, ymm7, xmmword ptr [rdx+rsi*8+0x10], 0x1

原因是不对齐的访存在老架构上可能更快

O3对于核心已经向量化的代码还有加速吗?

将IPCC初赛的代码去掉O3发现还是慢了10倍。

为什么连汇编函数调用也慢这么多呢?

这个不开O3的编译器所属有点弱智了,一条指令的两个操作数竟然在rbp的栈里存来存去的。

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

Arm vs X86 (unfinished)

ARM Ltd history

诞生

1981年,被Intel拒绝的Acorn(橡子) Computer Ltd公司,一气之下觉得基于当时新型处理器的研究——简化指令集,自己设计一款微处理器。

1985年,第一款芯片问世Acorn RISC Machine,简称ARM。

转型模式

1990年,Acorn为了和苹果合作,专门成立了一家公司,名叫ARM,但是全称是Advanced RISC Machines。

虽然有苹果的合资,但是初期极其艰难,ARM决定改变他们的产品策略——他们不再生产芯片,转而以授权的方式,将芯片设计方案转让给其他公司,即“Partnership”开放模式。

通过IP(Intellectual Property,知识产权)授权,授权费和版税就成了ARM的主要收入来源。这种授权模式,极大地降低了自身的研发成本和研发风险。风险共担、利益共享的模式使得低成本创新成为可能。

新模式下的移动黄金时代来临

  • 1993年,ARM将产品授权给德州仪器,给ARM公司带来了重要的突破。也给ARM公司树立了声誉,证实了授权模式的可行性。

  • ARM+Nokia,诺基亚6110成为了第一部采用ARM处理器的GSM手机,上市后获得了极大的成功,成为当年的机皇。
    1998年4月17日,业务飞速发展的ARM控股公司,同时在伦敦证交所和纳斯达克上市。
    虽然后来苹果公司,逐步卖掉了所持有的ARM股票,鉴于苹果研究人员对ARM芯片架构非常熟悉,iPod也继续使用了ARM芯片。

  • ARM+Apple:创造移动互联网、iPhone、ARM指令集的黄金时代。
    第一代iPhone,使用了ARM设计、三星制造的芯片。Iphone的热销,App Store的迅速崛起,让全球移动应用彻底绑定在ARM指令集上。

  • 苹果的A系列处理器是基于ARM指令集架构授权自研内核的成功典范。

    • 2012年9月,苹果随iPhone5上市发布了A6处理器SoC,这颗SoC基于ARMv7架构打造的Swift内核微架构开启了苹果基于ARM架构自研处理器内核的序幕。
    • 2013年9月,苹果率先发布搭载基于ARMv8架构研发的64位Cyclone架构的双核A7处理器。A7作为世界首款64位智能手机处理器,在性能表现力压还在32位四核方案上竞争的安卓阵营。
    • 2020年,苹果宣称新发布的A14 Bionic芯片性能已经堪比部分笔记本处理器。
    • 2021年,M1诞生

紧接着,2008年,谷歌推出了Android(安卓)系统,也是基于ARM指令集。

从ARM角度来看,苹果M1一旦成功也将帮助ARM实现一直以来希望撕开X86垄断的个人计算机市场的野心。

投胎日本,助力”富岳”

2016年7月18日,曾经投资阿里巴巴的孙正义和他的日本软银集团,以243亿英镑(约309亿美元)收购了ARM集团。

至此,ARM成为软银集团旗下的全资子公司。不过,当时软银集团表示,不会干预或影响ARM未来的商业计划和决策。

在2020年6月22日,日本超算“富岳”(Fugaku)成为史上第一台基于ARM芯片的全球超算TOP500冠军。

小结:轻资产、开放合作、共赢。 ARM在低功耗方面的DNA,刚好赶上了移动设备爆发式发展的时代,最终造就了它的辉煌。在即将到来的万物互联时代,可以预见,ARM极有可能取得更大的成功。

Nvidia垄断收购风波

2020年9月13日,NVIDIA(英伟达)和软银集团 (SoftBank Group Corp., SBG) 宣布了一项最终协议,根据此协议,NVIDIA 将以 400 亿美元的价格从软银集团和软银愿景基金(统称“软银”)收购 Arm Limited。

但是这场收购在全球IT行业掀起轩然大波,包括苹果、Intel、高通、三星、特斯拉等大部分巨头均表示反对。英国也反对。至今悬而未决。

64bits VS 32bits

主要区别在

  1. 通用寄存器一个是64位,一个是32位,
  2. 指令寻址能力增加,32位只能内存寻址4GB=4*1024*1024*1024 bytes

一些常见问题:

  1. 64位机器会比32位更快吗?
    1. 理论上计算不会,但是由于处理器一般先进,访存空间更大,会有些影响。和寄存器数量什么都有关。
  2. 32位机器就只有4GB内存?错误
    1. 其实32位处理器是可以使用4GB以上内存的,比如Pentium Pro的处理器具有36位物理地址,它就具有64GB(2^36b=64GB)的寻址空间,Intel称之为PAE(Physical Address Extension)。

x86 64位

Intel并没有开发64位版本的x86指令集。64位的指令集名为x86-64(有时简称为x64),实际上是AMD设计开发的。Intel想做64位计算,它知道如果从自己的32位x86架构进化出64位架构,新架构效率会很低,于是它搞了一个新64位处理器项目名为IA64。由此制造出了Itanium系列处理器。

同时AMD知道自己造不出能与IA64兼容的处理器,于是它把x86扩展一下,加入了64位寻址和64位寄存器。最终出来的架构,就是 AMD64,成为了64位版本的x86处理器的标准。IA64项目并不算得上成功,现如今基本被放弃了。Intel最终采用了AMD64。Intel当前给出的移动方案,是采用了AMD开发的64位指令集(有些许差别)的64位处理器。

x86-64架构诞生颇有时代意义。当时,处理器的发展遇到了瓶颈,内存寻址空间由于受到32位CPU的限制而只能最大到约4G。AMD主动把32位x86(或称为IA-32)扩充为64位。它以一个称为AMD64的架构出现(在重命名前也称为x86-64),且以这个技术为基础的第一个产品是单内核的Opteron和Athlon 64处理器家族。由于AMD的64位处理器产品线首先进入市场,且微软也不愿意为Intel和AMD开发两套不同的64位操作系统,Intel也被迫采纳AMD64指令集且增加某些新的扩充到他们自己的产品,命名为EM64T架构(显然他们不想承认这些指令集是来自它的主要对手),EM64T后来被Intel正式更名为Intel 64。这两者被统称为x86-64或x64,开创了x86的64位时代。

ARM 64位

而ARM在看到移动设备对64位计算的需求后,于2011年发布了ARMv8 64位架构,这是为了下一代ARM指令集架构工作若干年后的结晶。为了基于原有的原则和指令集,开发一个简明的64位架构,ARMv8使用了两种执行模式,AArch32和AArch64。顾名思义,一个运行32位代码,一个运行64位代码。ARM设计的巧妙之处,是处理器在运行中可以无缝地在两种模式间切换。这意味着64位指令的解码器是全新设计的,不用兼顾32位指令,而处理器依然可以向后兼容。

为什么X86比ARM更耗电呢?

在cpu同制程工艺下,

ARM的处理器有个特点,就是乱序执行能力不如X86。

X86为了增强对随机操作命令情况下的处理能力,加强了乱序指令的执行、单核的多线程能力。

缺点就是,无法很有效的关闭和恢复处理器子模块,因为一旦关闭,恢复起来就很慢,从而造成低性能。为了保持高性能,就不得不让大部分的模块都保持开启,并且时钟也保持切换。这样做的直接后果就是耗电高。

ARM的指令强在确定次序的执行,并且依靠多核而不是单核多线程来执行。这样容易保持子模块和时钟信号的关闭,显然就更省电。

ARM 架构


ARM架构新命名

ARM11芯片之后,也就是从ARMv7架构开始,改以Cortex命名,并分为三个系列,分别是Cortex-A,Cortex-R,Cortex-M。呵呵,发现了没,三个字母又是A、R、M。

  • Cortex-A系列(A:Application)
    针对日益增长的消费娱乐和无线产品设计,用于具有高计算要求、运行丰富操作系统及提供交互媒体和图形体验的应用领域,如智能手机、平板电脑、汽车娱乐系统、数字电视等。
    Cortex-A目前有A7x系列为代表的性能大核产品线和A5x系列为代表低功耗小核产品线。
    其中大核运行短时间的高性能需求任务;小核运行低性能需求的任务或者在待机状态支持背景任务运行。

  • Cortex-R系列 (R:Real-time)
    针对需要运行实时操作的系统应用,面向如汽车制动系统、动力传动解决方案、大容量存储控制器等深层嵌入式实时应用。

  • Cortex-M系列(M:Microcontroller)
    该系列面向微控制器microcontroller (MCU) 领域,主要针对成本和功耗敏感的应用,如智能测量、人机接口设备、汽车和工业控制系统、家用电器、消费性产品和医疗器械等。智能互联时代应用前景非常广阔。

  • Cortex-SC系列(SC:SecurCore)
    其实,除了上述三大系列之外,还有一个主打安全的(SC:SecurCore),主要用于政府安全芯片。

ARM v9架构

自 2011 年 10 月 Arm 首次公布 Armv8架构以来,已经有近 10 年的时间了

  1. 支持SVE2(可以打通512位矢量寄存器和128等各层次的使用,不用重新写)和矩阵乘法
  2. 安全、AI 以及改进矢量扩展(Scalable Vector Extensions,简称SVE)和 DSP 能力
    1. 新的可变向量长度 SIMD 指令集的首次迭代范围相当有限,而且更多的是针对 HPC 工作负载,缺少了许多通用性较强的指令
  3. 具有保密功能的计算架构

2021年3月31日,ARM V9发布


苹果 A16 架构

来自极客湾

白色部分为加宽的部分

aarm64 指令是多少位的?

AArch64:AArch64 state只支持A64指令集。这是一个固定长度的指令集,使用32位指令编码。

Arch32:AArch32 state支持以下指令集:

A32:这是一个固定长度的指令集,使用32位指令编码。它是与ARMv7 ARM指令集兼容。

T32:这是一个可变长度指令集,它同时使用16位和32位指令编码。它与ARMv7 Thumb®指令集兼容。

而CISC指令集都是变长的。

指令的长度

指令长度的范围可以说是相当广泛,从微控制器的4 bit,到VLIW系统的数百bit。在个人电脑,大型机,超级电脑内的处理器,其内部的指令长度介于8到64 bits(在x86处理器结构内,最长的指令长达15 bytes,等于120 bits)。在一个指令集架构内,不同的指令可能会有不同长度。在一些结构,特别是大部分的精简指令集(RISC),指令是固定的长度,长度对应到结构内一个字的大小。在其他结构,长度则是byte的整数倍或是一个halfword。

https://www.eet-china.com/mp/a23067.html

https://winddoing.github.io/post/7190.html

寄存器的区别

AArch64

In AArch64 state, the following registers are available:

  • Thirty-one 64-bit general-purpose registers X0-X30, the bottom halves of which are accessible as W0-W30.
  • Four stack pointer registers SP_EL0, SP_EL1, SP_EL2, SP_EL3.
  • Three exception link registers ELR_EL1, ELR_EL2, ELR_EL3.
  • Three saved program status registers SPSR_EL1, SPSR_EL2, SPSR_EL3.
  • One program counter.


X31 stack pointer

You can write the register names either in all upper case or all lower case.

In AArch64 state, the PC is not a general purpose register and you cannot access it by name.

All these registers are 64 bits wide except SPSR_EL1, SPSR_EL2, and SPSR_EL3, which are 32 bits wide.

Most A64 integer instructions can operate on either 32-bit or 64-bit registers.

The names Wn and Xn, where n is in the range 0-30. W means 32-bit and X means 64-bit.

更具体的细节请看 ARMv8 Instruction Set Overview 4.4.1 General purpose (integer) registers

AArch64 A64 Advanced SIMD (NEON)

更具体的细节请看 ARMv8 Instruction Set Overview 4.4.2 FP/SIMD registers 或者 Assembly Arm文章

向量寄存器有32个v0 - v31, 由于表示方法 Qn 也是 128位,所以汇编有时以 %qn出现(n为第几个寄存器)

AArch32

In all ARM processors in AArch32 state, the following registers are available and accessible in any processor mode:

  • 15 general-purpose registers R0-R12, the Stack Pointer (SP), and Link Register (LR).
  • 1 Program Counter (PC).
  • 1 Application Program Status Register (APSR).

r11是optional的,backtrace时候会启用,被称为FP,即frame pointer。

r12 IP The Intra-Procedure-call scratch register. (可简单的认为暂存SP)

r13 SP The Stack Pointer.

r14 LR The Link Register. 用于保存函数调用的返回地址

r15 PC The Program Counter.

x86的bp寄存器其实有两个功能:

  1. 指向栈底
  2. 指向返回地址
    arm却把这两个功能拆开了,用两个存,为的就是减少一步访存。

https://blog.csdn.net/tangg555/article/details/62231285

X86(-64)

The x86 architecture has 8 General-Purpose Registers (GPR), 6 Segment Registers, 1 Flags Register and an Instruction Pointer. 64-bit x86 has additional registers.

  1. General-Purpose Registers (GPR) - 16-bit naming conventions
    1. Accumulator register (AX). Used in arithmetic operations
    2. Counter register (CX). Used in shift/rotate instructions and loops.
    3. Data register (DX). Used in arithmetic operations and I/O operations.
    4. Base register (BX). Used as a pointer to data (located in segment register DS, when in segmented mode).
    5. Stack Pointer register (SP). Pointer to the top of the stack.
    6. Stack Base Pointer register (BP). Used to point to the base of the stack.
    7. Source Index register (SI). Used as a pointer to a source in stream operations.
    8. Destination Index register (DI). Used as a pointer to a destination in stream operations.
  2. Segment Registers
    1. Stack Segment (SS). Pointer to the stack.
    2. Code Segment (CS). Pointer to the code.
    3. Data Segment (DS). Pointer to the data.
    4. Extra Segment (ES). Pointer to extra data (‘E’ stands for ‘Extra’).
    5. F Segment (FS). Pointer to more extra data (‘F’ comes after ‘E’).
    6. G Segment (GS). Pointer to still more extra data (‘G’ comes after ‘F’).
  3. General-Purpose Registers 64-bit
    1. rax - register a extended
    2. rbx - register b extended
    3. rcx - register c extended
    4. rdx - register d extended
    5. rbp - register base pointer (start of stack)
    6. rsp - register stack pointer (current location in stack, growing downwards)
    7. rsi - register source index (source for data copies)
    8. rdi - register destination index (destination for data copies)

RIP (EIP)

X86-SIMD

http://home.ustc.edu.cn/~shaojiemike/posts/simd/#simd%E5%AF%84%E5%AD%98%E5%99%A8

关于零寄存器

某个寄存器是只读的,存的值一直为0

Most RISC architectures have a “zero register”(WZR/XZR reg31 for ARM) which always reads as zero and cannot be written to.

While the x86/x64 architectures do not have an architectural zero register.

通过zero Idiom :

The register renamer detects certain instructions (xor reg, reg and sub reg, reg and various others) that always zero a register

kunpeng 920

history

2019年1月,华为跟进一步发布自研服务器芯片鲲鹏920。该服务器芯片搭载了64颗海思基于ARMv8架构自研的泰山内核。整体服务器性能较市场现有竞品提升20%。2019年5月,华为宣布获得ARMv8架构永久授权,并且强调华为海思有持续自行开发设计基于ARM授权架构的处理器。

  • AMBA(Advanced Microcontroller Bus Architecture)是ARM公司定义的一个总线架构,用来连接不同的功能模块(如CPU核心、内存控制器、I/O端口等)。AMBA是一种开放标准,用于连接和管理集成在SOC(System on Chip)上的各种组件。它是为了高带宽和低延迟的内部通信而设计的,确保不同组件之间的高效数据传输。
  • ARM的SCP和MCP固件(System Control Processor & Management Control Processor firmware)则是指ARM提供的用于系统控制处理器和管理控制处理器的固件。这些固件通常负责处理系统管理任务,例如电源管理、系统启动和监控、安全性管理等。SCP和MCP是ARM架构中用于系统级管理和控制的专门处理器或子系统。

chip

wikiChip https://en.wikichip.org/wiki/hisilicon/microarchitectures/taishan_v110

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Architecture:                    aarch64
CPU op-mode(s): 64-bit
Byte Order: Little Endian
CPU(s): 96
On-line CPU(s) list: 0-95
Thread(s) per core: 1
Core(s) per socket: 48
Socket(s): 2
NUMA node(s): 4
Vendor ID: 0x48
Model: 0
Stepping: 0x1
CPU max MHz: 2600.0000
CPU min MHz: 200.0000
BogoMIPS: 200.00
L1d cache: 6 MiB
L1i cache: 6 MiB
L2 cache: 48 MiB
L3 cache: 192 MiB
NUMA node0 CPU(s): 0-23
NUMA node1 CPU(s): 24-47
NUMA node2 CPU(s): 48-71
NUMA node3 CPU(s): 72-95
Vulnerability Itlb multihit: Not affected
Vulnerability L1tf: Not affected
Vulnerability Mds: Not affected
Vulnerability Meltdown: Not affected
Vulnerability Spec store bypass: Not affected
Vulnerability Spectre v1: Mitigation; __user pointer sanitization
Vulnerability Spectre v2: Not affected
Vulnerability Srbds: Not affected
Vulnerability Tsx async abort: Not affected
Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm j
scvt fcma dcpop asimddp asimdfhm

鲲鹏920明显的几个特点,96个核,4个NUMA node, cache相较于Intel特别大

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

致谢ARM、驭势资本、EETOP…等原编著者,来源华为云社区
https://bbs.huaweicloud.com/blogs/262835

https://developer.arm.com/documentation

RIP register

https://stackoverflow.com/questions/42215105/understanding-rip-register-in-intel-assembly