【问题标题】:Short-circuiting on boolean operands without side effects在没有副作用的布尔操作数上短路
【发布时间】:2011-11-13 09:36:59
【问题描述】:

对于赏金:如何在不禁用或降低优化级别的情况下根据具体情况禁用此行为?

以下条件表达式是在 MinGW GCC 3.4.5 上编译的,其中 asigned long 类型,munsigned long 类型。

if (!a && m > 0x002 && m < 0x111)

使用的CFLAGS-g -O2。这是对应的汇编GCC输出(用objdump转储)

120:    8b 5d d0                mov    ebx,DWORD PTR [ebp-0x30]
123:    85 db                   test   ebx,ebx
125:    0f 94 c0                sete   al
128:    31 d2                   xor    edx,edx
12a:    83 7d d4 02             cmp    DWORD PTR [ebp-0x2c],0x2
12e:    0f 97 c2                seta   dl
131:    85 c2                   test   edx,eax
133:    0f 84 1e 01 00 00       je     257 <_MyFunction+0x227>
139:    81 7d d4 10 01 00 00    cmp    DWORD PTR [ebp-0x2c],0x110
140:    0f 87 11 01 00 00       ja     257 <_MyFunction+0x227>

120-131 可以很容易地追踪为首先评估!a,然后评估m &gt; 0x002。第一次跳转条件直到133 才会发生。此时,已经评估了 两个 表达式,无论第一个表达式的结果如何:!a。如果a 等于0,则表达式可以(并且应该)立即结束,这里不做。

这与要求布尔运算符在确定结果后立即短路的 C 标准有何关系?

【问题讨论】:

  • 可能是因为编译器知道比较 POD 没有副作用?这是在打开还是关闭优化的情况下编译的?
  • 我在帖子中给出了编译选项:-g -O2。所以是的,通过优化。
  • @RedX:这是 C,不是 C++。 “POD”未定义,所有比较都没有副作用(尽管对其操作数的评估可能有副作用,在这种情况下无法进行此类优化)。
  • 如果操作数之一是函数调用并且函数具有副作用,则比较不是无副作用的。在这种情况下,短路可能变得很重要。在上面的代码中,它不是。
  • @RudyVelthuis:R. 已经提到:“(尽管对其操作数的评估可能有副作用......)”。

标签: c gcc compiler-construction mingw compiler-optimization


【解决方案1】:

C 标准只规定了“抽象机器”的行为;它没有指定程序集的生成。只要程序的可观察行为与抽象机器上的行为相匹配,实现就可以使用它喜欢的任何物理机制来实现语言结构。标准(C99)中的相关部分是5.1.2.3 程序执行。

【讨论】:

  • 这被非正式地称为“as-if”规则——编译器可以生成它喜欢的任何代码,只要外部可见的行为与 as if 相同编写的代码在抽象机器上执行。对 volatile 变量的访问是外部可见行为的一部分,因此如果您将 m 声明为 volatile,那么您应该会看到生成了不同的代码。
  • @caf:您应该将其添加为答案,我会为 volatile 评论 +1。
  • 根据实现的不同,可能有也可能没有任何外部访问易失性对象的可能性。对于具有自动存储持续时间且地址从未被占用的对象,除了需要与 setjmp/longjmp 交互之外,volatile 肯定不需要任何影响。在内存完全由虚拟环境控制并且没有硬件访问或共享内存的实现中也可能出现这种情况,尤其是在实现没有异步生成的信号时...
  • 我不认为这取决于是否有外部访问的可能性(至少在常见的平台上,调试器甚至会为自动存储持续时间对象提供这种外部访问)。 §5.1.2.3 明确表示访问 volatile 对象是一种副作用,而 §6.7.3 说 "...任何引用此类对象的表达式都应严格按照抽象机的规则进行评估”。
  • 没有要求实现提供或促进调试器...据我所知,在唯一的输入和输出是文件的纯粹抽象、隔离的实现中,volatile 不需要强加任何对编译器的限制,除非存在 setjmp/longjmp...
【解决方案2】:

