有一个 x86 汇编指令ADC
。我发现这意味着“带进位添加”。这是什么意思/做什么?如何在 C++ 中实现这条指令的行为?
信息:
在 Windows 上编译。我正在使用 32 位 Windows 安装。我的处理器是 Intel 的 Core 2 Duo。
ADC 与 ADD 相同,但如果设置了处理器的进位标志,则增加一个额外的 1。
但是,Intel 处理器有一条特殊指令,称为 adc。此命令的行为类似于 add 命令。唯一额外的事情是它还添加了值进位标志。因此,这对于添加大整数可能非常方便。假设您想用 16 位寄存器添加一个 32 位整数。我们怎么能做到这一点?好吧,假设第一个整数保存在寄存器对 DX:AX 上,第二个整数保存在 BX:CX 上。这是如何:
add ax, cx adc dx, bx
啊,所以首先,通过 add ax, cx 添加低 16 位。然后使用 adc 而不是 add 来添加更高的 16 位。这是因为:如果有溢出,进位位会自动加到高16位。所以,没有繁琐的检查。这种方法可以扩展到 64 位等等... 注意:如果 32 位整数加法在高 16 位也溢出,结果将不正确并设置进位标志,例如加 50 亿到50亿。
从这里开始,请记住,它几乎属于实现定义的行为区域。
这是一个适用于 VS 2010(32 位,WinXp)的小示例
警告:$7.4/1-“asm 声明是有条件支持的;它的含义是实现定义的。[注意:通常它用于通过实现将信息传递给汇编程序。-结束说明]”
int main(){
bool carry = false;
int x = 0xffffffff + 0xffffffff;
__asm {
jc setcarry
setcarry:
mov carry, 1
}
}
ADC 行为可以在 C 和 C++ 中进行仿真。以下示例将两个数字相加(存储为无符号数组,因为它们太大而无法放入单个无符号数中)。
unsigned first[10];
unsigned second[10];
unsigned result[11];
.... /* first and second get defined */
unsigned carry = 0;
for (i = 0; i < 10; i++) {
result[i] = first[i] + second[i] + carry;
carry = (first[i] > result[i]);
}
result[10] = carry;
希望这可以帮助。
C++ 语言没有任何进位标志的概念,因此围绕ADC
指令制作内部函数包装器很笨重。然而,英特尔还是做到了: unsigned char _addcarry_u32 (unsigned char c_in, unsigned a, unsigned b, unsigned * out);
. 最后我检查了一下,gcc 在这方面做得很差(将进位结果保存到整数寄存器中,而不是将其留在 CF 中),但希望英特尔自己的编译器做得更好。
另请参阅x86标记 wiki 以获取程序集文档。
当添加比单个寄存器更宽的整数时,编译器将为您使用 ADC,例如添加int64_t
32 位代码或__int128_t
64 位代码。
#include <stdint.h>
#ifdef __x86_64__
__int128_t add128(__int128_t a, __int128_t b) { return a+b; }
#endif
# clang 3.8 -O3 for x86-64, SystemV ABI.
# __int128_t args passed in 2 regs each, and returned in rdx:rax
add rdi, rdx
adc rsi, rcx
mov rax, rdi
mov rdx, rsi
ret
Godbolt 编译器资源管理器的 asm 输出。clang-fverbose-asm
不是很冗长,但 gcc 5.3 / 6.1 浪费了两条mov
指令,因此可读性较差。
您有时可以手持编译器发出一个adc
或以其他方式使用add
成语uint64_t sum = a+b;
/的执行carry = sum < a;
。但是对于当前的编译器来说,将其扩展为从 anadc
而不是中执行add
是不可能的;c+d+carry_in
可以一直环绕,如果您安全地执行,编译器不会设法优化对每个+
in执行的多个检查。c+d+carry
_ExtInt
我知道有一种方法可以获得 add/adc/.../adc 链:Clang 的新_ExtInt(width)
功能提供了任何大小的固定位宽类型,最大为 16,777,215 位(博客文章)。它已于 2020 年 4 月 21 日添加到 clang 的开发版本中,因此尚未发布任何版本。
这有望在某个时候出现在 ISO C 和/或 C++ 中;N2472提案显然正在“被 ISO WG14 C 语言委员会积极考虑”
typedef _ExtInt(256) wide_int;
wide_int add ( wide_int a, wide_int b) {
return a+b;
}
-O2
使用x86-64 ( Godbolt )的 clang trunk 编译如下:
add(int _ExtInt<256>, int _ExtInt<256>):
add rsi, r9
adc rdx, qword ptr [rsp + 8]
adc rcx, qword ptr [rsp + 16]
mov rax, rdi # return the retval pointer
adc r8, qword ptr [rsp + 24] # chain of ADD / 3x ADC!
mov qword ptr [rdi + 8], rdx # store results to mem
mov qword ptr [rdi], rsi
mov qword ptr [rdi + 16], rcx
mov qword ptr [rdi + 24], r8
ret
显然_ExtInt
是在整数寄存器中按值传递,直到调用约定用完寄存器。(至少在这个早期版本中;也许 x86-64 SysV 应该将它归类为“内存”,当它比 2 或 3 个寄存器更宽时,比如大于 16 字节的结构。虽然比结构更多,但将它放在寄存器中很可能是有用。只需将其他 args 放在首位,这样它们就不会移位。)
第一个 _ExtInt 参数在 R8:RCX:RDX:RSI 中,第二个在 R9 中有它的低位 qword,其余的在内存中。
指向返回值对象的指针作为 RDI 中隐藏的第一个参数传递;x86-64 System V 只返回最多 2 个整数寄存器 (RDX:RAX),这不会改变这一点。
这有一个错误。试试这个输入:
unsigned first[10] = {0x00000001};
unsigned second[10] = {0xffffffff, 0xffffffff};
结果应该是 {0, 0, 1, ...} 但结果是 {0, 0, 0, ...}
更改此行:
carry = (first[i] > result[i]);
对此:
if (carry)
carry = (first[i] >= result[i]);
else
carry = (first[i] > result[i]);
修复它。
int32_t adc(uint32_t first, uint32_t second, uint32_t *carry)
{
uint32_t res;
uint32_t carry_out = 0;
if (!*carry)
{
res = first + second;
*carry = (res < first) && (res < second);
return res;
}
res = adc(first, second, &carry_out);
if (*carry)
{
res++;
carry_out |= !res;
}
*carry = carry_out;
return res;
}