n 是负数、正数还是零?返回 1、2 或 4
n is negative, positive or zero? return 1, 2, or 4
我正在构建一个PowerPC解释器,它运行良好。在电源架构中,条件寄存器 CR0(x86 上的 EFLAGS(几乎在任何指令上都会更新。它是这样设置的。如果最后一个结果为负数,则 CR0 的值为 1,如果最后一个结果为正,则为 2,否则为 4。
我解释这一点的第一个天真方法是:
if (n < 0)
cr0 = 1
else if (n > 0)
cr0 = 2;
else
cr0 = 4;
但是,我知道所有这些分支都不是最佳的,每秒运行数百万次。我已经看到一些关于SO的黑客攻击,但似乎都不够。例如,我找到了很多例子,将数字转换为 -1、0 或 1,对应于符号或 0。但是如何使-1 = 1,1 = 2,0 = 4?我正在寻求比特黑客的帮助...
提前致谢
更新:首先:谢谢大家,你们很棒。我会仔细测试你所有代码的速度,你将是第一个知道谁是赢家的人。
@jalf:关于你的第一个建议,我实际上并没有在每条指令上计算CR0。我宁愿保留一个 lastResult 变量,当(如果(以下指令要求一个标志时,进行比较。三个主要动机让我回到了"每次"更新:
- 在 PPC 上,您不会像在 x86 上那样被迫更新 CR0(其中 ADD 总是更改 EFLAGS,即使不需要(,您有两种风格的 ADD,一种正在更新。如果编译器选择使用更新的,则意味着它将在某个时候使用 CR0,因此延迟没有意义......
- 有一个特别痛苦的指令叫做mtcrf,它使你能够任意更改CR0。您甚至可以将其设置为 7,没有算术意义......这只会破坏保留"lastResult"变量的可能性。
首先,如果要在(几乎(每条指令之后更新此变量,则明显的建议是:
不要
仅当后续指令需要其值时才更新它。在任何其他时间,更新它都没有意义。
但无论如何,当我们更新它时,我们想要的是这种行为:
R < 0 => CR0 == 0b001
R > 0 => CR0 == 0b010
R == 0 => CR0 == 0b100
理想情况下,我们根本不需要分支。这是一种可能的方法:
- 将 CR0 设置为值
1
。(如果您确实想要速度,请调查是否可以在不从内存中获取常量的情况下完成此操作。即使你必须花一些说明,也可能是值得的( - 如果 R>= 0,则左移一位。
- 如果 R == 0,则左移一位
步骤2和3可以转换以消除"if"部分
CR0 <<= (R >= 0);
CR0 <<= (R == 0);
这样更快吗?我不知道。与往常一样,当您关注性能时,您需要衡量,衡量,再衡量。
但是,我可以看到这种方法的几个优点:
- 我们完全避免分支机构
- 我们避免内存加载/存储。
- 我们依赖的指令(位移和比较(应该具有低延迟,例如,乘法并不总是如此。
缺点是我们在所有三行之间都有一个依赖链:每行修改 CR0,然后在下一行中使用。这在一定程度上限制了指令级并行性。
为了最小化这个依赖链,我们可以做这样的事情:
CR0 <<= ((R >= 0) + (R == 0));
所以我们只需要在初始化 CR0 后修改一次。
或者,在一行中完成所有操作:
CR0 = 1 << ((R >= 0) + (R == 0));
当然,这个主题有很多可能的变体,所以继续尝试吧。
像往常一样,很多答案已经"没有"了:)你想要位黑客?你会得到的。然后随意使用它,只要你认为合适。
您可以使用该映射到 -1、0 和 1 ( sign
(,然后执行以下操作:
return 7 & (0x241 >> ((sign(x) + 1) * 4));
这本质上是使用一个微小的查找表。
或者"天真的bithack":
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
映射到 y。
当然,对于 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;
下面的表达式有点晦涩难懂,但并不过分,它看起来是编译器可以轻松优化的东西:
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
带有/Ox
的VC 2010看起来非常相似:
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 寄存器,我认为可能值得衡量此表达式是否有帮助。
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)
我认为你不太可能做得更好
当我的电脑崩溃时,我正在研究这个。
int cr0 = (-(n | n-1) >> 31) & 6;
cr0 |= (n >> 31) & 5;
cr0 ^= 4;
下面是生成的程序集(适用于英特尔 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;
}
如果有更快的方法,编译器可能已经在使用它。
保持代码简短;这使得优化器最有效。
简单直接的解决方案在速度方面做得非常好:
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
对于完全不便携的方法,我想知道这是否可能有任何速度优势:
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 上所有负数都设置了某个位的事实,我想我会扔掉一个。
BenVoigt建议cr0 = 4 >> ((n != 0) + (unsigned(n) >> 31));
消除逻辑否定,我的测试表明这是一个巨大的改进。
以下是我的尝试。
int cro = 4 >> (((n > 0) - (n < 0)) % 3 + (n < 0)*3);
- 来自 std::list 的迭代器 .end() 按预期返回"0xcdcdcdcdcdcdcdcd"但 .begin()
- 什么时候在C++中返回常量引用是个好主意
- 你能重载对象变量名本身返回的内容吗
- 为什么 Serial.println(<char[]>);返回随机字符?
- C++映射:具有自定义类的运算符[]不起作用(总是返回0)
- 如何获取std::result_of函数的返回类型
- QueryWorkingSet总是返回false
- (C++)分析树以计算返回错误值的简单算术表达式
- 访问者访问变体并返回不同类型时出错
- 如何返回一个类的两个对象相加的结果
- OpenInventor从9.8升级到10.4.2后,GLSL纹理返回零
- lower_bound()返回最后一个元素
- "throw expression code" 1e7 >返回 d 是什么?投掷标准::overflow_error( "too big" ) : d;意味 着?
- 奇怪的(对我来说)返回声明 - 在谷歌上找不到任何关于它的信息
- 如何取消对nullptr的屏蔽,返回正确的对象
- 奇怪的结构&GCC&clang(void*返回类型)
- 架构决策:返回std::future还是提供回调
- 从python中调用C++函数并获取返回值
- 矩阵向量乘法(cublasDgemv)返回零
- 为什么模板类中的对象不能返回值