如何高效地将0/1转换为符号?

3

请看下面的函数,其中a是一个无符号字节0-255,b是一个浮点数:

def convert(a, b):
    if a & 0x80:
        return -b
    return b

如果a的第一位被设置,它将否定b,否则不会发生任何事情。有人可能认为这并不那么酷,因为条件语句会破坏CPU中的分支预测。因此,人们会尝试将其转换为计算。
但是我只找到了这个解决方案,它看起来不太高效:
def convert(a, b):
    return (-1)**(a & 0x80) * b

哪种更有效?编译器是否简化了第二个?有更好的方法吗?

你对你的代码进行基准测试了吗? - MisterMiyagi
你将这个函数应用在数组上吗? - max9111
@max9111 最初这不是一个函数。我只是将其隔离出来。我看到一段代码使用指数运算将GW-Basic浮点数转换为现代浮点数,我想知道这是否可行。这是关于它们的文档:https://www-user.tu-chemnitz.de/~heha/viewchm.php/hs/gwbasic.chm/tokens.html 但是在此期间,我已经找到了一个相对最优的实现方法,通过使用分支。 - Crouching Kitten
1
如果性能很重要,您可以使用Numba或Cython。如果您在数组上运行此类算法(避免函数调用开销),则可以获得2到3个数量级的加速。这里有一些像分支预测这样的事情是很重要的。 - max9111
2个回答

6
这是Python。在你可能想到的意义上,这里没有编译器。假设你使用CPython(参考解释器),所有都通过一个巨大的开关语句循环来运行和解释每个字节码。你对分支预测的担忧在这里是无关紧要的;在每个操作中,将有半打CPU级别的分支,在switch、类型检查、动态函数指针查找和调用等之间。当它读取数百字节的字节码而不是下一个字节码时,远程跳转可能会对数据缓存造成一些损害,但分支预测(或其缺乏)并不是问题(100%可预测的跳转也会有相同的问题)。
基本上,凡是在C中可以工作并且可以通过编译器的优化器得到优化代码的东西,在CPython中都行不通。所以不要费心了。编写完整的代码,如果太慢,进行剖析,然后优化“最热门”(最常调用的)部分。你现在从事的是过早的优化,真的应该停下来。
如果我是你,我会选择选项#1(可能用if a >= 0x80:替换if a & 0x80:,因为前者需要返回一个int,然后更昂贵地进行真值测试,而后者直接返回bool,这是最便宜的真值测试),因为它很简单,不太可能很糟糕;只有在程序运行太慢,并且剖析显示出这个特定的代码是热点时,才去调查其他选项。

2
顺便提一下,我确实进行了本地微基准测试。当 a 小于 128 时,选项 #1 和 #2 基本相同,但是当 a 大于或等于 128 时,#2 的速度要慢得多(运行时间长约 3 倍),这是有道理的;Python 没有针对 -1 ** x 的显式优化,因此最终会执行几个无意义的 -11 相乘来计算 -1 ** 128。将选项 #1 中的 a&0x80 替换为 a> = 0x80 甚至更快(运行时间降低了 20-40%,具体取决于是否需要否定)。 - ShadowRanger

2

(-1)**(a & 0x80)计算幂,所以效率非常低。实际上,你可以用指数为a & 0x801 if exponent & 1 == 0 else -1来替换它。但更容易从0或1中获得1和-1,只需执行x*2 - 1

一些非分支版本

return (((a >> 7) << 1) - 1)*b;
return (((a >> 6) & 0x02) - 1)*b;
return math.copysign(b, -(a >> 7));
return math.copysign(b, -(a & 0x80));

我曾考虑过包含一个 math.copysign 的例子,但最终决定不这样做,因为 math.copysign 强制将输入转换为 float 并返回 float;即使你将其强制转换回 int,如果 b 是一个 53 位或更大的 int(超过该限制,float 无法表示每个 int 值),你也可能会丢失数据。 - ShadowRanger
@ShadowRanger 但前提是 b 是浮点数,所以不用担心。 - phuclv
现在我感觉自己像个白痴,完全错过了那个。哎呀! :-) - ShadowRanger
感谢提供创意示例! - Crouching Kitten

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接