阈值为绝对值

Threshold an absolute value

本文关键字:绝对值 阈值      更新时间:2023-10-16

我有以下函数:

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++有效?


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

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

char f2( int a, unsigned b );

这包含表达式

a + b
由于其中一个操作数具有有符号整数类型

,另一个操作数具有(相应的(无符号整数类型(因此它们具有相同的"整数转换等级"(,因此 - 在"常用算术转换"(§ 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
}

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

注意:无符号类型不会溢出,它们根据模算法"包装"。

引用自 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
}

由于 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++ 中工作,但存在可移植性问题的风险,除非"假设非负 b 并且没有溢出风险">在所有目标机器上都可以确定。

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

最后,快速和可移植代码的OP目标可能会找到最适合所选平台的代码,但不适用于其他平台 - 这就是微优化。

要确定这两个表达式是否等效于您的目的,您必须研究定义的域:

  • abs(a) <= b是针对int aunsigned b的所有值定义的,只有一个特殊情况用于a = INT_MIN;。在 2s 补码架构上,abs(INT_MIN) 没有定义,但最有可能的计算结果为 INT_MIN ,根据具有unsigned值的<=的要求转换为unsigned,产生正确的值。

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

编辑

好的,问题被编辑了...b现在是一个int.

请注意,a+b在溢出的情况下调用未定义的行为,对于2*b也是如此。 因此,您假设既不会溢出a+b也不会2*b溢出。 此外,如果b是负面的,你的小技巧就行不通了。

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

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

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

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

  • 有符号整数和无符号整数之间的转换
  • int
  • 到无符号 int 的转换
  • 在 C 语言中签名到无符号转换 - 它总是安全的吗?

C 规范中的描述有点做作:

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

C++规范以更合理的方式解决了相同的转换:

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


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

  • f2() unsigned操作数的存在会导致signed操作数的转换,这是此处对C++的要求。 无符号加法可能会或可能不会导致环绕超过零,这也是明确定义的[需要引用]。
  • f3()加法发生在有符号表示中,没有棘手之处,然后结果(显式地(转换为无符号。 所以这比f2()稍微简单一些(也更清楚(。

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


关于"通常的算术转换"的评论

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

unsigned用于文档目的(或利用移位值范围(可能很诱人,但由于转换规则,这往往是一个错误。在我看来,如果您假设算术更有可能涉及负值而不是溢出有符号值,那么"通常的算术转换"是不明智的。

我问这个后续问题是为了澄清这一点:混合符号整数数学取决于变量大小。 我学到的一件新事情是,混合符号操作通常不是可移植的,因为转换类型将取决于相对于int的大小。

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