设置进位标志的最快方法
Fastest way to set a Carry Flag
我正在做一个循环来对两个数组求和。我的 objective 是通过避免进位检查 c = a + b; carry = (c<a)
来实现的。当我使用 cmp
指令进行循环测试时,我丢失了 CF
。
目前,我正在使用 JE
和 STC
来测试和设置 CF
之前保存的状态。但是跳跃需要更多更少的7个周期,这对我想要的来说已经很多了。
//This one is working
asm(
"cmp [=10=],%0;"
"je 0f;"
"stc;"
"0:"
"adcq %2, %1;"
"setc %0"
: "+r" (carry), "+r" (anum)
: "r" (bnum)
);
我已经尝试使用 SAHF
(2 + 2(mov) 周期),但没有用。
//Do not works
asm(
"mov %0, %%ah;"
"sahf;"
"adcq %2, %1;"
"setc %0"
: "+r" (carry), "+r" (anum)
: "r" (bnum)
);
有谁知道可以更快地设置 CF
的方法吗?比如直接移动或类似的东西..
。请参阅 link 以获得更好的 asm 循环。
不要试图在 C 循环中仅使用内联汇编编写 adc
。这是不可能的,因为你不能要求 gcc 不破坏标志。尝试使用 GNU C 内联 asm 学习 asm 比编写独立函数更难,尤其是。在这种情况下,您试图保留进位标志。
您可以使用 setnc %[carry]
保存并使用 subb , %[carry]
恢复。 (或者我猜是 cmpb , %[carry]
。)或者正如 Stephen 指出的那样,negb %[carry]
.
0 - 1
产生进位,但 1 - 1
不产生进位。
使用 uint8_t
变量来保存进位,因为您永远不会将它直接添加到 %[anum]
。这避免了 的任何机会。例如
uint8_t carry = 0;
int64_t numa, numb;
for (...) {
asm ( "negb %[carry]\n\t"
"adc %[bnum], %[anum]\n\t"
"setc %[carry]\n\t"
: [carry] "+&r" (carry), [anum] "+r" (anum)
: [bnum] "rme" (bnum)
: // no clobbers
);
}
您还可以为寄存器源 reg/mem dest 提供替代约束模式。我使用 an x86 "e"
constraint 而不是 "i"
,因为 64 位模式仍然只允许 32 位符号扩展立即数。 gcc 必须自己将更大的编译时常量放入寄存器中。进位被早期破坏,所以即使它和 bnum
都是 1
开始,gcc 也不能对两个输入使用相同的寄存器。
这仍然很糟糕,并且将循环携带的依赖链的长度从 2c 增加到 4c(Intel pre-Broadwell),或者从 1c 到 3c(Intel BDW/Skylake,和 AMD)。
所以你的循环以 1/3 的速度运行,因为你使用了一个 kludge 而不是在 asm 中编写整个循环。
此答案的先前版本建议直接添加进位,而不是将其恢复为 CF
。这种方法有一个致命的缺陷:它混淆了进入本次迭代的传入进位和进入下一次迭代的传出进位。
此外,sahf
是从标志中设置 AH。 lahf
是将 AH 加载到标志中(它对标志的整个低 8 位进行操作。将这些指令配对;不要在从 setc
获得的 0 或 1 上使用 lahf
.
阅读 insn 集参考手册,了解任何似乎没有按照您的预期工作的 insn。参见 https://whosebug.com/tags/x86/info
如果数组大小在编译时已知,你可以这样做:
#include <inttypes.h>
#include <malloc.h>
#include <stdio.h>
#include <memory.h>
#define str(s) #s
#define xstr(s) str(s)
#define ARRAYSIZE 4
asm(".macro AddArray2 p1, p2, from, to\n\t"
"movq (\from*8)(\p2), %rax\n\t"
"adcq %rax, (\from*8)(\p1)\n\t"
".if \to-\from\n\t"
" AddArray2 \p1, \p2, \"(\from+1)\", \to\n\t"
".endif\n\t"
".endm\n");
asm(".macro AddArray p1, p2, p3\n\t"
"movq (\p2), %rax\n\t"
"addq %rax, (\p1)\n\t"
".if \p3-1\n\t"
" AddArray2 \p1, \p2, 1, (\p3-1)\n\t"
".endif\n\t"
".endm");
int main()
{
unsigned char carry;
// assert(ARRAYSIZE > 0);
// Create the arrays
uint64_t *anum = (uint64_t *)malloc(ARRAYSIZE * sizeof(uint64_t));
uint64_t *bnum = (uint64_t *)malloc(ARRAYSIZE * sizeof(uint64_t));
// Put some data in
memset(anum, 0xff, ARRAYSIZE * sizeof(uint64_t));
memset(bnum, 0, ARRAYSIZE * sizeof(uint64_t));
bnum[0] = 1;
// Print the arrays before the add
printf("anum: ");
for (int x=0; x < ARRAYSIZE; x++)
{
printf("%I64x ", anum[x]);
}
printf("\nbnum: ");
for (int x=0; x < ARRAYSIZE; x++)
{
printf("%I64x ", bnum[x]);
}
printf("\n");
// Add the arrays
asm ("AddArray %[anum], %[bnum], " xstr(ARRAYSIZE) "\n\t"
"setc %[carry]" // Get the flags from the final add
: [carry] "=q"(carry)
: [anum] "r" (anum), [bnum] "r" (bnum)
: "rax", "cc", "memory"
);
// Print the result
printf("Result: ");
for (int x=0; x < ARRAYSIZE; x++)
{
printf("%I64x ", anum[x]);
}
printf(": %d\n", carry);
}
这给出了这样的代码:
mov (%rsi),%rax
add %rax,(%rbx)
mov 0x8(%rsi),%rax
adc %rax,0x8(%rbx)
mov 0x10(%rsi),%rax
adc %rax,0x10(%rbx)
mov 0x18(%rsi),%rax
adc %rax,0x18(%rbx)
setb %bpl
由于对所有 f 加 1 将完全溢出所有内容,以上代码的输出为:
anum: ffffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffff
bnum: 1 0 0 0
Result: 0 0 0 0 : 1
如所写,ARRAYSIZE 最多可以包含大约 100 个元素(由于 gnu 的宏深度嵌套限制)。好像应该够了...
我正在做一个循环来对两个数组求和。我的 objective 是通过避免进位检查 c = a + b; carry = (c<a)
来实现的。当我使用 cmp
指令进行循环测试时,我丢失了 CF
。
目前,我正在使用 JE
和 STC
来测试和设置 CF
之前保存的状态。但是跳跃需要更多更少的7个周期,这对我想要的来说已经很多了。
//This one is working
asm(
"cmp [=10=],%0;"
"je 0f;"
"stc;"
"0:"
"adcq %2, %1;"
"setc %0"
: "+r" (carry), "+r" (anum)
: "r" (bnum)
);
我已经尝试使用 SAHF
(2 + 2(mov) 周期),但没有用。
//Do not works
asm(
"mov %0, %%ah;"
"sahf;"
"adcq %2, %1;"
"setc %0"
: "+r" (carry), "+r" (anum)
: "r" (bnum)
);
有谁知道可以更快地设置 CF
的方法吗?比如直接移动或类似的东西..
不要试图在 C 循环中仅使用内联汇编编写 adc
。这是不可能的,因为你不能要求 gcc 不破坏标志。尝试使用 GNU C 内联 asm 学习 asm 比编写独立函数更难,尤其是。在这种情况下,您试图保留进位标志。
您可以使用 setnc %[carry]
保存并使用 subb , %[carry]
恢复。 (或者我猜是 cmpb , %[carry]
。)或者正如 Stephen 指出的那样,negb %[carry]
.
0 - 1
产生进位,但 1 - 1
不产生进位。
使用 uint8_t
变量来保存进位,因为您永远不会将它直接添加到 %[anum]
。这避免了
uint8_t carry = 0;
int64_t numa, numb;
for (...) {
asm ( "negb %[carry]\n\t"
"adc %[bnum], %[anum]\n\t"
"setc %[carry]\n\t"
: [carry] "+&r" (carry), [anum] "+r" (anum)
: [bnum] "rme" (bnum)
: // no clobbers
);
}
您还可以为寄存器源 reg/mem dest 提供替代约束模式。我使用 an x86 "e"
constraint 而不是 "i"
,因为 64 位模式仍然只允许 32 位符号扩展立即数。 gcc 必须自己将更大的编译时常量放入寄存器中。进位被早期破坏,所以即使它和 bnum
都是 1
开始,gcc 也不能对两个输入使用相同的寄存器。
这仍然很糟糕,并且将循环携带的依赖链的长度从 2c 增加到 4c(Intel pre-Broadwell),或者从 1c 到 3c(Intel BDW/Skylake,和 AMD)。
所以你的循环以 1/3 的速度运行,因为你使用了一个 kludge 而不是在 asm 中编写整个循环。
此答案的先前版本建议直接添加进位,而不是将其恢复为 CF
。这种方法有一个致命的缺陷:它混淆了进入本次迭代的传入进位和进入下一次迭代的传出进位。
此外,sahf
是从标志中设置 AH。 lahf
是将 AH 加载到标志中(它对标志的整个低 8 位进行操作。将这些指令配对;不要在从 setc
获得的 0 或 1 上使用 lahf
.
阅读 insn 集参考手册,了解任何似乎没有按照您的预期工作的 insn。参见 https://whosebug.com/tags/x86/info
如果数组大小在编译时已知,你可以这样做:
#include <inttypes.h>
#include <malloc.h>
#include <stdio.h>
#include <memory.h>
#define str(s) #s
#define xstr(s) str(s)
#define ARRAYSIZE 4
asm(".macro AddArray2 p1, p2, from, to\n\t"
"movq (\from*8)(\p2), %rax\n\t"
"adcq %rax, (\from*8)(\p1)\n\t"
".if \to-\from\n\t"
" AddArray2 \p1, \p2, \"(\from+1)\", \to\n\t"
".endif\n\t"
".endm\n");
asm(".macro AddArray p1, p2, p3\n\t"
"movq (\p2), %rax\n\t"
"addq %rax, (\p1)\n\t"
".if \p3-1\n\t"
" AddArray2 \p1, \p2, 1, (\p3-1)\n\t"
".endif\n\t"
".endm");
int main()
{
unsigned char carry;
// assert(ARRAYSIZE > 0);
// Create the arrays
uint64_t *anum = (uint64_t *)malloc(ARRAYSIZE * sizeof(uint64_t));
uint64_t *bnum = (uint64_t *)malloc(ARRAYSIZE * sizeof(uint64_t));
// Put some data in
memset(anum, 0xff, ARRAYSIZE * sizeof(uint64_t));
memset(bnum, 0, ARRAYSIZE * sizeof(uint64_t));
bnum[0] = 1;
// Print the arrays before the add
printf("anum: ");
for (int x=0; x < ARRAYSIZE; x++)
{
printf("%I64x ", anum[x]);
}
printf("\nbnum: ");
for (int x=0; x < ARRAYSIZE; x++)
{
printf("%I64x ", bnum[x]);
}
printf("\n");
// Add the arrays
asm ("AddArray %[anum], %[bnum], " xstr(ARRAYSIZE) "\n\t"
"setc %[carry]" // Get the flags from the final add
: [carry] "=q"(carry)
: [anum] "r" (anum), [bnum] "r" (bnum)
: "rax", "cc", "memory"
);
// Print the result
printf("Result: ");
for (int x=0; x < ARRAYSIZE; x++)
{
printf("%I64x ", anum[x]);
}
printf(": %d\n", carry);
}
这给出了这样的代码:
mov (%rsi),%rax
add %rax,(%rbx)
mov 0x8(%rsi),%rax
adc %rax,0x8(%rbx)
mov 0x10(%rsi),%rax
adc %rax,0x10(%rbx)
mov 0x18(%rsi),%rax
adc %rax,0x18(%rbx)
setb %bpl
由于对所有 f 加 1 将完全溢出所有内容,以上代码的输出为:
anum: ffffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffff
bnum: 1 0 0 0
Result: 0 0 0 0 : 1
如所写,ARRAYSIZE 最多可以包含大约 100 个元素(由于 gnu 的宏深度嵌套限制)。好像应该够了...