阈值是绝对值

时间:2016-08-09 17:35:06

标签: c++ c language-lawyer undefined-behavior micro-optimization

我有以下功能:

char f1( int a, unsigned b ) { return abs(a) <= b; }

对于执行速度,我想按如下方式重写它:

char f2( int a, unsigned b ) { return (unsigned)(a+b) <= 2*b; } // redundant cast

或者使用此签名,即使对于非负面b也可能具有微妙含义:

char f3( int a, int b )      { return (unsigned)(a+b) <= 2*b; }

这两种替代方案都可以在一个平台上进行简单的测试,但我需要便携式。假设非负b并且没有溢出风险,这是典型硬件和C编译器的有效优化吗?它对C ++也有效吗?

注意:作为带有-O3的gcc 4.8 x86_64上的C ++,f1()使用6个机器指令而f2()使用4. f3()的说明与f2()的那些。同样感兴趣的是:如果b作为文字给出,则两个函数都编译为3条指令,直接映射到f2()中指定的操作。

4 个答案:

答案 0 :(得分:3)

从带有签名的原始代码开始

char f2( int a, unsigned b );

这包含表达式

a + b

由于其中一个操作数具有signed,另一个(对应的)无符号整数类型(因此它们具有相同的“整数转换等级”),因此 - 遵循“常用算术转换”(第6.3.1.8节) - 带有符号整数类型的操作数将转换为另一个操作数的无符号类型。

很好地定义了转换为无符号整数类型,即使有问题的值不能用新类型表示:

  

[..]如果新类型是无符号的,则通过重复加或减一个可以在新类型中表示的最大值来转换该值,直到该值在新类型的范围内。 60

     

§6.3.1.3/2

脚注60只是说所描述的算术与数学值一起工作,而不是键入的算法。

现在,使用更新的代码

char f2_updated( int a, int b ); // called f3 in the question
事情看起来会有所不同。但由于b被假定为非负数,并假设INT_MAX <= UINT_MAX您可以将b转换为unsigned,而不会担心之后会有不同的数学值。因此你可以写

char f2_updated( int a, int b ) {
  return f2(a, (unsigned)b); // cast unnecessary but to make it clear
}

再次查看f2表达式2*b进一步将b的允许范围限制为不大于UINT_MAX/2(否则数学结果将是错误的)。 所以只要你保持在这些范围内,每件事都很好。

注意:无符号类型不会溢出,它们会根据模运算“换行”。

来自N1570的报价(C11工作草案)

最后一句话:

IMO写这个函数的唯一合理选择是

#include <stdbool.h>
#include <assert.h>
bool abs_bounded(int value, unsigned bound) {
  assert(bound <= (UINT_MAX / 2));
  /* NOTE: Casting to unsigned makes the implicit conversion that
           otherwise would happen explicit. */
  return ((unsigned)value + bound) <= (2 * bound);
}

bound使用带符号的类型没有多大意义,因为值的绝对值不能小于负数。 abs_bounded(value, something_negative)总是假的。如果存在负面界限的可能性,那么我会在这个函数之外捕获它(否则它会“太多”),如:

int some_bound;
// ...
if ((some_bound >= 0) && abs_bounded(my_value, some_bound)) {
  // yeeeha
}

答案 1 :(得分:2)

要确定2个表达式是否与您的目的相同,您必须研究定义域:

  • abs(a) <= b是针对int aunsigned b的所有值定义的,只有a = INT_MIN;的一个特例。在2s补码架构上,abs(INT_MIN)未定义,但最有可能评估为INT_MINunsigned根据<=的{​​{1}}值转换为unsigned,产生正确的值。

  • (unsigned)(a+b) <= 2*b可能会为b > UINT_MAX/2产生不同的结果。例如,a = 1b = UINT_MAX/2+1的评估结果为false。可能会有更多情况,您替代公式会给出不正确的结果。

编辑:好的,问题已被修改...... b现在是int

请注意,a+b在溢出时调用未定义的行为,对2*b调用相同的行为。因此,您假设a+b2*b都没有溢出。此外,如果b为负数,则您的小技巧不起作用。

如果a位于范围-INT_MAX/2..INT_MAX/2范围内b0..INT_MAX/2范围内,则它似乎按预期运行。 C和C ++中的行为相同。

是否优化完全取决于编译器,命令行选项,硬件功能,周围代码,内联等。您已经解决了这一部分,并告诉我们您要刮一两条指令......请记住这一点一种微观优化并不是绝对的。即使计算说明也不一定有助于找到最佳性能。您是否执行了一些基准来衡量这个优化是否值得?差异是否可以衡量?

微优化这样一段代码是弄巧成拙的:它使代码的可读性降低,而且可能不正确。 b在当前版本中可能不是负面的,但如果下一个维护者改变了这一点,他/她可能看不到潜在的影响。

答案 2 :(得分:2)

由于OP需要快速且可移植的代码(且b为正),因此首先对安全编码有意义:

// return abs(a) <= b;
inline bool f1_safe(int a, unsigned b ) { 
  return (a >= 0 && a <= b) || (a < 0 && 0u - a <= b);
}

这适用于所有 a,b(假设为UINT_MAX > INT_MAX)。接下来,使用优化的编译来比较备选方案(让编译器尽其所能)。

OP代码的以下细微变化将在C / C ++中起作用,但存在可移植性问题,除非&#34;假设非负b并且没有溢出风险&#34;可以确定所有目标计算机。

bool f2(int a, unsigned b) { return a+b <= b*2; }

最后,快速便携式代码的OP目标可能会为选择平台找到最佳的代码,而不是其他代码 - 例如微优化。

答案 3 :(得分:1)

是的,这可以移植到兼容平台。从签名到无符号的转换已明确定义:

C规范中的描述有点人为:

  

如果新类型是无符号的,则重复转换该值   加或减一个可能的最大值   以新类型表示,直到值在新范围内   类型。

C++ spec以更合理的方式处理相同的转化:

  

在双补码表示中,此转换是概念性的   并且位模式没有变化

在问题中,f2()f3()略微不同的方式获得相同的结果。

  • f2()中,unsigned操作数的存在会导致signed操作数转换为C ++所需的here。无符号加法可能或者可能不会导致环绕过零,这也是明确定义的[需要引证]。
  • f3()中,添加发生在带符号的表示中,没有特技,然后结果(显式)转换为无符号。所以这比f2()稍微简单一点(也更清楚)。

在这两种情况下,您最终得到总和的相同无符号表示,然后可以将其(作为无符号)与2*b进行比较。将签名值视为无符号类型的技巧允许您仅使用一次比较来检查双边范围。另请注意,这比使用abs()函数更灵活,因为技巧并不要求范围以零为中心。

关于&#34;通常的算术转换&#34;

的评论

我认为这个问题表明使用无符号类型通常是一个坏主意。看看它在这里引起的混乱。

unsigned用于文档目的(或利用移位的值范围)可能很诱人,但由于转换规则,这可能会出错。在我看来,&#34;通常的算术转换&#34;如果你假设算术更可能涉及负值而不是溢出有符号值,则不明智。

我问这个后续问题澄清了这一点:mixed-sign integer math depends on variable size。我学到的一件新事情是,混合符号操作通常是可移植的,因为转换类型为depend on the size relative to that of int

总结:使用类型声明或强制转换来执行无符号操作是一种低级编码风格,应该在必要时谨慎处理。