【问题标题】:How to do a double-chunk add with no undefined behaviour?如何在没有未定义行为的情况下进行双块添加?
【发布时间】:2010-08-25 19:17:04
【问题描述】:

EDIT 公共卫生警告 - 此问题包含关于未定义行为的错误假设。查看已接受的答案。

在阅读recent blog post 之后,我一直在思考在 C 和 C++ 代码中避免所有标准未定义假设的实用性。这是一个从 C++ 中提取的 sn-p,用于进行无符号 128 位加法...

void c_UInt64_Pair::operator+= (const c_UInt64_Pair &p)
{
  m_Low  += p.m_Low;
  m_High += p.m_High;

  if (m_Low < p.m_Low)  m_High++;
}

这显然依赖于关于溢出行为的假设。显然,大多数机器都可以支持正确类型的二进制整数(尽管可能从 32 位块构建或其他),但优化器显然越来越有可能利用此处的标准未定义行为。也就是说,m_Low &lt; p.m_Low 条件可以通过的唯一方法是 m_Low += p.m_Low 溢出,这是未定义的行为,因此优化器可以合法地确定条件 always 失败。在这种情况下,这段代码就被破坏了。

因此问题是……

如何在不依赖未定义行为的情况下编写上述的合理有效版本?

假设您有一个适当的 64 位二进制机器整数,但您有一个恶意编译器,它总是以最坏可能(或不可能)的方式解释您未定义的行为。此外,假设您没有有一些特殊的内置、内在、库或任何东西可以为您做这些。

编辑 次要澄清 - 这不仅仅是检测溢出,还确保 m_Low 和 m_High 最终得到正确的模 2^64 结果,这也是标准未定义的。

【问题讨论】:

  • 1/这不是C。为什么你把这个问题标记为“C”? 2/ 如果这是 C,甚至在 C++ 中,这将不依赖于未定义的行为:无符号整数类型溢出已定义并具有模行为:C99 标准中的 6.2.5.9。
  • @Pascal:实际上,请参阅最新草案的第 5.4 节(我这里没有 C++03 标准,但我相信行为没有改变)--“如果在表达式的求值,结果未在数学上定义或不在其类型的可表示值范围内,行为未定义。"
  • @Pascal - (1) 问题同样与 C 和 C++ 有关。仅提供一个示例并不意味着问题仅与该示例有关。 (2) 那是什么时候发生的?如果是真的,那就是答案(C++ 可能还没有导入相关的 C 规则,但如果没有,它无疑会),所以把它放在一个带有参考的答案中,我会接受。
  • 好吧,在看到许多“C/C++”问题后,这有点下意识的反应。对于诸如此类的微妙问题,它们根本不能被认为是同一种语言。请记住我的评论只有“在 C99 中,此代码将被定义”部分。
  • @Steve314:你知道这两者是一样的吗?在这种特殊情况下,我相信它是。在其他情况下,它不是。假设您要询问'a' 的类型,例如:C 和C++ 中的不同答案。

标签: c++ c integer-overflow


【解决方案1】:

来自 C++ 1998 标准,3.9.1(4):“声明为无符号的无符号整数应遵守算术模 2^n 的定律,其中 n 是该特定整数大小的值表示中的位数。”请注意,这里的“整数”指的是任何整数类型,而不仅仅是int

因此,假设这些是无符号整数,如类型中所暗示的“UInt64”,这是 C++ 中定义的行为,应该可以按预期工作。

【讨论】:

  • 什么是 C++ 1995 标准?我知道'99,'03 ......从来没有听说过那个。在任何情况下,虽然这是真的,但这并不意味着定义了溢出行为,请参阅我上面引用的部分 - “如果在评估表达式期间,结果未在数学上定义或不在可表示值的范围内对于它的类型,行为是未定义的。”
  • @Billy,由于无符号类型遵循模运算规则,因此永远不会有超出可表示值范围的结果,因此定义了行为。 3.9.1/4 的脚注说明了很多。
  • @Billy ONeal:让我们假设 C++ 的引用是正确的。在任何情况下,相同的模数规则都适用于 C99。算术模 2^n 总是定义明确的,结果没有歧义:它们总是在0 &lt;= x &lt; 2^n 的范围内。因此,关于数学上未定义行为的额外条款永远不会适用于无符号类型。无符号整数类型会换行,但不会溢出。
  • 你也可以引用 C 标准。
  • 获得两个任意 32 位数字的乘积可能超过 2^63 的低 32 位的最佳可移植方法是什么?如果“unsigned int”是 32 位或更小,或者如果“signed int”是 65 位或更大,则没有问题,但如果两者都是 64 位呢? UInt32 转换为 Int64,然后乘积产生未定义的结果。将操作数转换为 UInt64 可能会起作用,但这可能会为处理器添加大量额外代码,否则 'int' 将是 32 位。
【解决方案2】:

如果你想要一个真正有效的方法,你必须用 C 或 C++ 以外的东西编写代码。为了合理高效,您必须确保溢出永远不会发生,并检测并补偿何时会发生。

基本上,对于每个 64 位组件,您需要分别使用低 63 位和最高位计算加法。从这些单独的计算中,您可以算出 64 位总数是多少,以及是否有进位。

然后,当您执行高位 64 位加法时,如果有进位,则添加进位。如果由此产生进位,则您的 128 位变量已溢出,您需要触发异常,或以其他方式处理此情况。

【讨论】:

  • 不;你所说的对于有符号整数类型是正确的,但不是无符号的。
  • Aannd... 为什么不能在 C 或 C++ 中有效地做到这一点?你会建议什么替代方案会有效?
  • 最有效的方法是实际使用为此目的而提供的硬件设施,即 128 位寄存器或携带标志。
  • 但我确实对整个签名/未签名的事情进行了纠正。出于某种原因,我似乎记得所有算术溢出都是未定义的。
  • @swestrup - 我也被愚弄了:-(
猜你喜欢
  • 2018-12-19
  • 2017-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-28
  • 2013-01-02
相关资源
最近更新 更多