当前位置: 首页 > 知识库问答 >
问题:

修改64位变量的低字节,如rax/al寄存器,而没有编译器开销

薛承基
2023-03-14

我有一段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
}

共有1个答案

阳光辉
2023-03-14

为什么不干脆写 <罢工> 内联 汇编代码?对于真正高度关键的代码,它可能是唯一的解决方案......我必须这样做才能在裸机平台上从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而