【问题标题】:What replacements are available for formerly-widely-supported behaviors not defined by C standard哪些替代品可用于 C 标准未定义的以前广泛支持的行为
【发布时间】:2015-06-21 17:38:14
【问题描述】:

在标准化之前的早期 C 语言中,实现有多种方法来处理各种动作的异常和半异常情况。其中一些会触发陷阱,如果没有先配置,可能会导致随机代码执行。因为此类陷阱的行为超出了 C 标准的范围(并且在某些情况下可能由运行程序控制之外的操作系统控制),并且避免要求编译器不允许依赖于此类的代码陷阱要继续这样做,可能导致此类陷阱的操作行为完全由编译器/平台自行决定。

到 1990 年代末,尽管 C 标准没有要求这样做,但每个主流编译器都在其中许多情况下采用了通用行为;使用此类行为可以提高代码速度、大小和可读性。

既然不再支持请求以下操作的“明显”方式,那么在使用旧编译器时,应该如何以不妨碍可读性或对代码生成产生不利影响的方式替换它们?为了便于说明,假设int 是32 位,ui 是无符号整数,si 是有符号整数,b 是无符号字符。

  1. 给定 ui 和 b,计算 b==0..31 时的 ui << b,或任意表现为 ui << (b & 31) 的值或值 32..255 时为零的值。请注意,如果当右侧操作数超过 31 时左侧操作数为零,则两种行为将相同。

  2. 对于只需要在处理器上运行的代码,当右移或左移从 32 到 255 时产生零,计算 ui << b 为 b==0..31 和 0 为b==32..255。虽然编译器可能能够优化为跳过值 32..255 的移位而设计的条件逻辑(因此代码将简单地执行将产生正确行为的移位),但我不知道有什么方法可以制定这种条件逻辑这将保证编译器不会为它生成不必要的代码。

  3. 与 1 和 2 一样,但用于右移。

  4. 给定 si 和 b 使得 b0..30 和 si*(1<<b) 不会溢出,计算 si*(1<<b)。请注意,使用乘法运算符会严重影响许多旧编译器的性能,但如果移位的目的是缩放有符号值,则在操作数在整个移位过程中保持负数的情况下强制转换为无符号感觉是错误的。

  5. 给定各种整数值,执行加法、减法、乘法和移位,如果没有溢出,结果将是正确的,如果有溢出,代码将产生高位行为的值以非陷阱和非 UB,但以其他方式不确定或将以平台定义的方式可识别陷阱(并且在不支持陷阱的平台上,只会产生不确定的值)。

  6. 给定一个指向分配区域的指针和一些指向其中事物的指针,使用realloc 更改分配大小并调整上述指针以匹配,同时避免在realloc 返回原始块。不一定在所有平台上都可能,但 1990 年代的主流平台都允许代码确定 realloc 是否导致事物移动,并通过减去该指针的前基地址来确定指针到死对象的偏移量曾经是多少对象(请注意,需要通过计算与每个死指针相关联的偏移量,然后将其添加到新指针来完成调整,而不是通过尝试计算旧指针和新指针之间的“差异”——这将是合法的在许多分段架构上失败)。

“超现代”编译器是否提供了对上述内容的任何良好替代品,不会降低代码大小、速度或可读性中的至少一个,而对其他任何一个都没有改进?据我所知,不仅 1990 年代 99% 的编译器可以完成上述所有操作,而且对于每个示例,几乎所有示例都可以以相同的方式编写代码。一些编译器可能已经尝试使用无保护的跳转表来优化左移和右移,但这是我能想到的唯一情况,1990 年代平台的 1990 年代编译器会对“明显”的编码方式有任何问题以上任何一项。如果超现代编译器不再支持经典形式,它们提供什么替代品?

【问题讨论】:

  • 我认为替换是如果你想要一个汇编器,使用一个。长大了的 C 不想再假装自己了。
  • 谷歌搜索“每个程序员都应该知道的关于未定义行为的知识”,并找到 Chris Lattner 的博客文章(Chris Lattner = LLVM 负责人和 Swift 的发明者)。
  • @WumpusQ.Wumbley:如果 C 不应该是汇编语言的升级版,这将允许代码在不同平台之间移植,这取决于它们之间的相似性,它是什么?为什么需要 C 功能并受益于其改进而不是汇编语言的程序员应该放弃这些改进?
  • 我可以看到你已经建立了很多咆哮,不想在它完成之前停止它。但它会随着你学习写b>=0&&b<32?ui<<b:0 而结束,这就是你的意思,并接受花在它上面的额外周期并不重要(因为你现在的周期比你在 1990 年代平台上的周期多得多),并且期望平台之间的可移植性与“移位操作符应该总是像我喜欢的 CPU 上的操作符一样”共存是不明智的
  • @WumpusQ.Wumbley:我编写了很多嵌入式系统代码,其中像这样的额外周期通常很重要,而且 1990 年代的 C 比汇编更好。如果将一个函数提供给旧的编译器需要 30K 的工作代码,那么将相同的函数提供给一个新的编译器将产生 20K 的不工作的代码,并且给新编译器提供一个经过修改的版本可以使用它产生 31K 的运行速度比原来慢的代码,我看不到新编译器的好处。上面的场景不现实吗?

