【问题标题】:Clamping short to unsigned char钳制短路到无符号字符
【发布时间】:2011-05-04 12:27:53
【问题描述】:

我有一个简单的C函数如下:

unsigned char clamp(short value){
    if (value < 0) return 0;
    if (value > 0xff) return 0xff;
    return value;
}

是否可以在不使用任何 if / else 分支的情况下重写它,同时提高效率?

编辑:

我基本上希望看看是否可以进行一些基于位算术的钳位实现。目标是在 GPU(图形处理单元)上处理图像。这种类型的代码将在每个像素上运行。我想如果可以避免分支,那么 GPU 的整体吞吐量会更高。

像 (value 255) ? 255 : value) ) 这样的解决方案只是使用语法糖对 if/else 分支进行重新散列。所以我不是在寻找它。

编辑 2:

如果如下,我可以将其缩减为单个,但我无法更好地思考:

unsigned char clamp(short value){
    int more = value >> 8;
    if(more){
        int sign = !(more >> 7);
        return sign * 0xff;
    }
    return value;
}

编辑 3:

刚刚在 FFmpeg 代码中看到了一个非常好的实现:

/**
 * Clip a signed integer value into the 0-255 range.
 * @param a value to clip
 * @return clipped value
 */
static av_always_inline av_const uint8_t av_clip_uint8_c(int a)
{
    if (a&(~0xFF)) return (-a)>>31;
    else           return a;
}

这当然有效,如果很好的话,它可以减少到一个。

【问题讨论】:

  • 只是对术语的快速说明,我相信这通常被称为钳位,而不是剪裁,如果它与计算相关联,它通常被称为“饱和算术”,如果你在谷歌搜索。
  • 您为什么要这样做?这看起来已经很有效了。
  • GPU 上有abs() 吗?
  • @roe,我已经更新了术语。谢谢。
  • 不确定这是否是您的意图,但 FFmpeg 的最后一个示例看起来像是进行了换行。所以 256 变成 -1

标签: c


【解决方案1】:

您写道,您希望避免在 GPU 上进行分支。确实,在并行环境中分支可能非常昂贵,因为要么必须评估两个分支,要么必须应用同步。但是如果分支足够小,代码将比大多数算术更快。 CUDA C best practices guide 描述了原因:

有时,编译器可能 [..] 优化 if 或 switch 语句 通过使用分支谓词代替。 在这些情况下,任何扭曲都不能 发散。 [..]

当使用分支谓词时,没有 执行的指令 取决于控制条件 被跳过。相反,他们每个人都是 与每个线程条件相关联 设置为 true 的代码或谓词 或假的基于控制 条件,虽然这些中的每一个 指令被安排在 执行,只有指令 一个真正的谓词实际上是 执行。带有错误的指令 谓词不写结果,并且 也不评估地址或阅读 操作数。

分支谓词很快。该死的快!如果您查看优化编译器生成的中间 PTX 代码,您会发现它甚至优于普通的算术。所以像 davmac 的答案中的代码可能会尽可能快。

我知道您并没有特别询问 CUDA,但大多数最佳实践指南也适用于 OpenCL 以及可能大部分 AMD 的 GPU 编程。

顺便说一句:在我见过的几乎所有 GPU 代码案例中,大部分时间都花在内存访问上,而不是算术上。确保配置文件! http://en.wikipedia.org/wiki/Program_optimization

【讨论】:

  • 谢谢。我应该花时间阅读 CUDA C 最佳实践指南!
【解决方案2】:

如果您只是想避免实际的 if/else,请使用 ? : 运算符:

return value < 0 ? 0 : (value > 0xff ? 0xff : value);

但是,就效率而言,这应该没有什么不同。

在实践中,您不应该担心像这样微不足道的事情的效率。让编译器做优化。

【讨论】:

  • 正如我所提到的,我希望摆脱分支。所以这和 if/else 是一样的。
  • 如果在目标架构上可行,编译器可能会优化掉分支。
  • 编译器优化不会更容易或更难?:但是。它只是一个语法混乱的 if-else。 ?: 操作符很可能是为了节省存储源代码文件的硬盘空间而发明的……所以除非你的 HD 不能再处理几个字节,否则永远没有理由使用 ?:
【解决方案3】:

你可以做一个 2D 查找表:

unsigned char clamp(short value)
{
  static const unsigned char table[256][256] = { ... }

  const unsigned char x = value & 0xff;
  const unsigned char y = (value >> 8) & 0xff;
  return table[y][x];
}

当然这看起来很奇怪(一个 64 KB 的表用于这个微不足道的计算)。但是,考虑到您提到您想在 GPU 上执行此操作,我认为上述可能是纹理查找,我相信这在 GPU 上非常快。

