【问题标题】:Threshold an absolute value阈值绝对值
【发布时间】:2016-08-09 17:35:05
【问题描述】:

我有以下功能:

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() 中指定的操作。

【问题讨论】:

  • 为什么你认为这个实现会更优化?您是否检查过编译器实际发出的内容?
  • 无论如何,演员阵容完全是多余的。
  • a*a &lt;= b*b - 没有分支..溢出危险,虽然
  • @BaummitAugen:由于测试了多个场景,我在这里犯了一个错误。演员 是多余的,因为这是最初发布的,因为它是基于 b 的类型为 unsigned 的隐式。我应该将b 保留为int 类型,现在将进行编辑。
  • 您是否检查过 6 指令实际上比 4 指令慢?情况并非总是如此。

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


【解决方案1】:

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

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 &lt;= 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 编写此函数的唯一真正合理的选择是 as

#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
}

【讨论】:

  • Arr ...当我写答案时,编辑将 b 类型更改为有符号整数。
  • 我真的很想回滚它,因为它使这个答案和所有 cmets 毫无意义。
  • @NathanOliver:cmets 注意到演员阵容是多余的,但这并不影响问题的重点。
  • @nobar:演员阵容是多余的,因为它在评估a+b 时应用了升级之后。更改b 的类型确实 有效果。仔细阅读人们对当二元运算符有两个不同类型的操作数时如何进行提升的说法。
  • @EOF 我倾向于不同意:“ - 任何无符号整数类型的等级应等于相应有符号整数类型的等级,如果有的话。”同等等级意味着转换为无符号。
【解决方案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 &gt; INT_MAX)。接下来,比较使用优化编译的备选方案(让编译器做它最擅长的事情)。


以下 OP 代码的细微变化将在 C/C++ 中工作,但存在可移植性问题,除非在所有目标机器上可以确定“假设非负 b 且没有溢出风险”。 p>

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

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

【讨论】:

  • 不做UINT_MAX &gt; INT_MAX的假设,使用return (a &gt;= 0 &amp;&amp; a &lt;= b) || (b &amp;&amp; a &lt; 0 &amp;&amp; -1 - a &lt;= b-1) || (b==0 &amp;&amp; a== 0); 如果b是一个常数,很多代码会优化出来。
【解决方案3】:

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

  • abs(a) &lt;= b 是为int aunsigned b 的所有值定义的,a = INT_MIN; 只有一种特殊情况。在 2s 补码架构上,abs(INT_MIN) 未定义,但很可能计算为 INT_MIN,根据需要,unsigned 转换为具有 unsigned 值的 &lt;=,产生正确的值。

  • (unsigned)(a+b) &lt;= 2*b 可能会为 b &gt; 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 在当前版本中可能不是负面的,但如果下一个维护者改变它,他/她可能看不到潜在的影响。

【讨论】:

  • 这个答案似乎只解决了边界问题。我试图通过说“没有溢出风险”来明确排除这些。
  • @nobar:你是在我指出之后才这样做的。以使答案无关紧要的方式修改问题只能通过 EDIT: 段落来完成。我回答了问题的有效部分,并提示它何时无效。请参阅我对问题的优化部分的更新答案。
【解决方案4】:

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

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

如果新类型是无符号的,则该值被反复转换 比最大值多加或减一 以新类型表示,直到值在新类型的范围内 输入。

C++ spec 以更明智的方式处理相同的转换:

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


在问题中,f2()f3()略有不同的方式实现相同的结果。

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

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


关于“常用算术转换”的评论

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

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

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

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

【讨论】:

  • 如果需要,您可以更具体地说“双补平台”,而不仅仅是“兼容平台”。后者就像说“它可以在它工作的平台上工作”。当您在一个补码或符号/大小机器上将负数 int 转换为无符号时会发生什么?这段代码在这样的机器上仍然安全吗?只要它没有可能的带符号溢出或其他 UB,它仍然很有用,但是知道它是否总体上是安全的,或者在非二进制补码 C 实现中到底哪里出错会很有趣。
  • @PeterCordes:“兼容”是指“符合语言标准”。我认为标准几乎意味着二进制补码,而不是完全要求它。据我了解,奇怪的硬件必须使用变通方法来实现语言合规性。解决方法,例如“重复地在可以表示的最大值之外加或减一……”。
  • 是的,我也没有考虑过该措辞的含义,但我不确定它是否非常昂贵。实现该措辞可能最多需要一个分支,并且从 unsigned 到 int 的强制转换不会一直发生。令人失望的是,该标准拒绝实际标准化除了原子int 之外的任何东西的二进制补码,或者像有符号int 的算术右移之类的东西。标准本身太弱;到目前为止,我喜欢我所看到的 Rust(尤其是内置的 popcnt 和其他东西),但还没有真正用它做任何事情。
  • C89 的作者面临的一个问题是,一些实现将短无符号类型提升为 int,而另一些实现提升为无符号。太糟糕了 C89 没有通过定义限定符来区分例如表示数字 0-65535 的 16 位类型来自表示 mod-65536 等价类的 16 位类型。然后,不合格的无符号类型可以以实现定义的方式运行,而需要数字或模运算的代码可以指定它想要的。
  • @nobar:在编写 C 标准时以及之后的很长一段时间内,大多数(例如 16 位)编译器会通过执行产生的加法来处理 f3(16384,24576) 40960 或 -24576 以及产生 49152 或 -16384 的乘法。然后它将两个结果都转换为无符号(分别为 40960 和 49152),并执行比较。然而,最近,编译器处理整数溢出已经成为一种时尚——即使是在像上面这样以前的良性情况下——以可能对其他代码造成任意副作用的方式。
猜你喜欢
  • 1970-01-01
  • 2021-02-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-09
  • 1970-01-01
  • 1970-01-01
  • 2014-06-08
相关资源
最近更新 更多