n是负数,正数还是零?返回1,2或4

时间:2012-03-04 20:33:40

标签: c++ bit-manipulation bit bit-shift

我正在构建一个PowerPC解释器,它运行得很好。在Power架构中,条件寄存器CR0(x86上的EFLAGS)几乎在任何指令上都会更新。它是这样设置的。如果最后一个结果为负,则CR0的值为1,如果最后一个结果为正,则为2,否则为4。

我的第一个解释这个的天真方法是:

if (n < 0)
    cr0 = 1
else if (n > 0)
    cr0 = 2;
else
    cr0 = 4;

但是我知道所有这些分支都不是最佳的,每秒运行数百万次。我已经看到了一些有点黑客攻击,但似乎没有任何东西。例如,我发现许多例子将数字转换为-1,0或1,相应地符号为0.但是如何使-1 = 1,1 = 2 = 0? 我正在寻求Bit Hackers的帮助......

提前致谢

更新 首先:谢谢你们,你们一直很棒。为了提高速度,我会仔细测试你的所有代码,你将是第一个知道谁是胜利者的代码。

@jalf:关于你的第一个建议,我实际上并没有在每条指令上计算CR0。我宁愿保留一个lastResult变量,当(和如果)以下指令要求标志时,进行比较。三个主要动机让我回到“每次”更新:

  1. 在PPC上,您不必像在x86上那样更新CR0(其中ADD总是更改EFLAGS,即使不需要),您有两种ADD,一种是更新。如果编译器选择使用更新版本,则意味着它将在某个时刻使用CR0,所以没有必要推迟...
  2. 有一个特别痛苦的指令叫做mtcrf,它可以让你随意改变CR0。你甚至可以把它设置为7,没有算术意义......这只会破坏保留“lastResult”变量的可能性。

8 个答案:

答案 0 :(得分:33)

首先,如果要在(几乎)每条指令之后更新此变量,那么显而易见的建议就是:

仅在后续指令需要其值时才更新它。在任何其他时间,更新它都没有意义。

但无论如何,当我们更新它时,我们想要的是这种行为:

R < 0  => CR0 == 0b001 
R > 0  => CR0 == 0b010
R == 0 => CR0 == 0b100

理想情况下,我们根本不需要分支。这是一种可能的方法:

  1. 将CR0设置为值1。 (如果你真的想要速度,请调查是否可以在不从内存中获取常量的情况下完成此操作。即使您必须在其上花费一些指令,也可能是值得的。)
  2. 如果R> = 0,则左移一位。
  3. 如果R == 0,则左移一位
  4. 可以转换步骤2和3以消除“if”部分

    CR0 <<= (R >= 0);
    CR0 <<= (R == 0);
    

    这更快吗?我不知道。与往常一样,当您关注绩效时,您需要进行衡量,衡量和衡量。

    但是,我可以看到这种方法的一些优点:

    1. 我们完全避免分支
    2. 我们避免内存加载/存储。
    3. 我们依赖的指令(位移和比较)应具有低延迟,例如,乘法并非总是如此。
    4. 缺点是我们在所有三条线之间都有一个依赖链:每条线修改CR0,然后在下一行中使用。这在一定程度上限制了指令级并行性。

      为了最大限度地减少这种依赖关系链,我们可以做这样的事情:

      CR0 <<= ((R >= 0) + (R == 0));
      

      因此我们只需在初始化后修改CR0一次。

      或者,在一行中完成所有事情:

      CR0 = 1 << ((R >= 0) + (R == 0));
      

      当然,这个主题有很多可能的变化,所以继续进行实验。

答案 1 :(得分:20)

很多答案大概都是“不”,像往常一样:)你想要点破解?你会得到的。然后随意使用它,因为认为合适。

您可以将该映射用于-1,0和1(sign),然后执行此操作:

return 7 & (0x241 >> ((sign(x) + 1) * 4));

本质上使用的是一个很小的查找表。

或“天真的傻瓜”:

int y = ((x >> 31) & 1) | ((-x >> 31) & 2)
return (~(-y >> 31) & 4) | y;

第一行将x < 0映射到1,将x > 0映射到2,将x == 0映射到0.第二行然后将y == 0映射到4和y != 0到收率


当然,对于x = 0x80000000,它有一个偷偷摸摸的边缘情况,映射到3.糟糕。好吧,让我们解决一下:

int y = ((x >> 31) & 1) | ((-x >> 31) & 2)
y &= 1 | ~(y << 1);  // remove the 2 if odd
return (~(-y >> 31) & 4) | y;

答案 2 :(得分:6)

以下表达式有点神秘,但并不过分,它看起来像编译器可以很容易地优化:

cr0 = 4 >> ((2 * (n < 0)) + (n > 0));

以下是x86目标的GCC 4.6.1将其编译为-O2

xor ecx, ecx
mov eax, edx
sar eax, 31
and eax, 2
test    edx, edx
setg    cl
add ecx, eax
mov eax, 4
sar eax, cl

VC 2010与/Ox看起来很相似:

xor ecx, ecx
test eax, eax
sets cl
xor edx, edx
test eax, eax
setg dl
mov eax, 4
lea ecx, DWORD PTR [edx+ecx*2]
sar eax, cl

使用if测试的版本编译为使用与这些编译器中的任何一个进行跳转的程序集。当然,除非你真正检查输出,否则你永远不会确定任何特定的编译器会对你选择的任何特定代码做什么。我的表达足够神秘,除非它真的是一个性能关键的代码,否则我仍然可以使用if语句版本。由于您需要经常设置CR0寄存器,我认为值得测量这个表达式是否有用。

