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

Pet*_*der 20 c c++ optimization undefined-behavior language-lawyer

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

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

图2:意外的优化使src/backend/utils/adt/int8.cPostgreSQL中的除零检查无效.该召唤 ereport(ERROR, :::)将引发异常.

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

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

编辑:整个事情取决于ereport,因此描述:

   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  *----------
Run Code Online (Sandbox Code Playgroud)

Ste*_*sop 14

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

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

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

论文说,

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

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

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

优化不允许破坏有效的程序,它们受到保留可观察行为的"as-if"规则的约束.如果ereport不返回则"优化"会改变程序的可观察行为(从做任何事情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的结果,那么它不是.比较例如-fwrapvGCC 的选项,它将整数溢出从UB(标准所说的)改变为环绕(编译器保证,只有在指定选项时).在MIPS上,gcc有一个选项-mcheck-zero-division,看起来它确定了除以零的行为,但我从未使用它.

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

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

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


Ben*_*igt 8

我想在第1.9p5节中找到了答案,至少对于C++来说

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

实际上,宏扩展到errstart将返回的调用(ERROR >= ERROR),显然是正确的.这会触发一个调用errfinish,调用proc_exit运行一些已注册的清理,然后运行标准运行时函数exit.因此,没有可能的执行包含被零除.但是,编译逻辑测试这一定必须弄错.或者可能是早期版本的代码未能正确退出.

  • "如果任何此类执行包含未定义的操作" - 但**它不会**.除非您在考虑其中的操作顺序中包含的任何操作是否未定义之前应用引用的部分*.对于`arg2 == 0`,代码永远不会执行,因此*no*"执行包含未定义的操作"(因为没有执行,完全停止). (6认同)
  • @Cornstalks:我会这么说,是的(实际上我已经在这里的某个地方发表了评论,我不能责怪你没有发现它).同样,编译器必须假设它们看不到的所有函数都可以修改指针可能存在于调用函数之外的任何非"严格"内存.这些假设阻碍了优化.事实上,内联代码的许多好处并不是避免在堆栈上推送参数并跳转到被调用者的成本,而是调用者和调用者可以联合优化而不是单独优化. (2认同)