我读过的介绍性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位寄存器有什么优势吗?
答案 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
在引导加载程序和其他位提琴手之外未被使用:当您写入al
或ah
时,另一个不会被破坏!在16位时代,这很棒,因为现在您拥有的寄存器数量是和的两倍,而您拥有的是32位寄存器!但是随着寄存器数量的增加,我们不再需要更多的寄存器。我们需要的是快速寄存器和更高的GHz。从这个角度来看,每次您写入al
或ah
时,您仍然必须从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, 5
和add 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的精彩回答所示),并且可能需要更多或更少的字节进行编码。在某些情况下,您也许可以改善寄存器分配。