我有一段html" target="_blank">性能关键的代码,实际上包含uint8\u t变量(保证不会溢出),这些变量是从其他uint8\u t值递增的,也用作数组索引和其他64位地址计算的一部分。
我已经研究了MSVC编译器的反汇编,并进行了充分的优化,令人恼火的是,无论我如何尝试,在8位和64位操作之间进行转换时,都有大量过量的movzx和其他不必要的附加指令。如果我使用8位变量,地址计算会通过临时寄存器执行额外的零扩展等。如果我使用64位变量,则会对添加到其中的其他8位值执行类似的额外操作。
如果这是用汇编编写的,就没有问题,因为可以根据需要同时使用例如rax和al寄存器访问该值。有没有办法用C访问uint64_t变量的低字节(尤其是用于执行添加),以便MSVC足够聪明,可以在完整变量在rax寄存器中时使用简单的直接al寄存器访问(例如添加al,other_uint8_var)来编译它?
我尝试了几种替代方法,如用于模拟低字节更改的位掩码高/低部分、使用8位和64位值的并集进行别名、使用临时8位参考变量对64位值进行别名等。所有这些都只会导致更糟糕的结果,通常会使变量从寄存器移动到临时内存位置以执行更改。
#include <stdint.h>
unsigned idx(unsigned *ptr, uint8_t *v)
{
uint8_t tmp = v[1] + v[2]; // truncate the sum to 8-bit
return ptr[tmp]; // before using with a 64-bit pointer
}
所有编译器(Godbolt:GCC11/clang14/MSVC19.31/ICC19.01)都做得不好,浪费了一个movzx eax,al,因为它们在同一寄存器内加宽,甚至不能从零延迟的mov消除中受益。MSVC 19.31-O2编译为:
unsigned int idx(unsigned int *,unsigned char *) PROC ; idx, COMDAT
movzx eax, BYTE PTR [rdx+2]
add al, BYTE PTR [rdx+1]
movzx eax, al ;; fully redundant, value already truncated to 8-bit and zero-extended to 64
mov eax, DWORD PTR [rcx+rax*4]
ret 0
Clang/LLVM实际上做得更糟,从mov-al、[mem]加载开始,错误地依赖于RAX的旧值(在除P6系列和第一代Sandybridge之外的CPU上)。但它节省了一个字节的机器代码大小。
以下可运行程序生成3个随机整数,这些整数相加为数组索引。有趣的部分放在test()函数中,以强制编译器使用所需的参数类型,并将该部分分开,以便从其他内联代码中轻松查看程序集。
#include <iostream>
#include <cstdlib>
static constexpr uint64_t array[30]{ 0,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 };
struct Test {
__declspec(noinline) static uint64_t test(uint64_t base, uint8_t added1, uint8_t added2) {
uint64_t index = base;
index += added1;
index += added2;
return array[index];
}
};
int main()
{
uint64_t base = rand() % 10;
uint8_t added1 = rand() % 10;
uint8_t added2 = rand() % 10;
uint64_t result = Test::test(base, added1, added2);
std::cout << "array[" << base << "+" << (uint64_t)added1 << "+" << (uint64_t)added2 << "]=" << result << std::endl;
return 0;
}
以上带有uint64基索引的测试函数编译为:
uint64_t test(uint64_t base, uint8_t added1, uint8_t added2) {
movzx edx,dl
add rcx,rdx
movzx eax,r8b
add rax,rcx
lea rcx,[array (07FF74FD63340h)]
mov rax,qword ptr [rcx+rax*8]
ret
}
编译器分配了rcx=base, dl=added1, r8b=added2。uint8_t值在求和之前分别为零扩展。
将基索引更改为uint8\t编译:
uint64_t test(uint8_t base, uint8_t added1, uint8_t added2) {
uint8_t index = base;
index += added1;
index += added2;
return array[index];
}
uint64_t test(uint8_t base, uint8_t added1, uint8_t added2) {
add cl,dl
add cl,r8b
movzx eax,cl
lea rcx,[array (07FF6287C3340h)]
mov rax,qword ptr [rcx+rax*8]
ret
}
因此,现在编译器很乐意使用8位寄存器进行数学运算,但需要单独对结果进行零扩展以进行寻址。
我想得到的基本上是上面没有movzx的,所以rcx将直接用作数组偏移量,因为我知道除了最低字节之外的所有字节都已经为零。显然编译器不能自动这样做,因为不像我,它不知道添加不会导致溢出。所以我缺少的是一种方法来告诉它。
如果我尝试将目标寄存器转换为8位(这往往适用于读取操作)或使用并集来建模变量,如rcx/cl寄存器,它会通过堆栈变量来实现:
uint64_t test(uint64_t base, uint8_t added1, uint8_t added2) {
uint64_t index = base;
reinterpret_cast<uint8_t&>(index) += added1;
reinterpret_cast<uint8_t&>(index) += added2;
return array[index];
}
OR:
union Register {
uint64_t u64;
uint8_t u8;
};
uint64_t test(uint64_t base, uint8_t added1, uint8_t added2) {
Register index;
index.u64 = base;
index.u8 += added1;
index.u8 += added2;
return array[index.u64];
}
uint64_t test(uint64_t base, uint8_t added1, uint8_t added2) {
mov qword ptr [rsp+8],rcx
add cl,dl
add cl,r8b
mov byte ptr [index],cl
lea rcx,[array (07FF798B53340h)]
mov rax,qword ptr [index]
mov rax,qword ptr [rcx+rax*8]
ret
}
试图通过一些位掩码来表明我的意图编译为:
uint64_t test(uint64_t base, uint8_t added1, uint8_t added2) {
uint64_t index = base;
index = (index & 0xffffffffffffff00) | (uint8_t)(index + added1);
index = (index & 0xffffffffffffff00) | (uint8_t)(index + added2);
return array[index];
}
uint64_t test(uint64_t base, uint8_t added1, uint8_t added2) {
lea eax,[rdx+rcx]
and rcx,0FFFFFFFFFFFFFF00h
movzx edx,al
or rdx,rcx
lea rcx,[array (07FF70F4B3340h)]
lea eax,[r8+rdx]
and rdx,0FFFFFFFFFFFFFF00h
movzx eax,al
or rax,rdx
mov rax,qword ptr [rcx+rax*8]
ret
}
我相信一些类似的模式可以在一些编译器上用于覆盖(而不是添加)低字节,但在这里,编译器显然无法识别该模式。另一个类似的模式产生了这一点:
uint64_t test(uint64_t base, uint8_t added1, uint8_t added2) {
uint64_t index = base;
index = (index & 0xffffffffffffff00) | (((index & 0xff) + added1) & 0xff);
index = (index & 0xffffffffffffff00) | (((index & 0xff) + added2) & 0xff);
return array[index];
}
uint64_t test(uint64_t base, uint8_t added1, uint8_t added2) {
movzx eax,dl
add rax,rcx
xor rax,rcx
movzx edx,al
xor rdx,rcx
movzx eax,r8b
add rax,rdx
lea rcx,[array (07FF6859F3340h)]
xor rax,rdx
movzx eax,al
xor rax,rdx
mov rax,qword ptr [rcx+rax*8]
ret
}
为什么不干脆写 <罢工> 内联 汇编代码?对于真正高度关键的代码,它可能是唯一的解决方案......我必须这样做才能在裸机平台上从CompactFlash读取,例如:C或C代码甚至无法匹配所需的时间......
此外,它可以帮助你,通过基准测试这个汇编程序代码,看看你是否真的得到了更多的性能-或者没有!-也许会给你一个目标和/或一个你可以期望达到的目标的指示。
如果您需要可移植性,您仍然可以进行条件编译,所有优化 <罢工> 内联 所有已知目标的汇编器,并为所有其他情况留下最好的C/C代码-不会是完美的,但并非所有CPU都有部分寄存器,因此通用C代码可以为这些平台完成任务。
所以,我有两个问题: > 我认为这种尴尬的行为必须在某个地方记录下来,但我似乎找不到详细的解释(关于64位寄存器的32位高值是如何被清除的)。对的写入总是擦除,还是更复杂?它是否适用于所有64位寄存器,或者有一些例外? 一个非常相关的问题提到了同样的行为,但是,唉,再次没有确切的文档参考。 只是我,还是这整件事看起来真的很奇怪和不合逻辑(即eax-ax-ah-al,rax-ax-ah-al有一种行
基本上指令有8->16、8->32、8->64、16->32和16->64。 32->64的转换在哪里?我必须使用签名版本吗? 如果是的话,您如何使用完整的64位来表示无符号整数?
#include <stdio.h> int main(void) { int a =0; a++; a++; printf("%d\n", a); return 0; } 技巧 PC寄存器会存储程序下一条要执行的指令,通过修改这个寄存器的值,
考虑以下x86程序集: 序列结束时,rax的值与输入时的值相同,但从CPU的角度来看,其值取决于从内存加载到rcx的值。特别是,在该加载和两个异或指令完成之前,不会开始后续使用rax。 有什么方法可以比两个异或序列更有效地实现这种效果,例如,使用单个单uop单周期延迟指令?如果某个常量值需要在序列之前设置一次(例如,有一个零寄存器),则可以。
问题内容: 我有用32位汇编语言编写的程序…现在,我无法在64位OS上对其进行编译。在我们学校,它们是特定的,程序必须以32位版本编写。这是我的程序: 任何的想法?我尝试了很多方法来编译它。编译后输出错误: 输出: 问题答案: 首先将更改为并将符号更改为,然后使用链接目标文件,该文件将自动链接至该文件, 您需要这样做,因为AFAIK如果没有,就无法链接至libc。另外,在汇编时也应使用elf32而