如果我有一个8位值,那么使用8位寄存器代替16、32或64位有什么好处?

时间:2018-11-30 18:09:26

标签: assembly x86

我读过的介绍性x86 asm文献似乎在所有实际情况下都坚持使用32位寄存器(eax,ebx等),除了证明64位寄存器也存在之外。如果只提到16位寄存器,则这是一个历史记录,它解释了为什么32位寄存器的名称前面带有“ e”。编译器似乎同样对32位以下的寄存器不感兴趣。

考虑以下C代码:

int main(void) { return 511; }

尽管main声称要返回一个整数,但实际上Linux退出状态码是8位,这意味着超过255的任何值将是最低有效的8位,即。

hc027@HC027:~$ echo "int main(void) { return 511; }" > exit_gcc.c
hc027@HC027:~$ gcc exit_gcc.c 
hc027@HC027:~$ ./a.out 
hc027@HC027:~$ echo $?
255

因此,我们看到系统仅使用int main(void)返回值的前8位。 ,当我们向GCC询问同一程序的汇编输出时,它将返回值存储在8位寄存器中吗?让我们找出答案吧!

hc027@HC027:~$ cat exit_gcc.s
    .file   "exit_gcc.c"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $511, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

不!它使用%eax,一个非常多的32位寄存器!现在,GCC比我更聪明,也许int main(void) 的返回值用于其他不知道返回值不会被截断为8的东西有效位(或者C标准规定,无论实际命运如何,它都必须返回真实的int实际值)

但是,不管我的具体例子的功效如何,问题仍然存在。据我所知,现代x86汇编程序员和编译器都忽略了32位以下的寄存器。粗略的Google表示“何时使用16位寄存器x86”不会返回任何相关答案。我很好奇:在x86 CPU中使用8位和16位寄存器有什么优势吗?

2 个答案:

答案 0 :(得分:2)

这里有一段历史。尝试运行

mov rax, -1
mov eax, 0
print rax

在您喜欢的x86桌面上(print根据您的环境)。您会注意到,即使rax开头都是全1,并且您认为自己只抹掉了最低的32位,但print语句仍显示为零!写入eax会完全擦除rax。为什么?因为它快得多。当您继续写入rax时,尝试维持较高的eax值是绝对的痛苦。

Intel / AMD决定转移到32bit时并没有考虑到这一点,并犯了一个错误,导致al / ah在引导加载程序和其他位提琴手之外未被使用:当您写入alah时,另一个不会被破坏!在16位时代,这很棒,因为现在您拥有的寄存器数量是的两倍,而您拥有的是32位寄存器!但是随着寄存器数量的增加,我们不再需要更多的寄存器。我们需要的是快速寄存器和更高的GHz。从这个角度来看,每次您写入alah时,您仍然必须从eax进行读取。这给乱序的执行者造成了沉重的负担。

理论已经足够,让我们进行一些实际的测试。每个测试了三遍。这些测试在Intel Core i5-4278U CPU @ 2.60GHz

上运行

时间:1.067s,1.072s,1.097s

global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov rax, 5
mov rax, 5
mov rax, 6
mov rax, 6
mov rax, 7
mov rax, 7
mov rax, 8
mov rax, 8
dec ecx
jmp loop
exit:
ret

时间:1.072s,1.062s,1.060s

global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov eax, 5
mov eax, 5
mov eax, 6
mov eax, 6
mov eax, 7
mov eax, 7
mov eax, 8
mov eax, 8
dec ecx
jmp loop
exit:
ret

时间:2.702s,2.748s,2.704s

global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov ah, 5
mov ah, 6
mov ah, 6
mov ah, 7
mov ah, 7
mov ah, 8
mov ah, 8
dec ecx
jmp loop
exit:
ret

次数:1.432s,1.457s,1.427s

global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
mov ah, 6
mov al, 6
mov ah, 7
mov al, 7
mov ah, 8
mov al, 8
dec ecx
jmp loop
exit:
ret

时间:1.117s,1.084s,1.082s

