ADC
。我发现这意味着"带进位相加"。这是什么意思/作用?如何在C++中实现这个指令的行为?
信息:在Windows上编译。我正在使用32位Windows安装程序。我的处理器是英特尔的Core 2 Duo。
ADC与ADD相同,但是如果处理器的进位标志被设置,它会额外添加1。
setc al
(如果需要,则为movzx eax,al
)相比,那个asm序列有点糟糕。在英特尔SnB系列CPU上,pushf
是一个3-uop指令。push/pop存储转发往返会向涉及CF的依赖链添加约5个周期的延迟。 - Peter Cordes然而,英特尔处理器有一条特殊的指令叫做adc。这个命令的行为与add命令类似,唯一的额外之处是它还会将进位标志也加上。因此,在添加大整数时,这可能非常方便。假设您想要使用16位寄存器添加32位整数。我们该怎么办呢?好吧,假设第一个整数存储在DX:AX寄存器对中,第二个整数存储在BX:CX中。方法如下:
啊,首先,低16位使用 add ax,cx 进行相加。然后高16位使用 adc 而不是 add 进行相加。这是因为:如果有溢出,在高16位中自动添加进位标志位。所以,不需要繁琐的检查。这种方法可以扩展到64位等等...请注意:如果32位整数相加在高16位也发生了溢出,则结果将不正确,并且进位标志位被设置,例如将50亿加到50亿。add ax, cx adc dx, bx
从这里开始的所有内容都记住它们基本上属于实现定义行为的范畴。
这是一个在VS 2010(32位,WinXp)上运行的小样例。
警告:$7.4/1- "asm 声明是有条件支持的;其含义是实现定义的。[ 注:通常用于通过实现向汇编程序传递信息。—注释结束 ]"int main(){ bool carry = false; int x = 0xffffffff + 0xffffffff; __asm { jc setcarry setcarry: mov carry, 1 } }
asm
块外的C语句中依赖于CF
是否被设置。它可能在调试模式下运行良好,但启用优化后就没什么用了。此外,请使用setc carry
根据CF
将进位设置为0或1。 - Peter CordesADC
指令是很麻烦的。然而,英特尔还是做到了:unsigned char _addcarry_u32 (unsigned char c_in, unsigned a, unsigned b, unsigned * out);
。据我所知,gcc对此做得不好(将进位结果保存到整数寄存器中,而不是留在CF中),但希望英特尔自己的编译器能做得更好。请参阅x86标签wiki以获取汇编文档。
当添加比单个寄存器更宽的整数时,例如在32位代码中添加int64_t
或在64位代码中添加__int128_t
,编译器将为您使用ADC。
#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编译器浏览器的汇编输出。clang的-fverbose-asm
不太详细,但是gcc 5.3/6.1浪费了两个mov
指令,因此不易阅读。
有时候你可以通过使用习语 uint64_t sum = a+b;
/ carry = sum < a;
强制编译器发出 adc
或者其他使用 add
的进位标志。但是,使用当前的编译器无法将此扩展为从 adc
而不是 add
获取进位标志;如果你安全地在 c+d+carry
中执行多个进位标志检查,则可能会导致 c+d+carry_in
回绕。编译器无法优化每个 +
上的进位标志检查。
_ExtInt
我知道一种使用 add/adc/.../adc 链的方法:Clang 的新特性 _ExtInt(width)
,它提供了任意大小(最大可达16,777,215位)的固定位宽类型(blog post)。该特性于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;
}
使用clang trunk -O2
编译x86-64的代码(Godbolt)如下:
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字节的结构体一样。尽管与结构不同,但将其放在寄存器中可能很有用。只需将其他参数放在前面,使它们不被替换即可。)add
、adc
指令链。你认为现在有可能吗? - madhur4127sum=a+b;
/ carry = sum<a;
技巧或者__int128
是可行的。但是对于更长的链条,据我所知,即使使用_addcarry_u32
编译器仍然很糟糕。 - Peter Cordes_ExtInt
可以让您使用任何宽度高达 16,777,215 位的固定宽度整数类型。(http://blog.llvm.org/2020/04/the-new-clang-extint-feature-provides.html)。 https://godbolt.org/z/bsDCvh 显示 _ExtInt(256)
上的 +
编译为 add
/ adc
/ adc
/ adc
。 - Peter Cordesunsigned 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;
second==~0U && carry==1
,那么这段代码将无法设置进位。例如:对于32位无符号整数来说,这将是 second[i]==0xFFFFFFFF && carry==1
。在这种情况下,即使发生了溢出(进位),first[i] == result[i]
仍然成立。 - Stephane Hockenhullunsigned tmp = second[i] + carry; result[i] = first[i] + tmp; carry = (first[i] > result[i]) | (second[i] > tmp);
。 - Stephane Hockenhulladd rax,rbx
执行rax = rax + rbx
。在C++中,您可以这样模拟ADD:
uint64_t a, b;
bool carry;
a += b;
carry = (a < b); // a+b can't be smaller than b: there must have been an overflow
ADC指令类似于ADD,但会将进位标志加到结果中:
adc rax, rbx
执行rax = rax + rbx + 进位标志
。
如果存在无符号溢出,它还会设置进位标志。
在C++中:
uint64_t tmp = b + carry;
a += tmp;
carry = (tmp < carry) + (a < tmp); // only one overflow can happen
ADD和ADC指令可用于添加具有n“位”的大整数。
对于最低有效数字,请使用ADD,然后使用ADC(n - 1)次来添加其余数字。
这是“竖式加法算法”。
例如,使用四个64位“数字”添加256位大整数:
mov rax, [rsi] ; load the least significant source digit
mov rbx, [rsi + 8] ; ...
mov rcx, [rsi + 16]
mov rdx, [rsi + 24]
add [rdi], rax ; add it to the least significant destination digit
adc [rdi + 8], rbx ; ... propagate carry up
adc [rdi + 16], rcx
adc [rdi + 24], rdx
最近版本的clang
编译器可以识别大整数加法并且使用ADD/ADC实现。
constexpr uint64_t n = 4;
uint64_t dst[n], src[n];
// Add src to dst.
uint64_t carry = 0;
for (int i = 0; i < n; i++) {
uint64_t tmp = src[i] + carry;
dst[i] += tmp;
carry = (tmp < carry) + (dst[i] < tmp);
}
adc
指令链(而clang 8及更早版本则会错过peephole优化,并使用setc
或setb
将carry
实现为0/1)。 - Peter Cordes这里有一个bug。请尝试输入以下内容:
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]);
carry=(carry&&first[i]>=result[i])||(!carry&&first[i]>result[i])
避免分支并且执行相同的操作,如果有人感兴趣的话。 - Hassedev||
和&&
运算符会导致分支,因为它们仅在必要时才评估右侧操作数。与易于阅读的if()
语句相比,这个单行代码中有更多的分支。 - Stephane Hockenhulltemplate <class Ty>
constexpr bool add_carry_ux(bool carry_in, Ty src1, Ty src2, Ty* sum_out)
{
const Ty sum = src1 + src2;
const bool carry_out = (sum < src1) | ((sum == ~static_cast<Ty>(0)) & carry_in);
*sum_out = sum + carry_in;
return carry_out;
}
add_carry_ux(bool, unsigned long, unsigned long, unsigned long*):
add rsi, rdx
movzx eax, dil
setc dl
add rax, rsi
cmp rsi, -1
mov QWORD PTR [rcx], rax
sete al
movzx edx, dl
and eax, edi
or eax, edx
ret
adc
指令。当内联时,是否能够更好地处理进位输入和输出在CF寄存器中的情况呢?因为这段代码看起来有很多汇编语句。 - Peter Cordesint32_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;
}
result >> 16
右移即可获得高半部分(进位输出),或者直接使用该更宽的类型,并让编译器使用 adc
或目标ISA上有效的任何内容来实现它。 - Peter Cordesunsigned int
实际上是一个像你所假设的16位类型,那么在你赋值给result
之前,first + second
已经发生了包装。也许你的意思是first + (unsigned long)second
? - Peter Cordesresult < second
足以检测到进位,但你不应该将其添加到this元素中,而应该单独返回它。这就像add eax,ecx
/ adc eax,0
,这几乎永远不是你想要的。 - Peter Cordes0xffffffff + 0 + carry=1
这样的情况下会失败。while循环执行 0xffffffff + 1 = 0
,产生一个进位,使得while循环再次运行,产生res=1和carry=0。然后它通过尾调用自身来执行 1 + 0
,返回1并将carry留为0,而正确的结果是 0
和carry=1。在 first + second + *carry
中的任何一个 +
操作中,都需要将进位发送到最终的 *carry
输出中,而不是加回到返回值中。请记住,进位是32+32 => 33位加法的第33位,但进位具有仅为1的位值。 - Peter Cordes