将整数限制为 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 &amp; 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;

对于浮点操作数,现代浮点单元通常提供FMINFMAX 指令,这些指令直接映射到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 位整数

华为OD机试真题Python实现IPv4 地址转换成整数真题+解题思路+代码(2022&2023)

python 将rgb值转换为Maya标志的0.0-1.0值