Assembly 如何让BSR指令在64位上工作?
我正在尝试查找无符号64位int的前导位。我正在使用BSR,因为我的处理器没有LZCNT指令。我的问题是,一旦输入正好是2^32,它将返回2^64作为前导位值,然后循环返回输出直到2^64 这是我的代码:Assembly 如何让BSR指令在64位上工作?,assembly,g++,x86-64,inline-assembly,Assembly,G++,X86 64,Inline Assembly,我正在尝试查找无符号64位int的前导位。我正在使用BSR,因为我的处理器没有LZCNT指令。我的问题是,一旦输入正好是2^32,它将返回2^64作为前导位值,然后循环返回输出直到2^64 这是我的代码: unsigned long int LeadingBit(unsigned long int a){ 如果(a==0) 返回0; 无符号长整数nlb; asm( BSR%1,%0\n :“=r”(nlb) :“先生”(a) :“抄送” ); 返回1如前所述,这一行是问题所在: return
unsigned long int LeadingBit(unsigned long int a){
如果(a==0)
返回0;
无符号长整数nlb;
asm(
BSR%1,%0\n
:“=r”(nlb)
:“先生”(a)
:“抄送”
);
返回1如前所述,这一行是问题所在:
return 1<<nlb;
正如所指出的,这条线就是问题所在:
return 1<<nlb;
虽然迈克尔/弗洛里安的修复可能是最简单的,但可能不是最好的
您现有的代码(用1UL修改)编译如下:
xor eax, eax
test rdi, rdi
je .L1
mov eax, 1
BSR rdi, rcx
sal rax, cl
.L1:
ret
不错,但与其测试零,然后调用BSR(也检查零),不如:
unsigned long int LeadingBit(unsigned long int a) {
unsigned long int nlb;
bool z;
asm (
"BSR %[a], %[nlb]"
: [nlb] "=r" (nlb), "=@ccz"(z)
: [a] "mr" (a)
);
unsigned long int one;
if (!z)
one = 1;
else
one = 0;
return one << nlb;
}
“=@ccz”约束在下面的文档中有描述。基本上它只是说“这个变量的值取自Z(C)条件(C)ode。”
编辑:看到Peter发表评论我并不感到惊讶,但我很惊讶他没有发表自己的备选方案
以下是我将他的评论翻译成代码的尝试。这可能比OP感兴趣的更复杂,但为了完整性:
unsigned long int LeadingBit(unsigned long int a) {
unsigned long int bit;
unsigned long int res;
asm (
"BSR %[a], %[bit] \n\t" // sets both (bit) and ZF
"BTS %[bit], %[res] \n\t" // sets the corresponding bit in (res) and
// carry flag, but *doesn't* change ZF
"CMOVZ %[zero], %[res]" // reset res to 0 if BSR detected a==0
: [bit] "=&r" (bit), [res] "=&r"(res)
: [a] "mr" (a), [zero] "r"(0UL), "[res]"(0UL)
: "cc"
);
return res;
}
虽然BSR/BTS/CMOVZ非常直截了当,但约束的废话可能是代码维护人员难以解决的问题
所以,来解释发生了什么
“=&r”(res)
将保留返回值。&
用于指示它不能与任何其他参数共享寄存器。请澄清,仅因为您将约束声明为“=r”,这并不意味着您将获得一个唯一的寄存器,除非您使用&
。通过减少使用的寄存器数量,这可能是一件好事,但在这种情况下不会太多。如果编译器决定对%[zero]和%[res]使用相同的寄存器,这将导致CMOVZ失败
“[res]”(0UL)
说明在进入asm时,无论使用哪种寄存器%[res]应该初始化为零。是的,我们可以在asm中执行此操作,但是通过将其放入C代码中,它允许编译器以任何可能对周围代码最有效的方式执行此操作。见鬼,它可能已经有一个零寄存器,它只是用于其他用途
BTS
允许您直接在寄存器中设置位号。它通常比使用SAL
更快(这就是Peter所说的SAL的“3 UOP”与BTS的“1 UOP”),但与BSR一样,gcc没有它的固有值。虽然它返回进位标志中指定位的现有值(这就是BTS的T部分的意思),它不修改零标志。这允许我们在BTS之后仍然执行CMOVZ。这样更新标志寄存器的“部分”在某些处理器上可能效率低下,但在较新的处理器上效率不高
以下是输出:
xorl %edx, %edx
movq %rdx, %rax
BSR %rdi, %rcx
BTS %rcx, %rax
CMOVZ %rdx, %rax
ret
我对movq
(为什么不xor
或movl
?)有点怀疑,但我相信这有一个很好的理由。我认为这与“别名”有关
如果perf具有足够高的优先级(尽管OP从来没有说过),那么我可以考虑做一件事。如果LeadBit可能被一个常量调用,编译器通常可以在编译时而不是在运行程序时预先计算它周围的大量数学数
但是,gcc无法通过内联asm进行预计算。如果可以使用常量调用LeadBit,您可以使用If(\uu builtin\u constant\u p(a))
来包装我在这里显示的代码,以查看a
是否为常量,并使用if
的一个分支。虽然Michael/Florian的修复方法可能是最简单的,但可能不是最好的
您现有的代码(用1UL修改)编译如下:
xor eax, eax
test rdi, rdi
je .L1
mov eax, 1
BSR rdi, rcx
sal rax, cl
.L1:
ret
不错,但与其测试零,然后调用BSR(也检查零),不如:
unsigned long int LeadingBit(unsigned long int a) {
unsigned long int nlb;
bool z;
asm (
"BSR %[a], %[nlb]"
: [nlb] "=r" (nlb), "=@ccz"(z)
: [a] "mr" (a)
);
unsigned long int one;
if (!z)
one = 1;
else
one = 0;
return one << nlb;
}
“=@ccz”约束在下面的文档中有描述。基本上它只是说“这个变量的值取自Z(C)条件(C)ode。”
编辑:看到Peter发表评论我并不感到惊讶,但我很惊讶他没有发表自己的备选方案 以下是我将他的评论翻译成代码的尝试。这可能比OP感兴趣的更复杂,但为了完整性:
unsigned long int LeadingBit(unsigned long int a) {
unsigned long int bit;
unsigned long int res;
asm (
"BSR %[a], %[bit] \n\t" // sets both (bit) and ZF
"BTS %[bit], %[res] \n\t" // sets the corresponding bit in (res) and
// carry flag, but *doesn't* change ZF
"CMOVZ %[zero], %[res]" // reset res to 0 if BSR detected a==0
: [bit] "=&r" (bit), [res] "=&r"(res)
: [a] "mr" (a), [zero] "r"(0UL), "[res]"(0UL)
: "cc"
);
return res;
}
虽然BSR/BTS/CMOVZ非常直截了当,但约束的废话可能是代码维护人员难以解决的问题
所以,来解释发生了什么
将保留返回值。“=&r”(res)
用于指示它不能与任何其他参数共享寄存器。请澄清,仅因为您将约束声明为“=r”,这并不意味着您将获得一个唯一的寄存器,除非您使用&
。通过减少使用的寄存器数量,这可能是一件好事,但在这种情况下不会太多。如果编译器决定对%[zero]和%[res]使用相同的寄存器,这将导致CMOVZ失败&
说,在进入asm时,%[res]使用的任何寄存器都应该初始化为零。是的,我们可以在asm中这样做,但是通过将其放入C代码中,它允许编译器以任何可能对周围代码最有效的方式来执行此操作。见鬼,它可能已经有了零“[res]”(0UL)
// See the linked answer for the ICC/MSVC part of this #ifdef __GNUC__ #ifdef __clang__ static inline unsigned BSR64(uint64_t x) { return 63-__builtin_clzll(x); // gcc/ICC can't optimize this back to just BSR, but clang can and doesn't provide alternate intrinsics // update: clang8.0 regressed here but still doesn't do __builtin_ia32_bsrdi } #else #define BSR64 __builtin_ia32_bsrdi #endif #include <x86intrin.h> #define BSR32(x) _bit_scan_reverse(x) #endif uint64_t LeadingBitIsolate_gnuc_v2(uint64_t a) { uint64_t bit = BSR64(a); return a ? (1ULL << bit) : 0; }
# gcc -O3 -mtune=skylake LeadingBitIsolate_gnuc_v2(unsigned long): movq %rdi, %rax testq %rdi, %rdi je .L9 bsrq %rdi, %rcx movl $1, %eax salq %cl, %rax # missed optimization: xor-zero + bts would be much better especially with tune=skylake .L9: ret
#include <stdint.h> #include <limits.h> uint64_t LeadingBitIsolate_asm(uint64_t a) { uint64_t bit; uint64_t bts_target = 0; asm ( "BSR %[a], %[bit] \n\t" // ZF = (a==0) "BTS %[bit], %[res] \n\t" // sets CF = old bit = 0 but not ZF // possible partial-flag stall on some CPUs for reading ZF after writing CF // but not SnB-family "CMOVZ %[a], %[res]" // res = a (= 0) if BSR detected a==0 : [bit] "=r" (bit), [res] "+r"(bts_target) // no early clobber: input read before either output written : [a] "r" (a) : "cc" // redundant but not harmful on x86: flags clobber is always implied ); return bts_target; }
gcc9.2 -O3 -mtune=skylake LeadingBitIsolate_asm(unsigned long): xorl %eax, %eax BSR %rdi, %rdi BTS %rdi, %rax CMOVZ %rdi, %rax ret
// depends on BSR behaviour that only AMD documents, but all real CPUs implement (for now?) // it seems unlikely that this will change, though. // The main advantage of this version is on P6-family and early Sandybridge // where it definitely does work safely. uint64_t LeadingBitIsolate_asm_p6(uint64_t a) { uint64_t btc_target; uint64_t bit = 0; //bool input_was_zero; asm ( "xor %k[res], %k[res]\n\t" // make sure we avoid P6-family partial-reg stalls with setz + reading full reg by forcing xor-zeroing, not MOV "bsr %[a], %[bit] \n\t" // bit=count or unmodified (if a==0) "setz %b[res]\n\t" // res = (a==0) "btc %[bit], %[res] \n\t" // flip a bit. For a==0, flips bit 0 which SETZ set to 1 : [bit] "+r" (bit), [res] "=&q"(btc_target) // q = reg accessible as low-8, like RAX has AL. Any x86-64 reg // early-clobber: [res] is written before a, but [bit] isn't. // ,"=@ccz" (input_was_zero) // optional GCC6 flag output. Or "=@ccc" to read from CF (btc result) and avoid partial-flag stalls : [a] "r" (a) : "cc" ); return btc_target; //return input_was_zero; }
gcc8.3 -O3 -mtune=nehalem LeadingBitIsolate_asm_p6(unsigned long): xorl %edx, %edx # compiler-generated xor %eax, %eax # inline asm bsr %rdi, %rdx setz %al btc %rdx, %rax ret
# starting: rdx=rax=0 bsr %rdi, %rdx # leaves RDX=0 (unmodified) and sets ZF setz %al # sets RAX=1 btc %rdx, %rax # flips bit 0 of RAX, changing it back to 0 # rax = 0 (result), rdx = 0 (bitpos)