当前位置: 首页 > 知识库问答 >
问题:

如何防止gcc优化器产生错误的位操作?

秦奇
2023-03-14

考虑以下程序。

#include <stdio.h>

int negative(int A) {
    return (A & 0x80000000) != 0;
}
int divide(int A, int B) {
    printf("A = %d\n", A);
    printf("negative(A) = %d\n", negative(A));
    if (negative(A)) {
        A = ~A + 1;
        printf("A = %d\n", A);
        printf("negative(A) = %d\n", negative(A));
    }
    if (A < B) return 0;
    return 1;
}
int main(){
    divide(-2147483648, -1);
}

当它在没有编译器优化的情况下编译时,它会产生预期的结果。

gcc  -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1

当使用编译器优化对其进行编译时,会产生以下错误输出。

gcc -O3 -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative 
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0

我正在运行gcc 5.4.0版。

我是否可以在源代码中进行更改,以防止编译器在-O3下产生这种行为?

共有3个答案

巫马山
2023-03-14

详细解释这里发生的事情:

>

  • 在这个答案中,我假设long是32位,long long是64位。这是最常见的情况,但不能保证。

    C没有有符号整数内容。-2147483648实际上是long long类型,您在其上应用一元减号运算符。

    • int中?不,它不能。
    • long中?不,它不能。
    • long long中?是的,它可以。因此,整数常量的类型将是long long。然后在那个long long上应用一元减号。

    下一个棘手的部分是函数negative,在这里使用0x8000000。这也不是一个int,也不是一个long,而是一个无符号int(请参见此处了解解释)。

    将传递的int无符号int进行比较时,“常用算术转换”(见此)强制将int隐式转换为无符号int。在这种特定情况下,它不会影响结果,但这就是为什么gcc-Wconversion用户在这里得到了一个很好的警告。

    (提示:已经启用-Wconversion!它有助于捕捉细微的bug,但不是-Wall-Wextra的一部分)

    接下来执行~A,这是值的二进制表示形式的一个按位反转,结果是值0x7FFFFFFF。事实证明,这与32位或64位系统上的INT_MAX值相同。因此,0x7FFFFFFF 1给出一个有符号整数溢出,从而导致未定义的行为。这就是该项目行为不端的原因。

    厚颜无耻地,我们可以将代码更改为A=~A 1u;,突然一切都按预期工作,这也是因为隐式整数提升。

    经验教训:

    在C语言中,整数常量以及隐式整数提升是非常危险和不直观的。它们可以微妙地完全改变程序的含义,并引入错误。在C语言中的每个操作中,都需要考虑所涉及的操作数的实际类型。

    玩转C11_Generic可能是查看实际类型的好方法。示例:

    #define TYPE_SAFE(val, type) _Generic((val), type: val)
    ...
    (void) TYPE_SAFE(-2147483648, int); // won't compile, type is long or long long
    (void) TYPE_SAFE(0x80000000, int);  // won't compile, type is unsigned int
    

    保护自己免受此类错误侵害的良好安全措施是始终使用stdint. h并使用MISRA-C。

  • 季博
    2023-03-14

    编译器将您的A=~A 1;语句替换为单个neg指令,即此代码:

    int just_negate(int A) {
        A = ~A + 1;
        return A;
    }
    

    将被汇编为:

    just_negate(int):
      mov eax, edi
      neg eax         // just negate the input parameter
      ret
    

    但是,编译器也足够聪明,可以实现这一点,如果A

    这意味着第二个printf(“负(A)=%d\n”,负(A)) 可以“安全”优化为:

    mov edi, OFFSET FLAT:.LC0    // .string "negative(A) = %d\n"
    xor eax, eax                 // just set eax to zero
    call printf
    

    我使用在线Godbolt编译器资源管理器来检查程序集是否进行了各种编译器优化。

    顾高扬
    2023-03-14

    >

    A=~A 1;调用未定义的行为,因为~A 1会导致整数溢出。

    这不是编译器,而是你的代码。

     类似资料:
    • 我试图找出关键的优化选项。首先,用 -Q-v列出了启用的标志(-faggressive loop optimizations-falign labels-fasynchronous unwind tables等)。然后,如果将这些标志直接提供给gcc而不是-O3,那么如果禁用了优化,则生成的程序的性能将降低。 gcc文件指出 并不是所有的优化都由一个标志直接控制 会是这个问题还是我错过了其他的?

    • 问题内容: Netbeans每15-30分钟显示一次“ ”。从我从Google那里学到的信息来看,这似乎与类加载器泄漏或内存泄漏有关。 不幸的是,我发现的所有建议都与应用程序服务器有关,并且我不知道将其应用于Netbeans。(我什至不确定这是同一个问题) 我的应用程序有问题吗?我如何找到来源? 问题答案: 这是因为常量类加载。 Java将类字节码和所有常量(例如,字符串常量)存储在默认情况下不会

    • 我最近遇到了这个精彩的cpp2015演讲cppCon 2015:钱德勒·卡鲁斯“调整C:基准、CPU和编译器!哦,天哪!” 提到的防止编译器优化代码的技术之一是使用以下函数。 我在努力理解这一点。问题如下。 1)逃避比重击有什么好处? 2) 从上面的例子来看,clobber()似乎可以防止前面的语句(push_back)以这种方式进行优化。如果是这样,为什么下面的代码片段不正确? 如果这还不够混乱

    • 我最近发现,DRAMs中的比特可以被其中粒子的衰变或宇宙射线随机翻转。我想知道这种错误发生的频率。 不幸的是,我发现的最新统计数据来自1990年(来源),其中指出每128MB内存每月都会发生错误。 由于我找不到任何关于现代ram软错误率的最新统计数据,我尝试用java编写一个程序来测量4GB RAM上的软错误频率。我希望该程序能够检测到分配的4GB内存中的每个软错误,如果它没有以任何方式进行优化的

    • 所以我有一个函数,在小列表上执行得很好。它的功能是检查从序列中删除一个元素是否会使该序列成为严格的递增序列: 但我需要它来处理长达10万个元素的列表。我可以做什么样的优化来更快地工作?现在,在10万个元素的列表中,它的速度非常慢,一秒钟要处理几千个元素。

    • 问题内容: 首先,我想提一下,我有一个3 GB的内存。 我正在研究一种在节点上时间呈指数形式的算法,因此在代码中已经有了 生成列表中所有顶点的组合,然后我可以处理其中一种排列。 但是,当我为40个顶点运行程序时,它给出了内存错误。 有没有一种更简单的实现方式可以通过它生成顶点的所有组合而不会出现此错误。 问题答案: 尝试使用由排列生成的迭代器,而不是用它重新创建一个列表: 通过这样做,python