这可能是编译器优化,因为比较整数类型没有副作用。您可以尝试在不进行优化的情况下进行编译,或者使用具有副作用的函数而不是比较运算符,看看它是否仍然这样做。

例如,试试

if (printf("a") || printf("b")) {
    printf("c\n");
}

它应该打印ac

【讨论】:

    【解决方案3】:

    正如其他人所提到的,这个程序集输出是一种编译器优化,不会影响程序的执行(就编译器而言)。如果您想有选择地禁用此优化,您需要告诉编译器您的变量不应跨代码中的序列点进行优化.

    序列点是控制表达式(ifswitchwhiledofor 的所有三个部分中的评估) 、逻辑 OR 和 AND、条件 (?:)、逗号和 return 语句。

    为防止编译器在这些点上进行优化,您必须声明变量volatile。在您的示例中,您可以指定

    volatile long a;
    unsigned long m;
    {...}
    if (!a && m > 0x002 && m < 0x111) {...}
    

    之所以有效,是因为volatile 用于指示编译器它无法预测等效机器相对于变量的行为。因此,它必须严格遵守你代码中的序列点。

    【讨论】:

    • 干杯!我没有改进我的答案,所以真正需要代表的人可以获得赏金,看起来效果很好。 :-)
    【解决方案4】:

    编译器的优化 - 它将结果放入 EBX,将其移至 EAX 的一部分 AL,对 EDX 进行第二次检查,然后根据 EAX 和 EDX 的比较进行分支。这样可以节省一个分支并让代码运行得更快,而不会产生任何副作用。

    如果您使用-O0 而不是-O2 进行编译,我想它会生成更简单的程序集,更符合您的期望。

    【讨论】:

    • 正如我在其他地方所说的,特定的内存映射设备可能并且确实存在这种行为的问题。所以在特定情况下,是的,读取内存本身会产生副作用。
    • 在特定情况下,是的。我对 gcc 在这里所做的事情不做任何承诺,但至少在理论上他们可以知道所讨论的模式是安全的。他们已经加载了 ebp-0x30。这不仅引入了这四个字节,还引入了整个缓存行。如果 ebp 是 16 字节甚至 8 字节对齐的,则高速缓存行包括 ebp-0x2c 处的内存。在这些情况下,无论您是否进行比较,都会产生内存读取副作用。正如我所说,我不保证 gcc 实际上满足所有这些条件,但它可能是。
    【解决方案5】:

    无论哪种方式,代码的行为都是正确的(即,符合语言标准的要求)。

    您似乎正试图找到一种方法来生成特定的汇编代码。在两种可能的汇编代码序列中,它们的行为方式相同,你会发现一个令人满意,另一个不令人满意。

    保证令人满意的汇编代码序列的唯一真正可靠的方法是显式编写汇编代码。 gcc 确实支持内联汇编。

    C 代码指定行为。汇编代码指定机器代码。

    但这一切都引发了一个问题:为什么这对你很重要? (我不是说不应该,我只是不明白为什么应该。)

    编辑:am 究竟是如何定义的?如果按照您的建议,它们与内存映射设备有关,那么它们应该被声明为volatile——这可能正是您问题的解决方案。如果它们只是普通变量,那么编译器可以随心所欲地处理它们(只要它不影响程序的可见行为)因为你没有要求它不要这样做

    【讨论】:

    • 某些类型的低级内存映射设备可能会出现这种行为问题。这就是行为。
    • am 是否声明为 volatile?如果是这样,您应该在原始问题中提到它。如果不是,则语言不认为对它们的访问是“行为”,只要程序的可见行为(输入、输出、对易失性对象的访问)满足语言要求,编译器就可以做任何它喜欢的事情。
    • 不,它们没有被声明为volatile。你原来的答案没有提到它。 :)
    • @Unsigned:没有将它们定义为volatile 的内存映射设备的定义虽然很常见,但完全是错误的。
    • @supercat 我完全同意。
    猜你喜欢
    • 2021-07-25
    • 2020-10-03
    • 2011-07-16
    • 1970-01-01
    • 2011-04-07
    • 2018-05-13
    • 2015-12-06
    • 2014-02-26
    • 1970-01-01
    相关资源
    最近更新 更多