将整数限制为 0-255 并加倍为 0.0-1.0 的技巧?
Posted
技术标签:
【中文标题】将整数限制为 0-255 并加倍为 0.0-1.0 的技巧?【英文标题】:Hacks for clamping integer to 0-255 and doubles to 0.0-1.0? 【发布时间】:2015-10-16 08:20:50 【问题描述】:是否有任何无分支或类似的技巧可以将整数限制在 0 到 255 的区间内,或者将整数限制在 0.0 到 1.0 的区间内? (这两个范围都是封闭的,即端点都包含在内。)
我正在使用明显的最小-最大检查:
int value = (value < 0? 0 : value > 255? 255 : value);
但是有没有办法让这个更快——类似于“模”钳value & 255
?有没有办法用浮点做类似的事情?
我正在寻找一种便携式解决方案,所以最好不要使用特定于 CPU/GPU 的东西。
【问题讨论】:
如果您使用value = min (value, 255)
,您的编译器可以为您完成工作,尤其是当硬件包含整数MIN
操作时。 min/max 的无分支序列是众所周知的,并且经常被合并到编译器中。
【参考方案1】:
这是我用来将 int 限制在 0 到 255 范围内的技巧:
/**
* Clamps the input to a 0 to 255 range.
* @param v any int value
* @return @code v < 0 ? 0 : v > 255 ? 255 : v
*/
public static int clampTo8Bit(int v)
// if out of range
if ((v & ~0xFF) != 0)
// invert sign bit, shift to fill, then mask (generates 0 or 255)
v = ((~v) >> 31) & 0xFF;
return v;
它仍然有一个分支,但一个方便的事情是,您可以通过 ORing 一起测试多个 int 中的任何一个是否超出范围,这在所有它们都是的常见情况下使事情变得更快在范围内。例如:
/** Packs four 8-bit values into a 32-bit value, with clamping. */
public static int ARGBclamped(int a, int r, int g, int b)
if (((a | r | g | b) & ~0xFF) != 0)
a = clampTo8Bit(a);
r = clampTo8Bit(r);
g = clampTo8Bit(g);
b = clampTo8Bit(b);
return (a << 24) + (r << 16) + (g << 8) + (b << 0);
【讨论】:
不错!尤其是组合的 OR hack。事实上,处理 RGB 组件是这个问题的起点。 一些快速性能测试表明,如果 50% 的随机输入超出 0-255 范围,这比我的方法快 4 倍(Java 1.6)。我的测试表明,如果更多的输入位于钳制范围内,它会变得更快(高达 12 倍!)——我原以为由于更好的分支预测,差异会变得不那么显着,但这可能只是一个假象我草率的性能测试。 @FranzD。我个人发现该技术的优势非常微不足道,但它的相对优势当然取决于首先生成要钳位的值涉及多少计算。 当然——我的性能测试只是测量了夹紧速度本身,它只是为了快速进行第一次检查。您需要在生产代码中对此进行分析才能看到真正的区别。 有没有办法给这个任意的最大值,比如 45 或 79?【参考方案2】:请注意,如果您编写value = min (value, 255)
,您的编译器可能已经为您提供了您想要的东西。如果存在,这可能会被转换为 MIN
指令,或者转换为比较后的条件移动,例如 x86 上的 CMOVcc
指令。
以下代码假定整数的二进制补码表示,这通常是今天给定的。从布尔到整数的转换不应该涉及底层分支,因为现代架构要么提供可直接用于形成掩码的指令(例如 x86 上的 SETcc
和 NVIDIA GPU 上的 ISETcc
),要么可以应用谓词或有条件的动作。如果缺少所有这些,编译器可能会根据 Boann 的回答发出基于算术右移的无分支指令序列以构造掩码。但是,编译器可能会做错事情,因此存在一些残余风险,因此如果有疑问,最好将生成的二进制文件反汇编以进行检查。
int value, mask;
mask = 0 - (value > 255); // mask = all 1s if value > 255, all 0s otherwise
value = (255 & mask) | (value & ~mask);
在许多架构上,使用三元运算符?:
也可能导致无分支指令序列。硬件可能支持选择类型指令,这些指令本质上是三元运算符的硬件等价物,例如 NVIDIA GPU 上的ICMP
。或者它提供了 x86 中的CMOV
(条件移动),或者 ARM 上的谓词,这两者都可以用于实现三元运算符的无分支代码。与前一种情况一样,需要检查反汇编的二进制代码以绝对确定生成的代码没有分支。
int value;
value = (value > 255) ? 255 : value;
对于浮点操作数,现代浮点单元通常提供FMIN
和FMAX
指令,这些指令直接映射到C/C++ 标准数学函数fmin()
和fmax()
。或者,fmin()
和fmax()
可以转换为比较,然后是条件移动。同样,谨慎的做法是检查生成的代码以确保它是无分支的。
double value;
value = fmax (fmin (value, 1.0), 0.0);
【讨论】:
关系表达式到整数的转换是否涉及条件分支? @PatriciaShanahan 好点。我想有一些风险是受编译器的支配。在最常见的架构上不应涉及分支,例如可以通过谓词形成掩码的 ARM,以及具有SETcc
的 x86。据我所知,PowerPC 的编译器也发出无分支序列。 NVIDIA GPU 有一个ISETcc
指令,它直接返回掩码作为比较的结果。我将更新答案,指出编译器存在残留风险。【参考方案3】:
我用这个东西,100% 无分支。
int clampU8(int val)
val &= (val<0)-1; // clamp < 0
val |= -(val>255); // clamp > 255
return val & 0xFF; // mask out
【讨论】:
非常整洁 :) 虽然无分支可能取决于编译器和系统。【参考方案4】:对于那些使用 C#、Kotlin 或 Java 的人来说,这是我能做的最好的,虽然有点神秘,但它很好而且简洁:
(x & ~(x >> 31) | 255 - x >> 31) & 255
它仅适用于有符号整数,因此可能会成为某些人的障碍。
【讨论】:
谢谢Jean,非常棒的第一次贡献:) 我愚蠢的大脑很难完全理解它,但我看到一个巧妙地利用了 0 和 255 只是分开的事实(模块 256) .以前没有考虑过,但正如我所说——我的大脑很愚蠢。 (我可以这么说,我们住在一起。) @FranzD。如果您有兴趣,我在github.com/jdarc/branchless 创建了一个小型基准测试项目,它使用 Kotlin,但理论上 VM 应该能够发挥魔力并找出最佳指令。有趣的是,minmax 版本的性能与上面的一个衬里一样好,也许它使用了某种内在函数? 不错 :) 是的,minmax() 的性能令人惊讶。它一定是某种编译器魔法。这再次表明,善良的老 Knuth 对他的万恶之源是正确的 - 只需尽可能以最愚蠢的方式为编译器提供最佳优化机会。尽管如此,我还是很感兴趣 minmax() 与公认解决方案的那个不错的 OR 技巧相比如何。【参考方案5】:对于钳位双打,恐怕没有语言/平台无关的解决方案。
浮点的问题是,它们可以选择从最快的操作 (MSVC /fp:fast
, gcc -funsafe-math-optimizations
) 到完全精确和安全的 (MSVC /fp:strict
, gcc -frounding-math -fsignaling-nans
)。在完全精确模式下,编译器不会尝试使用任何位黑客,即使他们可以。
处理double
位的解决方案不能移植。可能有不同的字节顺序,也可能没有(有效的)方法来获取double
位,double
毕竟不一定是 IEEE 754 binary64。此外,直接操作不会在预期的情况下产生用于向 NAN 发送信号的信号。
对于整数,编译器很可能无论如何都会正确处理,否则已经给出了很好的答案。
【讨论】:
以上是关于将整数限制为 0-255 并加倍为 0.0-1.0 的技巧?的主要内容,如果未能解决你的问题,请参考以下文章
在RGB到HSV的转换中,V=max(R,G,B),V不是RGB中的最大值吗?范围应该是在0~255之间,怎么会在0~1之间?
ARM NEON Intrinsics:将向量的值限制为 0-255
使用 RGB 数据将输入数据剪切到 imshow 的有效范围(浮点数为 [0..1] 或整数为 [0..255])
SSE 内在函数:将 32 位浮点数转换为 UNSIGNED 8 位整数