将整数限制为 0-255 并加倍到 0.0-1.0 的技巧?

Hacks for clamping integer to 0-255 and doubles to 0.0-1.0?

是否有任何无分支或类似的技巧可以将整数限制在 0 到 255 的区间内,或者将整数限制在 0.0 到 1.0 的区间内? (两个范围都是封闭的,即包括端点。)

我正在使用明显的最小值-最大值检查:

int value = (value < 0? 0 : value > 255? 255 : value);

但是有没有办法让它更快——类似于 "modulo" 钳位 value & 255?有没有办法用浮点数做类似的事情?

我正在寻找便携式解决方案,所以最好不要 CPU/GPU-specific 东西。

这是我用来将 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;
}

它仍然有一个分支,但它的一个方便之处在于,您可以通过对它们进行 OR 运算来一次测试多个整数中的任何一个是否超出范围,这使得在所有的常见情况下速度更快他们在范围内。例如:

/** 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);
}

请注意,如果您编写 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);

在许多体系结构中,使用三元运算符 ?: 也可以产生无分支指令序列。硬件可能支持 select 类型的指令,这些指令本质上是三元运算符的硬件等价物,例如 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);

我用这个东西,100%无分支。

int clampU8(int val)
{
    val &= (val<0)-1;  // clamp < 0
    val |= -(val>255); // clamp > 255
    return val & 0xFF; // mask out
}

对于那些使用 C#、Kotlin 或 Java 的人来说,这是我能做的最好的,虽然有点神秘,但它很好而且简洁:

(x & ~(x >> 31) | 255 - x >> 31) & 255

它只适用于有符号整数,因此对某些人来说可能是个阻碍。

对于夹紧双打,恐怕没有 language/platform 不可知的解决方案。

浮点数的问题,他们有从最快的操作(MSVC /fp:fast、gcc -funsafe-math-optimizations)到完全精确和安全(MSVC /fp:strict、gcc [=13=)的选项]).在完全精确模式下,编译器不会尝试使用任何位黑客,即使他们可以。

操作 double 位的解决方案不可移植。可能有不同的字节顺序,也可能没有(有效的)方法来获取 double 位,毕竟 double 不一定是 IEEE 754 binary64。此外,直接操作不会在预期的情况下产生用于发送 NAN 信号的信号。


对于整数,编译器很可能无论如何都会正确处理,否则已经给出了很好的答案。