【问题标题】:Char substraction with clipped underflow带有截断下溢的字符减法
【发布时间】:2020-04-06 20:32:24
【问题描述】:

有没有办法计算:

/** clipped(a - b) **/
unsigned char clipped_substract(unsigned char a, unsigned char b)
{
    return a > b ? a - b : 0;
}

使用一些二进制操作而不是测试?

【问题讨论】:

  • 不,没有这样的东西。你的函数有什么问题?
  • 如果您使用支持 mmx 命令扩展集的 cpu 架构,您可以执行无符号饱和减法来实现此目的。其他架构可能有类似的说明。
  • @Jabberwocky 我想优化计算,测试会降低性能。
  • @Abitbol 如果您关心性能,那么您就可以配置文件。您不会“测试会降低性能,所以我不会使用测试”。这是错误的、危险的,并且会导致性能不佳的糟糕代码。再次说明,或者不要说你关心性能。
  • 您应该看看生成的代码。如果您使用的是不错的编译器,那么这段代码很简单,可以很好地优化。

标签: c++ c binary-operators


【解决方案1】:

原始函数编译为条件移动,不受您的性能问题的影响。你试图在这里执行premature optimization,它对你绝对没有好处。编写可读代码,然后对其进行分析,然后识别和优化性能瓶颈。

您说“测试会降低性能”。如果你想做这样的微优化,你应该着眼于理解这些经验法则背后的原因,而不是把它们当作无条件的真理。

降低“测试”性能的是(错误预测的)控制流分支。现代 CPU 大量使用instruction pipelining,控制流分支意味着不清楚接下来是哪条指令。常见的方法是 CPU 猜测(使用分支预测硬件/算法)分支会走哪条路。如果弄错了,它必须冲洗整个管道并重新填充它,这会浪费循环。

好吧,有问题的代码没有这样的问题。它编译为有条件的移动:

https://godbolt.org/z/upkgok

clipped_substract(unsigned char, unsigned char):
    mov     eax, edi
    mov     edx, 0
    sub     eax, esi
    cmp     dil, sil
    cmovbe  eax, edx
    ret

可以看到这里没有控制流分支。请参阅Why is a conditional move not vulnerable for Branch Prediction Failure?,了解为什么不受上述性能问题的影响。

我重复一遍:编写可读代码,然后对其进行分析,然后识别和优化性能瓶颈。您在这里尝试修复一个性能问题代码,该代码甚至不存在于第一名(更不用说你没有证明这段代码的性能甚至与你的程序的性能有关)。

【讨论】:

  • 感谢提醒!这是解决问题的最佳方法,但请原谅我的好奇心。
  • 一开始就停不下来哈哈哈。再次感谢。
【解决方案2】:

使用clang 11.0,启用编译器优化

unsigned char clipped_substract(unsigned char a, unsigned char b)
{   
    long c = b - a;
    return ((unsigned long)c >> (sizeof(c)*8-1)) * c;
}

尽管任何替代方案所花费的时间几乎可以忽略不计,但在执行 10000000 迭代时,其他方式在我的机器上大约需要 0.300secs 而这个解决方案大约需要 0.200secs

但这仅在您使用大小大于char 的类型时才有效,例如这里我采用了long。我不记得charlong 的确切定义,但我知道long 在我的机器上比char 更宽。所以相应地调整它。

基本上,我只是将减法的结果存储在一个更大的宽度变量中。如果结果是否定的,最高有效位将为1,否则为0

【讨论】:

  • 不,从不移位负类型。需要修复错误:((unsigned long)c >> (sizeof(c)*8-1)) * c;。否则你无法知道你得到的是算术移位还是逻辑移位。
  • @Lundin 实际上,如果需要保留符号位,则转换负数类型可能非常有用。由于确实不能保证算术移位,我通常使用#if -1 >> 1 != -1 #error no arithmetic shift #endif 或类似的保护...
  • @Aconcagua:这个预处理器测试是不够的,因为预处理器可能使用与目标系统不同的算法。
【解决方案3】:
/** clipped(a - b) **/
unsigned char clipped_substract(unsigned char a, unsigned char b)
{
    return a - (a+b) / 2 + getAbs(a-b) / 2;
}

unsigned int getAbs(int n)
{
    int const mask = n >> (sizeof(int) * 8 - 1);

    return ((n + mask) ^ mask);
}

【讨论】:

  • 这样真的更好吗? abs 可能会进行测试,在这种情况下,性能会降低。
  • @Abitbol abs 很容易实现为清除整数的符号位。见godbolt.org/z/Qkb6Fk(这里它使用符号扩展而不是代码中的符号位掩码常量)。
  • abs 可以计算without branching。更高效???不会分析...
  • 问题不在于 abs()。这段代码比单次减法有更多的操作,而且性能必然低于原始代码。我们还应该记住,所有现代处理器都有分支预测器。
  • 我不认为这更好:看看generated assembly code。此版本使用的指令数量是原始版本的两倍多。我知道指令的数量并不是唯一重要的因素,但在这种情况下,我认为这很明显。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-28
  • 2012-01-20
  • 2019-12-19
  • 1970-01-01
  • 1970-01-01
  • 2017-01-23
相关资源
最近更新 更多