是否假定C / C ++中的所有函数都返回?

我正在阅读关于未定义行为的本文 ,其中一个示例“优化”看起来非常可疑:

if (arg2 == 0) ereport(ERROR, (errcode(ERRCODE_DIVISION_BY_ZERO), errmsg("division by zero"))); /* No overflow is possible */ PG_RETURN_INT32((int32) arg1 / arg2); 

图2 :意外的优化使PostgreSQL的src/backend/utils/adt/int8.c中的除零检查src/backend/utils/adt/int8.c 。 对ereport(ERROR, :::)的调用将引发exception。

本质上,编译器假定 ereport将返回,并删除arg2 == 0检查,因为除法的存在意味着非零分母,即arg2 != 0

这是一个有效的优化吗? 编译器是否可以自由地假设函数将始终返回?

编辑:整个事情取决于电子报,因此描述:

  84 /*---------- 85 * New-style error reporting API: to be used in this way: 86 * ereport(ERROR, 87 * (errcode(ERRCODE_UNDEFINED_CURSOR), 88 * errmsg("portal \"%s\" not found", stmt->portalname), 89 * ... other errxxx() fields as needed ...)); 90 * 91 * The error level is required, and so is a primary error message (errmsg 92 * or errmsg_internal). All else is optional. errcode() defaults to 93 * ERRCODE_INTERNAL_ERROR if elevel is ERROR or more, ERRCODE_WARNING 94 * if elevel is WARNING, or ERRCODE_SUCCESSFUL_COMPLETION if elevel is 95 * NOTICE or below. 96 * 97 * ereport_domain() allows a message domain to be specified, for modules that 98 * wish to use a different message catalog from the backend's. To avoid having 99 * one copy of the default text domain per .o file, we define it as NULL here 100 * and have errstart insert the default text domain. Modules can either use 101 * ereport_domain() directly, or preferably they can override the TEXTDOMAIN 102 * macro. 103 * 104 * If elevel >= ERROR, the call will not return; we try to inform the compiler 105 * of that via pg_unreachable(). However, no useful optimization effect is 106 * obtained unless the compiler sees elevel as a compile-time constant, else 107 * we're just adding code bloat. So, if __builtin_constant_p is available, 108 * use that to cause the second if() to vanish completely for non-constant 109 * cases. We avoid using a local variable because it's not necessary and 110 * prevents gcc from making the unreachability deduction at optlevel -O0. 111 *---------- 

编译器是否可以自由地假设函数将始终返回?

在C或C ++中,编译器在此基础上进行优化是不合法的,除非它以某种方式明确知道ereport返回(例如通过内联并检查代码)。

ereport依赖于至少一个#define和传入的值,所以我不能确定,但​​它肯定看起来被设计为有条件地不返回(并且它调用extern函数errstart ,就编译器而言,可能会或可能不会返回)。 因此,如果编译器确实假设它总是返回,那么编译器是错误的,或者ereport的实现是错误的,或者我完全误解了它。

该报说,

但是,程序员未能通知编译器对ereport(ERROR,:::)的调用没有返回。

我不相信程序员有任何这样的义务,除非在编译这个特定的代码时可能存在一些非标准的扩展,这使得记录的优化能够在某些条件下破坏有效的代码。

不幸的是,通过引用标准来certificate代码转换是不正确的是相当困难的,因为我无法引用任何东西来表明没有,隐藏在700-900页的某个地方,一个小句子说“哦,通过顺便说一句,所有function必须返回“。 我实际上并没有读过标准的每一行,但是这样的子句是荒谬的:需要允许函数调用abort()exit()longjmp() 。 在C ++中,它们也可以抛出exception。 并且它们需要被允许有条件地执行此操作 – 属性noreturn意味着函数永远不会返回,而不是它可能不会返回,并且它的缺失certificate函数是否返回。 我对这两个标准的体验是它们不是荒谬的。

优化不允许破坏有效的程序,它们受到保留可观察行为的“as-if”规则的约束。 如果ereport没有返回,则“优化”会改变程序的可观察行为(从做任何报告而不是返回,到由于除零而导致的未定义行为)。 因此被禁止。

这里有关于这个特定问题的更多信息:

http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=616180

它提到了一个GCC错误报告http://gcc.gnu.org/bugzilla/show_bug.cgi?id=29968被(正确的IMO)拒绝,但如果ereport没有返回那么PostGreSQL问题与那个不一样拒绝GCC错误报告。

在debian中,bug描述如下:

gcc的家伙们充满了它。 这里相关的问题是C标准对序列点的定义,特别是要求在执行早期函数调用之前不能发生后面语句的可见副作用。 我最后一次纠缠他们,我得到了一些蹩脚的声称,SIGFPE并不是规范定义中的副作用。 在那时,有用的讨论停止了,因为不可能与愿意宣称这一点的人谈判。

事实上,如果后来的声明有UB,则标准明确规定整个程序具有UB。 Ben在答案中有引用。 事实并非如此(因为这个人似乎认为)所有可见的副作用必须发生在UB之前的最后一个序列点。 UB允许发明一个时间机器(更多的是,它允许乱序执行,假设所执行的一切都有定义的行为)。 如果这就是他们所说的话,那些gcc的家伙并不满足于此。

如果编译器选择保证并记录(作为标准的扩展)它发生的SIGFPE将是一个可见的副作用,但如果它只是UB的结果,那么它不是。 比较例如GCC的-fwrapv选项,它将UB(标准所说的)的整数溢出更改为环绕(编译器保证, 只有在指定选项时 )。 在MIPS上,gcc有一个选项-mcheck-zero-division ,看起来它确定了除以零的行为,但我从未使用它。

该论文的作者可能会注意到对GCC的投诉是错误的,并且认为其中一个PostGreSQL作者在这种方式错误时会影响他们,因为他们将窃笑引号放入:

我们在PostgreSQL中发现了七个类似的问题,在源代码注释中被称为“GCC错误”

但是一个不返回的函数与一些副作用后返回的函数非常不同。 如果它没有返回,那么具有UB的语句不会在标准中的C(或C ++)抽象机器的定义中执行 。 未执行的语句不会执行:我希望这不是有争议的。 因此,如果“gcc guys”声称未完成的语句中的UB使得整个程序未定义, 那么他们就会充满它。 我不知道他们声称这一点,并且在Debian报告的最后,有人建议GCC 4.4可能已经消除了这个问题。 如果是这样,那么也许PostGreSQL确实遇到了一个最终被承认的错误,而不是(正如你所链接的论文的作者所认为的那样)一个有效的优化,或者(正如那个说gcc家伙充满它的人所想的那样)误解了GCC作者的标准。

我认为答案是在1.9p5节中找到的,至少对于C ++来说

执行格式良好的程序的符合实现应该产生与具有相同程序和相同输入的抽象机的相应实例的可能执行之一相同的可观察行为。 但是,如果任何此类执行包含未定义的操作 ,则此国际标准不要求使用该输入执行该程序的实现( 甚至不考虑第一个未定义操作之前的操作 )。

事实上,宏扩展为对errstart的调用,它将返回(ERROR >= ERROR) ,显然是真的。 这会触发对errfinish的调用,调用proc_exit运行一些已注册的清理,然后运行标准运行时函数。 因此,没有可能的执行包含被零除。 但是,编译逻辑测试一定是错误的。 或者可能是早期版本的代码无法正常退出。

在我看来,除非编译器能够certificateereport()不调用exit()abort()或其他一些程序终止机制,否则这种优化是无效的。 语言标准提到了几种终止机制,甚至通过从exit()函数返回main()来定义“正常”程序终止。

更不用说程序终止不是避免分割表达式所必需的。 for (;;) {}完全有效C.

不,在最新的C标准C11中,甚至有一个新的关键字来指定函数不会返回, _Noreturn

该论文没有if (arg2 == 0)检查被删除了。 它说在检查之前移动了分部。

引用论文 :

… GCC在零检查arg2 == 0之前移动除法,导致除零。

结果是一样的,但推理是不同的。

如果编译器认为ereport将返回,那么它“知道”将在所有情况下执行除法。 此外,if语句不会影响除法的参数。 显然,除法不影响if语句。 虽然对ereport调用可能具有可观察到的副作用,但是除法不会(如果我们忽略任何除零exception)。

因此,编译器认为as-if规则赋予它相互重新排序这些语句的自由 – 它可以在测试之前移动除法,因为可观察行为应该是相同的(对于产生定义行为的所有情况) )。

一种看待它的方法是未定义的行为包括时间旅行。 😉

我认为未定义的行为(例如,除以0)应该被视为可观察的行为。 这将阻止这种重新排序,因为除法的可观察行为必须在对ereport的调用的可观察行为之后发生。 但我不写标准或编译器。

在嵌入式系统中,永不返回的function是司空见惯的。 它们也不应该被优化。

例如,一个常见的算法是在main() (也就是后台循环)中有一个永久循环,所有function都发生在ISR(中断服务程序)中。

另一个例子是RTOS任务。 在我们的嵌入式系统项目中,我们有一个infinte循环中的任务:Pend on message queue,process message,repeat。 他们将在项目的整个生命周期中这样做。

某些嵌入式系统具有安全关闭环路,可将机器置于安全状态,锁定所有用户输入,并等待电源关闭或复位。

此外,某些嵌入式系统可能会关闭系统。 关闭电源可防止系统返回。

有理由不是所有function都需要返回或必须返回。 如果您手机中的所有function都退回了,那么您使用它的速度就不够快。

假设大多数函数最终返回。 某些编译器中存在特定于编译器的扩展,以通知编译器函数永远不会返回。

__attribute__ ((noreturn))为gcc执行此操作。