此外,如果您的 GPU 使用 OpenGL,您当然可以直接使用 @987654321@ 内置函数:

clamp(value, 0, 255);

这不会进行类型转换(似乎 GLSL 中没有 8 位整数类型),但仍然如此。

【讨论】:

【解决方案4】:

您可以在没有明确的if 的情况下使用另一张海报所示的?: 或使用abs() 的有趣属性来计算两个值的最大值或最小值。

例如,表达式(a + abs(a))/2 对正数返回a,否则返回0a0 的最大值)。

这给了

unsigned char clip(short value)
{
  short a = (value + abs(value)) / 2;
  return (a + 255 - abs(a - 255)) / 2;
}

为了让自己相信这是可行的,这里有一个测试程序:

#include <stdio.h>

unsigned char clip(short value)
{
  short a = (value + abs(value)) / 2;
  return (a + 255 - abs(a - 255)) / 2;
}

void test(short value)
{
  printf("clip(%d) = %d\n", value, clip(value));
}

int main()
{
  test(0);
  test(10);
  test(-10);
  test(255);
  test(265);
  return 0;
}

运行时会打印出来

clip(0) = 0
clip(10) = 10
clip(-10) = 0
clip(255) = 255
clip(265) = 255

当然,有人可能会争辩说abs() 中可能有一个测试,但是gcc -O3 例如线性编译它:

clip:
    movswl  %di, %edi
    movl    %edi, %edx
    sarl    $31, %edx
    movl    %edx, %eax
    xorl    %edi, %eax
    subl    %edx, %eax
    addl    %edi, %eax
    movl    %eax, %edx
    shrl    $31, %edx
    addl    %eax, %edx
    sarl    %edx
    movswl  %dx, %edx
    leal    255(%rdx), %eax
    subl    $255, %edx
    movl    %edx, %ecx
    sarl    $31, %ecx
    xorl    %ecx, %edx
    subl    %ecx, %edx
    subl    %edx, %eax
    movl    %eax, %edx
    shrl    $31, %edx
    addl    %edx, %eax
    sarl    %eax
    ret

但请注意,这将比您的原始函数效率低得多,该函数编译为:

clip:
    xorl    %eax, %eax
    testw   %di, %di
    js      .L1
    movl    $-1, %eax
    cmpw    $255, %di
    cmovle  %edi, %eax
.L1:
    rep
    ret

但至少它回答了你的问题:)

【讨论】:

  • abs 还增加了额外的函数调用开销。我认为 abs 可以检查值是否小于零。
  • @Shailesh Kumar:事实上,在这种情况下,GCC 不会为abs() 生成函数调用或测试,如编辑版本所示。
【解决方案5】:

怎么样:

unsigned char clamp (short value) {
    unsigned char r = (value >> 15);          /* uses arithmetic right-shift */
    unsigned char s = !!(value & 0x7f00) * 0xff;
    unsigned char v = (value & 0xff);
    return (v | s) & ~r;
}

但我严重怀疑它的执行速度是否比涉及分支的原始版本快。

【讨论】:

    【解决方案6】:

    假设一个两字节短,并且以代码的可读性为代价:

    clipped_x =  (x & 0x8000) ? 0 : ((x >> 8) ? 0xFF : x);
    

    【讨论】:

    • ?和 : 本质上是分支。
    【解决方案7】:

    你应该为这个丑陋但仅算术的版本计时。

    unsigned char clamp(short value){
      short pmask = ((value & 0x4000) >> 7) | ((value & 0x2000) >> 6) |
        ((value & 0x1000) >> 5) | ((value & 0x0800) >> 4) |
        ((value & 0x0400) >> 3) | ((value & 0x0200) >> 2) |
        ((value & 0x0100) >> 1);
      pmask |= (pmask >> 1) | (pmask >> 2) | (pmask >> 3) | (pmask >> 4) |
        (pmask >> 5) | (pmask >> 6) | (pmask >> 7);
      value |= pmask;
      short nmask = (value & 0x8000) >> 8;
      nmask |= (nmask >> 1) | (nmask >> 2) | (nmask >> 3) | (nmask >> 4) |
        (nmask >> 5) | (nmask >> 6) | (nmask >> 7);
      value &= ~nmask;
      return value;
    }
    

    【讨论】:

      【解决方案8】:

      提高效率的一种方法是将此函数声明为内联函数以避免函数调用开销。您也可以使用三元运算符将其转换为宏,但这将删除编译器的返回类型检查。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-12-02
        • 1970-01-01
        • 1970-01-01
        • 2016-02-17
        • 2015-10-21
        • 2011-06-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多