在 C 中遍历列表时,我是否看到了优化错误?

Am I seeing an optimisation bug when iterating through a list in C?

我一直致力于将同事的软件库集成到我们更大的应用程序中。他一直在 gcc 4.9.3 -O0 下编写和测试他的库。这是用于报警系统的嵌入式软件。这个错误是在 -Os 优化下观察到的,我们正在使用直接 C(none 的 C++ 废话),也在 gcc 4.9.3 上。架构为ARM Cortex-M4F。

我在将该代码集成到更大的堆栈时遇到了问题。下面的代码最多应该遍历 GLOBAL_MAX_DEVICES,直到它在 table 中找到空闲的 space 以插入其条目:

RET_VALUES DEVICE_Add_To_New_Table( uint8_t *new_id, uint16_t unique_id )
{
    RET_VALUES ret_value = RET_OK;
    uint8_t pos = 0;

[...]
    else
    {
        debug_print( "A,", true, DEBUG_DEVELOP_MODE_PLAIN, DEBUG_TRACE );

        // See if the unique_id is already in the table (relearn)
        while(( unique_id != DEVICE_new_table.unique_id[pos] ) && ( pos < GLOBAL_MAX_DEVICES ))
        {
            debug_print( "B,", false, DEBUG_DEVELOP_MODE_PLAIN, DEBUG_TRACE );
            pos++;
        }
[...]

我们遇到的问题是循环迭代了 GLOBAL_MAX_DEVICES 次(当前为 13 次),而是迭代了 200 多次。序列 "B," 在 while(( unique_id != DEVICE_new_table.unique_id[pos] ) && ( pos < GLOBAL_MAX_DEVICES )) 循环中多次打印出来。

可以进行三项更改来解决此问题;

似乎只需要一个给定的修复程序即可解决该问题。

我最初认为问题可能归结为短路优化。但是,我可以确认 unique_id 参数不为零(在此示例中为 0x042b),并且 DEVICE_new_table 在结构的 unique_id 字段中初始化为全零。而且,在任何情况下,即使第一个参数为真,也应该评估后者;短路应该只适用于第一个为假的情况,这显然不是这种情况,至少最初不是,因为循环迭代了一段时间。

因此我很困惑为什么这个循环迭代了数百次(我假设最终会发生与内存地址的冲突,这会导致与 unique_id 的机会匹配,可能与堆栈中的值.) 在打印迭代器 pos 的循环中添加打印语句会导致循环 运行 超过 13,000 次。 13000比13大很多,我查了好几次

比较损坏示例(添加了额外的 printf 语句)和工作示例(也使用 printf)生成的机器代码表明,工作变体(布尔运算符中的参数颠倒)包括与迭代的比较13.

的最大值

损坏:

        debug_print( "A,", true, DEBUG_DEVELOP_MODE_PLAIN, DEBUG_TRACE );
    ea7a:   4822        ldr r0, [pc, #136]  ; (eb04 <DEVICE_Add_To_New_Table+0xb0>)
    ea7c:   f8df 80a4   ldr.w   r8, [pc, #164]  ; eb24 <DEVICE_Add_To_New_Table+0xd0>
    ea80:   2101        movs    r1, #1
    ea82:   2200        movs    r2, #0
    ea84:   2306        movs    r3, #6
    ea86:   f00b fef9   bl  1a87c <debug_print>

        // See if the unique_id is already in the table (relearn)
        while(( unique_id != DEVICE_new_table.unique_id[pos] ) && ( pos < GLOBAL_MAX_DEVICES ))
    ea8a:   2400        movs    r4, #0
    ea8c:   f838 3f02   ldrh.w  r3, [r8, #2]!
    ea90:   454b        cmp r3, r9
    ea92:   b2e7        uxtb    r7, r4
    ea94:   4626        mov r6, r4
    ea96:   f104 0401   add.w   r4, r4, #1
    ea9a:   d00c        beq.n   eab6 <DEVICE_Add_To_New_Table+0x62>
        {
            uart_printf(-1, "%d,", pos);
    ea9c:   4632        mov r2, r6
    ea9e:   491a        ldr r1, [pc, #104]  ; (eb08 <DEVICE_Add_To_New_Table+0xb4>)
    eaa0:   f04f 30ff   mov.w   r0, #4294967295
    eaa4:   f7f8 ff6c   bl  7980 <uart_printf>
            debug_print( "B,", false, DEBUG_DEVELOP_MODE_PLAIN, DEBUG_TRACE );
    eaa8:   2100        movs    r1, #0
    eaaa:   4818        ldr r0, [pc, #96]   ; (eb0c <DEVICE_Add_To_New_Table+0xb8>)
    eaac:   460a        mov r2, r1
    eaae:   2306        movs    r3, #6
    eab0:   f00b fee4   bl  1a87c <debug_print>
    eab4:   e7ea        b.n ea8c <DEVICE_Add_To_New_Table+0x38>
            pos++;
        }

工作(修剪不必要的部分),注意 eab2 处的 cmp r4, #13 语句,它在损坏的样本中不存在:

        debug_print( "A,", true, DEBUG_DEVELOP_MODE_PLAIN, DEBUG_TRACE );
    ea7a:   4845        ldr r0, [pc, #276]  ; (eb90 <DEVICE_Add_To_New_Table+0x13c>)
    ea7c:   f8df 8148   ldr.w   r8, [pc, #328]  ; ebc8 <DEVICE_Add_To_New_Table+0x174>
    ea80:   2101        movs    r1, #1
    ea82:   2200        movs    r2, #0
    ea84:   2306        movs    r3, #6
    ea86:   f00b ff4d   bl  1a924 <debug_print>
    ea8a:   4647        mov r7, r8
    ea8c:   2400        movs    r4, #0

        // See if the unique_id is already in the table (relearn)
        while(( pos < GLOBAL_MAX_DEVICES ) && ( unique_id != DEVICE_new_table.unique_id[pos] ))
    ea8e:   f837 3f02   ldrh.w  r3, [r7, #2]!
    ea92:   454b        cmp r3, r9
    ea94:   b2e5        uxtb    r5, r4
    ea96:   d00f        beq.n   eab8 <DEVICE_Add_To_New_Table+0x64>
        {
            uart_printf(-1, "%d,", pos);
    ea98:   4622        mov r2, r4
    ea9a:   493e        ldr r1, [pc, #248]  ; (eb94 <DEVICE_Add_To_New_Table+0x140>)
    ea9c:   f04f 30ff   mov.w   r0, #4294967295
    eaa0:   f7f8 ff6e   bl  7980 <uart_printf>
            debug_print( "B,", false, DEBUG_DEVELOP_MODE_PLAIN, DEBUG_TRACE );
    eaa4:   2100        movs    r1, #0
    eaa6:   483c        ldr r0, [pc, #240]  ; (eb98 <DEVICE_Add_To_New_Table+0x144>)
    eaa8:   460a        mov r2, r1
    eaaa:   2306        movs    r3, #6
    eaac:   3401        adds    r4, #1
    eaae:   f00b ff39   bl  1a924 <debug_print>

        // See if the unique_id is already in the table (relearn)
        while(( pos < GLOBAL_MAX_DEVICES ) && ( unique_id != DEVICE_new_table.unique_id[pos] ))
    eab2:   2c0d        cmp r4, #13
    eab4:   d1eb        bne.n   ea8e <DEVICE_Add_To_New_Table+0x3a>
    eab6:   4625        mov r5, r4
        {
            uart_printf(-1, "%d,", pos);
            debug_print( "B,", false, DEBUG_DEVELOP_MODE_PLAIN, DEBUG_TRACE );
            pos++;
        }

我觉得一个程序员责备他的工具就是承认失败,但我真的看不出这除了编译器错误之外还有什么。如果有人有任何想法,我将不胜感激。

如评论中所述,这是您程序中的错误,即数组的简单 out-of-bounds 访问。

给定 GLOBAL_MAX_DEVICES = 13,那么您最终会访问 DEVICE_new_table.unique_id[13] 甚至更多。在调试版本中,您可能在那里有一个零,它 "works" 偶然。然后在发布中你得到一些 non-zero 值并且循环变得混乱,short-circuiting 离开循环终止条件。

此外,如果您的代码包含未定义的行为,例如 out-of-bounds 访问,您可能会在优化器尝试循环展开等操作时生成非常奇怪的机器代码。例如,编译器可能会推理为:"okay this first check is always true since all items in the array are zero, so we can remove the check after && entirely".

这可以通过以更简单的方式编写循环来解决。假设你想在循环(?)之后保留 pos 的值,那么这样做:

uint8_t pos = 0;
...
for(; pos<GLOBAL_MAX_DEVICES; pos++)
{
  if(unique_id != DEVICE_new_table.unique_id[pos])
    break;
  debug_print(...);
}