cer*_*ka2 32 c c++ arrays gcc compiler-optimization
我有以下 C/C++ 代码片段:
#define ARRAY_LENGTH 666
int g_sum = 0;
extern int *g_ptrArray[ ARRAY_LENGTH ];
void test()
{
    unsigned int idx = 0;
    // either enable or disable the check "idx < ARRAY_LENGTH" in the while loop
    while( g_ptrArray[ idx ] != nullptr /* && idx < ARRAY_LENGTH */ )
    {
        g_sum += *g_ptrArray[ idx ];
        ++idx;
    }
    return;
}
当我使用版本 12.2.0 中的 GCC 编译器编译上述代码时,并选择-Os两种情况:
g_ptrArray[ idx ] != nullptrg_ptrArray[ idx ] != nullptr && idx < ARRAY_LENGTH我得到以下程序集:
test():
        ldr     r2, .L4
        ldr     r1, .L4+4
.L2:
        ldr     r3, [r2], #4
        cbnz    r3, .L3
        bx      lr
.L3:
        ldr     r3, [r3]
        ldr     r0, [r1]
        add     r3, r3, r0
        str     r3, [r1]
        b       .L2
.L4:
        .word   g_ptrArray
        .word   .LANCHOR0
g_sum:
        .space  4
正如您所看到的,程序集确实如此!不!idx根据值对变量进行任何检查ARRAY_LENGTH。
这怎么可能?编译器如何为这两种情况生成完全相同的程序集,并忽略idx < ARRAY_LENGTH代码中存在的条件?向我解释一下规则或过程,编译器如何得出可以完全删除条件的结论。
编译器资源管理器中显示的输出程序集(看到两个程序集是相同的):
while 条件是g_ptrArray[ idx ] != nullptr:
while 条件是g_ptrArray[ idx ] != nullptr && idx < ARRAY_LENGTH:
注意:如果我将条件的顺序交换为idx < ARRAY_LENGTH && g_ptrArray[ idx ] != nullptr,则输出程序集包含对 的值的检查,idx如您在此处看到的:https: //godbolt.org/z/fvbsTfr9P。
Mar*_*lli 60
越界访问数组是未定义的行为,因此编译器可以假设它永远不会在表达式的 LHS 中发生&&。然后跳过一些环节(优化),注意到由于ARRAY_LENGTH是数组的长度,RHS 条件必须成立(否则 UB 会出现在 LHS 中)。因此你看到的结果。
正确的检查是idx < ARRAY_LENGTH && g_ptrArray[idx] != nullptr. 这将避免 RHS 上任何未定义行为的可能性,因为必须首先评估 LHS,并且除非 LHS 为 true,否则不会&&评估 RHS(在 C 和 C++ 中,保证运算符以这种方式运行)。
即使是潜在的未定义行为也可能会做出这样令人讨厌的事情!
C 标准 (C17 6.5.6 \xc2\xa78) 规定我们不能在数组之外进行指针算术,也不能在数组之外访问它 - 这样做是未定义的行为,任何事情都可能发生。
\n因此,严格来说,数组越界检查是多余的,因为循环条件为“在数组中发现空指针时停止”。如果g_ptrArray[ idx ]是越界访问,您将调用未定义的行为,因此理论上该程序此时会被烘烤。因此无需计算 的正确操作数&&。(你可能知道,&&具有严格的从左到右评估。)编译器可以假设访问始终位于已知大小的数组内。
我们可以通过添加一些使编译器无法预测代码的内容来使编译器恢复正常:
\nint** I_might_change_at_any_time = g_ptrArray;\nvoid test2()\n{\n    unsigned int idx = 0;\n     \n\n    // check for idx value is NOT present in code\n    while( I_might_change_at_any_time[ idx ] != nullptr && idx < ARRAY_LENGTH)\n    {\n        g_sum += *g_ptrArray[ idx ];\n        ++idx;\n    }\n}\n这里指针充当“中间人”。它是一个具有外部链接的文件范围变量,因此它可能随时更改。编译器不能再假设它始终指向g_ptrArray. 现在, 的左操作数可以&&成为明确定义的访问。因此 gcc 现在向汇编器添加了越界检查:
        cmp     QWORD PTR [rdx+rax*8], 0\n        je      .L6\n        cmp     eax, 666\n        je      .L6\n解释:正如Marco Bonelli所记录的,编译器假设编写第一个测试的程序员g_ptrArray[idx] != nullptr知道该代码已定义行为,因此它假设idx在适当的范围内,即:idx < ARRAY_LENGTH。由此可见,下一个测试&& idx < ARRAY_LENGTH是冗余idx的< ARRAY_LENGTH,因此可以省略该代码。
目前尚不清楚这种范围分析发生在哪里,也不清楚编译器是否也可以警告程序员 a ,它标记或redundant test elided中潜在编程错误的方式if (a = b) ...a = b << 1 + 2;
与编译器不同,程序员是人,也会犯错误。即使是 10 倍的程序员有时也会犯愚蠢的错误,编译器不应该假设程序员总是对的,除非他们明确地吹嘘这一点,如if ((a = b)) ...
测试顺序不正确g_ptrArray[idx] != nullptr,idx < ARRAY_LENGTH在代码审查中应该很明显。相反,如果假设合法,idx < ARRAY_LENGTH则冗余的事实对于代码的人类读者来说并不明显。g_ptrArray[idx] != nullptr编译器假设程序员知道g_ptrArray[idx] != nullptr可以执行该操作,因此假设它idx在适当的范围内,并推断第二个测试是多余的。这是反常的:如果程序员足够精明,能够正确地假设 idx 始终在正确的范围内,那么他们肯定不会编写多余的测试。相反,如果他们犯了错误并以错误的顺序编写了测试,那么标记冗余代码将有助于修复明显的错误。
当编译器变得足够聪明,能够检测到这种冗余时,这种级别的分析应该对程序员有利,并有助于检测编程错误,而不是使调试变得比现在更困难。
| 归档时间: | 
 | 
| 查看次数: | 4196 次 | 
| 最近记录: |