global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
mov eax, 6
mov al, 6
mov ah, 7
mov eax, 7
mov ah, 8
mov al, 8
dec ecx
jmp loop
exit:
ret

这些测试与部分寄存器停顿无关,因为在写入eax之后我没有读取ah。当您的总线是32位或64位时,这仅仅是使用8位或16位的成本。就我而言,我的CPU的总线是64位。 32位写入不受损害,因为它实际上是64位写入。每次写入eax时都会填充一个隐式的32位零。这很重要,因为许多代码已编译到x86,并且需要在x86_64系统上才能实现。

此外,如果您想尝试,您会注意到add eax, 5add ah, 5都花费相同的时间(在我的CPU上为2.7秒,与mov ah, 5相同) 。在这种情况下,您仍然必须从eax进行读取,因此没有区别。区别在于mov ah, 5不需要读取,但仍然需要读取。 mov eax, 5可以从中受益,ah不能。

在ah / al交换测试中,我们可以看到寄存器重命名可能对所有“ mov ah,5; mov al,5”的写操作都有帮助。看起来“ ah”和“ al”有自己的寄存器可以使用,然后可以并行进行,从而节省了大量时间。使用ah / al / eax测试,它几乎与eax测试一样快!在这种情况下,我预测所有三个都拥有自己的寄存器,并且代码高度并行化,即使单独写入ah / al的成本很高。当然,当必须合并ah / al时,尝试在该循环中的任何地方读取eax都会降低性能:

时间:3.412s,3.390s,3.515s

global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
xor eax, 5
mov al, 6
mov ah, 8
xor eax, 5
mov al, 8
dec ecx
jmp loop
exit:
ret

上面的测试没有控制组,因为它使用xor而不是mov(如果仅使用“ xor”会很慢,该怎么办)。因此,这是一个将其与以下内容进行比较的测试:

次数:1.426s,1.424s,1.392s

global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
xor ah, 5
mov al, 6
mov ah, 8
xor ah, 5
mov al, 8
dec ecx
jmp loop
exit:
ret

过去的两个测试显示了部分寄存器停顿,我起初甚至没有考虑过。我首先想到重命名寄存器将有助于缓解此问题,他们肯定会在ah / al混合和ah / al / eax混合中做到这一点。但是,读取带有脏ah / al值的eax是残酷的,因为处理器现在必须合并ah / al寄存器。看来处理器制造商认为重命名部分寄存器仍然值得,但是这是有道理的,因为大多数使用ah / al的工作都不会涉及读取传真。这样一来,紧紧的循环会给ah / al带来极大的好处,而唯一的危害是下次使用eax时会遇到麻烦(此时不再使用ah / al)。

总体而言,即使在没有部分寄存器停顿的情况下,对ah的写入也比对eax的写入要慢得多,这就是我试图克服的问题。

当然,结果可能会有所不同。其他处理器(很可能是很旧的处理器)可能具有控制位来关闭总线的一半,这将允许总线在需要时像8位总线一样工作。这些控制位必须通过逻辑门连接到寄存器(即,连接到触发器的复位标志),这将大大降低它们的速度,因为现在在寄存器更新之前还有一个门需要经过。由于此类控制位在大多数情况下都不可用,因此英特尔似乎决定不这样做(出于充分的理由)。

答案 1 :(得分:0)

var displaySortedTaskList = function(...args) { if (args.length !== 3) { document.getElementById("message").textContent = "The displaySortedTaskList function of the tasklist library requires three arguments"; return; // or `throw new Error('not enough args')` ? } const [tasks, div, handler] = args; // rest of your code .textContent有两种实际用途。它可以节省内存,这很重要,这不是因为主流计算机会用完,而是因为它可以让更多数据放入CPU的缓存中,这一点很重要。而且有时您还需要精确地指定内存中的布局,例如设备驱动程序或数据包头。

指令本身并没有更快的速度(正如Nicholas Pipitone的精彩回答所示),并且可能需要更多或更少的字节进行编码。在某些情况下,您也许可以改善寄存器分配。