答案 3 :(得分:4)

没有优化的gcc

        movl    %eax, 24(%esp)  ; eax has result of reading n
        cmpl    $0, 24(%esp)
        jns     .L2
        movl    $1, 28(%esp)
        jmp     .L3
.L2:
        cmpl    $0, 24(%esp)
        jle     .L4
        movl    $2, 28(%esp)
        jmp     .L3
.L4:
        movl    $4, 28(%esp)
.L3:

使用-O2:

        movl    $1, %edx       ; edx = 1
        cmpl    $0, %eax
        jl      .L2            ; n < 0
        cmpl    $1, %eax       ; n < 1
        sbbl    %edx, %edx     ; edx = 0 or -1
        andl    $2, %edx       ; now 0 or 2
        addl    $2, %edx       ; now 2 or 4
.L2:
        movl    %edx, 4(%esp)

我认为你不太可能做得更好

答案 4 :(得分:4)

当我的电脑崩溃时,我正在研究这个。

int cr0 = (-(n | n-1) >> 31) & 6;
cr0 |= (n >> 31) & 5;
cr0 ^= 4;

以下是生成的程序集(适用于Intel x86):

PUBLIC  ?tricky@@YAHH@Z                                 ; tricky
; Function compile flags: /Ogtpy
_TEXT   SEGMENT
_n$ = 8                                                 ; size = 4
?tricky@@YAHH@Z PROC                                    ; tricky
; Line 18
        mov     ecx, DWORD PTR _n$[esp-4]
        lea     eax, DWORD PTR [ecx-1]
        or      eax, ecx
        neg     eax
        sar     eax, 31                                 ; 0000001fH
; Line 19
        sar     ecx, 31                                 ; 0000001fH
        and     eax, 6
        and     ecx, 5
        or      eax, ecx
; Line 20
        xor     eax, 4
; Line 22
        ret     0
?tricky@@YAHH@Z ENDP                                    ; tricky

完整的详尽测试,也非常适合基准测试:

#include <limits.h>

int direct(int n)
{
    int cr0;
    if (n < 0)
        cr0 = 1;
    else if (n > 0)
        cr0 = 2;
    else
        cr0 = 4;
    return cr0;
}

const int shift_count = sizeof(int) * CHAR_BIT - 1;
int tricky(int n)
{
    int cr0 = (-(n | n-1) >> shift_count) & 6;
    cr0 |= (n >> shift_count) & 5;
    cr0 ^= 4;
    return cr0;
}

#include <iostream>
#include <iomanip>
int main(void)
{
    int i = 0;
    do {
        if (direct(i) != tricky(i)) {
            std::cerr << std::hex << i << std::endl;
            return i;
        }
    } while (++i);
    return 0;
}

答案 5 :(得分:1)

如果有更快的方法,编译器可能已经在使用它了。

保持代码简洁明了;这使得优化器最有效。

简单直接的解决方案出乎意料地速度快得多:

cr0 = n? (n < 0)? 1: 2: 4;

x86汇编(由VC ++ 2010制作,标记/Ox):

PUBLIC  ?tricky@@YAHH@Z                                 ; tricky
; Function compile flags: /Ogtpy
_TEXT   SEGMENT
_n$ = 8                                                 ; size = 4
?tricky@@YAHH@Z PROC                                    ; tricky
; Line 26
        mov     eax, DWORD PTR _n$[esp-4]
        test    eax, eax
        je      SHORT $LN3@tricky
        xor     ecx, ecx
        test    eax, eax
        setns   cl
        lea     eax, DWORD PTR [ecx+1]
; Line 31
        ret     0
$LN3@tricky:
; Line 26
        mov     eax, 4
; Line 31
        ret     0
?tricky@@YAHH@Z ENDP                                    ; tricky

答案 6 :(得分:1)

对于完全不可移植的方法,我想知道这是否有任何速度优势:

void func(signed n, signed& cr0) {
    cr0 = 1 << (!(unsigned(n)>>31)+(n==0));
}

mov         ecx,eax  ;with MSVC10, all optimizations except inlining on.
shr         ecx,1Fh  
not         ecx  
and         ecx,1  
xor         edx,edx  
test        eax,eax  
sete        dl  
mov         eax,1  
add         ecx,edx  
shl         eax,cl  
mov         ecx,dword ptr [cr0]  
mov         dword ptr [ecx],eax  

与我机器上的代码进行比较:

test        eax,eax            ; if (n < 0)
jns         func+0Bh (401B1Bh)  
mov         dword ptr [ecx],1  ; cr0 = 1;
ret                            ; cr0 = 2; else cr0 = 4; }
xor         edx,edx            ; else if (n > 0)
test        eax,eax  
setle       dl  
lea         edx,[edx+edx+2]  
mov         dword ptr [ecx],edx ; cr0 = 2; else cr0 = 4; }
ret  

我对装配一点都不太了解,所以我不能确定这是否有任何好处(或者即使我有任何跳跃。我看不到任何以j开头的说明)。一如既往,(和其他人一样说的一百万次)简介。

我怀疑这比Jalf或Ben的更快,但我没有看到任何利用x86上所有负数都设置了一定数量的事实,我想我会抛出一个。< / p>

[编辑] BenVoigt建议cr0 = 4 >> ((n != 0) + (unsigned(n) >> 31));删除逻辑否定,我的测试表明这是巨大的改进。

答案 7 :(得分:-1)

以下是我的尝试。

int cro = 4 >> (((n > 0) - (n < 0)) % 3 + (n < 0)*3);