在x86-64 Tour of Intel Manuals中,我读到了
也许最令人惊讶的事实是,诸如
MOV EAX, EBX自动将指令的高32位归零的指令RAX.
同一来源引用的英特尔文档(3.4.1.1 64位手动基本架构中的通用寄存器)告诉我们:
- 64位操作数在目标通用寄存器中生成64位结果.
- 32位操作数生成32位结果,在目标通用寄存器中零扩展为64位结果.
- 8位和16位操作数生成8位或16位结果.目标通用寄存器的高56位或48位(分别)不会被操作修改.如果8位或16位操作的结果用于64位地址计算,则将寄存器显式符号扩展为完整的64位.
在x86-32和x86-64汇编中,16位指令如
mov ax, bx
Run Code Online (Sandbox Code Playgroud)
不要表现出这种"奇怪"的行为,即eax的上层词被归零.
因此:引入这种行为的原因是什么?乍一看似乎不合逻辑(但原因可能是我习惯了x86-32汇编的怪癖).
例如,如果一个32位整数溢出,而不是升级int到long,如果我们需要一个仅在2 40之内的范围,我们可以使用一些40位类型,这样我们就可以节省24(64-40)位整数?
如果是这样,怎么样?
我必须处理数十亿和空间是一个更大的约束.
我发现了这个有趣且功能强大的工具IACA(英特尔架构代码分析器),但我无法理解它.我能用它做什么,它的局限性是什么?我该怎么做:
如果允许在输入缓冲区末尾读取少量数据,则可以(并且)简化在高性能算法中找到的许多方法.这里,"少量"通常意味着W - 1超过结束的字节,其中W是算法的字节大小(例如,对于处理64位块中的输入的算法,最多7个字节).
很明显,写入输入缓冲区的末尾通常是不安全的,因为您可能会破坏缓冲区1之外的数据.同样清楚的是,在缓冲区的末尾读取到另一页面可能会触发分段错误/访问冲突,因为下一页可能不可读.
但是,在读取对齐值的特殊情况下,页面错误似乎是不可能的,至少在x86上是这样.在该平台上,页面(以及因此内存保护标志)具有4K粒度(较大的页面,例如2MiB或1GiB,可能,但这些是4K的倍数),因此对齐的读取将仅访问与有效页面相同的页面中的字节缓冲区的一部分.
这是一个循环的规范示例,它对齐其输入并在缓冲区末尾读取最多7个字节:
int processBytes(uint8_t *input, size_t size) {
uint64_t *input64 = (uint64_t *)input, end64 = (uint64_t *)(input + size);
int res;
if (size < 8) {
// special case for short inputs that we aren't concerned with here
return shortMethod();
}
// check the first 8 bytes
if ((res = match(*input)) >= 0) {
return input + res;
}
// align pointer to the next 8-byte …Run Code Online (Sandbox Code Playgroud) 此循环在英特尔Conroe/Merom上每3个周期运行一次,imul按预期方式在吞吐量方面存在瓶颈.但是在Haswell/Skylake上,它每11个循环运行一次,显然是因为setnz al它依赖于最后一个循环imul.
; synthetic micro-benchmark to test partial-register renaming
mov ecx, 1000000000
.loop: ; do{
imul eax, eax ; a dep chain with high latency but also high throughput
imul eax, eax
imul eax, eax
dec ecx ; set ZF, independent of old ZF. (Use sub ecx,1 on Silvermont/KNL or P4)
setnz al ; ****** Does this depend on RAX as well as ZF?
movzx eax, al
jnz .loop ; }while(ecx);
Run Code Online (Sandbox Code Playgroud)
如果setnz al …
write(1,"hi",3)在linux上反汇编,gcc -s -nostdlib -nostartfiles -O3结果如下:
ba03000000 mov edx, 3 ; thanks for the correction jester!
bf01000000 mov edi, 1
31c0 xor eax, eax
e9d8ffffff jmp loc.imp.write
Run Code Online (Sandbox Code Playgroud)
我不是到编译器的开发,但由于移动到这些寄存器的每一个值是恒定的和已知的编译时间,我很好奇,为什么不GCC使用dl,dil和al来代替.也许有人会说,此功能不会让任何性能上的差异,但有一个在之间的可执行文件的大小有很大的区别mov $1, %rax => b801000000,并mov $1, %al => b001当我们谈论数千寄存器的程序访问.如果软件的优雅部分不仅体积小,它确实会对性能产生影响.
有人可以解释为什么"海湾合作委员会决定"它无所谓?
在汇编编程中,想要从寄存器的低位计算某些东西是相当普遍的,这些位不能保证将其他位置零.在像C这样的高级语言中,你只需将输入转换为小尺寸,让编译器决定是否需要分别将每个输入的高位归零,或者是否可以在输出之后切断结果的高位.事实.
这是为x86-64的(又名AMD64),出于各种原因尤其常见1,其中的一些是存在于其它的ISA.
我将使用64位x86作为示例,但目的是询问/讨论2的补码和无符号二进制算法,因为所有现代CPU都使用它.(注意,C和C++不保证两个补码4,并且有符号溢出是未定义的行为.)
作为示例,考虑一个可以编译为LEA指令2的简单函数.(在X86-64 SysV的(Linux)的ABI 3,前两个函数参数是rdi和rsi,与在返回rax. int是一个32位的类型.)
; int intfunc(int a, int b) { return a + b*4 + 3; }
intfunc:
lea eax, [edi + esi*4 + 3] ; the obvious choice, but gcc can do better
ret
Run Code Online (Sandbox Code Playgroud)
gcc知道即使是负有符号整数,加法也只是从右到左,所以输入的高位不会影响进入的内容eax.因此,它保存了一个指令字节并使用 lea eax, [rdi + rsi*4 + 3]
为什么它有效?
1为什么x86-64频繁出现这种情况:x86-64有可变长度指令,其中额外的前缀字节改变了操作数大小(从32到64或16),因此在指令中通常可以保存一个字节.以相同的速度执行.当写入低8b或16b的寄存器(或稍后读取完整寄存器(Intel pre-IvB)时的失速)时,它也具有错误依赖性(AMD/P4/Silvermont):由于历史原因, …