最近,我将项目从gcc 4.3升级到gcc 5.5。之后,我看到后增量运算符中的行为更改导致了我的项目中的问题。我使用全局变量作为控制变量。例如,请考虑以下示例程序:
int i = 0;
int main()
{
int x[10];
x[i++] = 5; ===> culprit
return 0;
}
在上面的代码段中,i
的值只有在5
分配给x[0]
后才会增加,这样才能保证x[0]
分配了正确的有效值在i
递增之前。
现在问题出现了,我看到在移动到gcc 5.5之后,汇编指令已经改变,并且即使在赋值发生之前,i的值也会增加。上述代码段的汇编说明:
Dump of assembler code for function main():
6 {
0x0000000000400636 <+0>: push %rbp
0x0000000000400637 <+1>: mov %rsp,%rbp
7 int x[10];
8
9 x[i++] = 1;
0x000000000040063a <+4>: mov 0x200a00(%rip),%eax # 0x601040 <i>
0x0000000000400640 <+10>: lea 0x1(%rax),%edx
0x0000000000400643 <+13>: mov %edx,0x2009f7(%rip) # 0x601040 <i> ====> i gets incremented here
0x0000000000400649 <+19>: cltq
0x000000000040064b <+21>: movl $0x5,-0x30(%rbp,%rax,4) =====> x[0] is assigned value here
10
11 return 0;
0x0000000000400653 <+29>: mov $0x0,%eax
12
13 }
0x0000000000400658 <+34>: pop %rbp
0x0000000000400659 <+35>: retq
由于上面的程序集,使用变量i
的进程中的另一个线程开始从全局数组中读取不正确的值。
现在使用gcc 4.3编译相同的代码时,遵循我理解的行为,即首先分配值,然后递增i
。使用gcc 4.3对相同代码段的汇编指令:
Dump of assembler code for function main():
5 int main()
0x00000000004005da <+0>: push %rbp
0x00000000004005db <+1>: mov %rsp,%rbp
6 {
7 int x[10];
8
9 x[i++] = 1;
0x00000000004005de <+4>: mov 0x200a64(%rip),%edx # 0x601048 <i>
0x00000000004005e4 <+10>: movslq %edx,%rax
0x00000000004005e7 <+13>: movl $0x5,-0x30(%rbp,%rax,4) ======> x[0] gets assigned here
0x00000000004005ef <+21>: lea 0x1(%rdx),%eax
0x00000000004005f2 <+24>: mov %eax,0x200a50(%rip) # 0x601048 <i> ======> i gets incremented here
10
11 return 0;
0x00000000004005f8 <+30>: mov $0x0,%eax
12
13 }
0x00000000004005fd <+35>: leaveq
0x00000000004005fe <+36>: retq
我想知道这是否是新编译器的预期行为?有没有我可以切换回旧行为的开关?或者这是新编译器中存在的错误吗?
任何帮助或线索都将受到赞赏。
注意:由于性能问题,我希望在阅读i
期间避免锁定。上述代码中的culprit
行在锁内执行。因此,只有一个线程可以在任何时候更新i
,但由于编译器中汇编指令的更改,引入了竞争条件而没有任何代码更改。
编辑1:我知道存在锁定问题,我也将其作为一个选项保留,但我真正想知道的是,如果有任何开关或标志用于我可以回到旧的行为。代码库非常庞大,我将不得不通过整个代码库来检查代码中其他位置是否存在类似的问题。因此,恢复原有的行为将会挽救生命。
答案 0 :(得分:14)
你误解了后增量优惠的保证。它保证将使用旧值x
计算存储i
的位置。绝对不保证在更新值存储在x
之前存储i
。
编译器可以自由地将代码转换为:
int temp = i;
i = temp+1;
x[temp] = 5;
此外,如果您有一个线程修改i
而另一个线程读取i
,则无法保证其他线程将看到哪个值。除非i
为std::atomic
,否则您甚至无法保证会看到新值或旧值。
鉴于您尝试以协调方式更新i
和x
,您必须锁定。
编译器甚至可以将您的代码转换为:
i = i + 1;
// <<<<
x[i-1] = 5;
如果另一个线程跳入并在标记为i
的位置修改<<<<
,这会很有趣。
答案 1 :(得分:3)
当从不同的线程读取和写入相同的变量时,必须使用某种同步机制,例如mutex
或atomic
变量(如果您只想同步一个变量)。在这个意义上,x86以外的平台要宽容得多。
此外,当修改多个变量时,您需要确保发生在 memory ordering语义之前,以便其他线程能够&#34;参见&#34 ;按时间顺序排列新值。使用mutex
自动确保获取释放语义(即,在释放互斥锁之前在一个线程中发生的任何事情对于再次锁定它的线程是可见的)。如果没有内存排序,你就不能保证线程会及时看到彼此的变化(或根本没有)。
atomic
还附带内存排序,但仅适用于变量本身。
没有任何内存同步,编译器会假设没有其他线程在运行,并且可以按照自己喜欢的方式自由地命令内存访问。只要当前线程中没有可观察到的效果,它就可以在增量部分之前,之后或任何地方移动。
如果您想了解更多,我建议您在内存订购时观看Herb's excellent talk。