标签: c language-lawyer undefined-behavior


【解决方案1】:

现代标准 C 以这样一种方式指定,即当且仅当您编写代码时对将运行的底层硬件的期望不高于给定的期望时,才能保证它是可移植的由 C 抽象机标准隐式和显式描述。

您仍然可以为给定目标 CPU 和体系结构在给定优化级别具有特定行为的特定编译器编写代码,但是不要期望任何其他编译器(现代或其他,甚至如果您的代码违反了标准规定的条件,即期望任何定义明确的实现无关行为是不合理的,那么您编写的代码的一个小修订版)会竭尽全力尝试直觉您的期望。

【讨论】:

  • 标准的作者通常会指定一个动作调用未定义的行为,如果保证最昂贵的实现上的行为的成本可能超过保证应用程序字段的行为的好处将是最不值得的。这是邀请实施者对他们的目标平台和执行领域的成本和收益进行判断。不能以微不足道的成本保证行为的目标的质量实现不应该保证它。
【解决方案2】:

两个通用原则适用于标准 C 和标准 C++:

  • 无符号数的行为通常比有符号数的行为定义得更好。
  • 将优化视为实施质量问题。这意味着如果您担心特定内部循环的微优化,您应该读取编译器的汇编输出(例如使用gcc -S),如果您发现它失败了要针对适当的机器指令优化定义明确的行为,请向编译器的发布者提交缺陷报告。 (但是,当唯一针对特定平台的实用编译器的发布者对优化不是很感兴趣时,这不起作用,例如针对 MOS 6502 的 cc65。)

从这些原则中,您通常可以得出一个定义明确的方法来实现相同的结果,然后将trust-but-verify principle 应用于生成代码的质量。例如,使移位函数具有明确定义的行为,并让优化器删除架构本身保证的任何不需要的检查。

// Performs 2 for unsigned numbers.  Also works for signed
// numbers due to rule for casting between signed and unsigned
// integer types.
inline uint32_t lsl32(uint32_t ui, unsigned int b) {
  if (b >= 32) return 0;
  return ui << b;
}

// Performs 3 for unsigned numbers.
inline uint32_t lsr32(uint32_t ui, unsigned int b) {
  if (b >= 32) return 0;
  return ui >> b;
}

// Performs 3 for signed numbers.
inline int32_t asr32(int32_t si, unsigned int b) {
  if (si >= 0) return lsr32(si, b);
  if (b >= 31) return -1;
  return ~(~(uint32)si >> b);
}

对于 4 和 5,转换为无符号数,进行数学运算,然后转换回有符号数。这会产生非陷阱的定义明确的行为。

【讨论】:

  • 不幸的是,在许多情况下编译器无法优化测试。例如,在所有 ARM 处理器上,lsr r0,r0,r1 之类的东西对于 0 到 31 范围内的 r1 将表现为 ro&gt;&gt;=r1,对于 32 到 127 范围内的所有 r1 值,r0=0`,但至少在某些情况下它会为其他一些 r1 值产生非零值。因此,即使 lsr r0,r0,r1 在所有情况下都能产生令人满意的结果,像 r1 &gt; 31 ? 0 : r0&gt;&gt;r1 这样的表达式通常也无法优化比较。
  • @supercat 如果你使用assert或类似的东西告诉编译器不会有大于127的值。
  • C 被设计成一种简单的语言,使熟悉平台的程序员能够使用简单且轻量级的编译器为该平台生成合理高效的代码。程序员想要生成类似于以 x 和 y 作为操作数的简单“lsr”指令的行为应该需要写出一个巨大的复杂混乱并希望编译器能够简化它的概念有些落后,而不是简单地写出 x>>y 并让编译器生成特定于平台的移位。
猜你喜欢
  • 2010-11-03
  • 2011-02-06
  • 1970-01-01
  • 2018-09-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-21
  • 1970-01-01
相关资源
最近更新 更多