最近开始大量基于ARM-2D开发应用项目,为了达到最佳性能,我们使用了编译器的许多特殊技能,其中就包含了-Ofast优化,很不幸,一不小心踩坑了。
案发情况如下:
使用的MDK版本5.36,编译器6.16
优化选项配置如下:
不使用mcirolib
代码比较简单,读写一个Norflash数据:
bash
uint8_t sFLASH_SendByte(uint8_t byte)
{
uint32_t i;
i = 0;
while ((SPI_I2S_GetFlagStatus(sFLASH_SPI, SPI_I2S_FLAG_TXE) == RESET) && (i++ < 5000)); //set timeout
SPI_SendData(sFLASH_SPI, byte);
i = 0;
while ((SPI_I2S_GetFlagStatus(sFLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET) && (i++ < 5000)); //set timeout
return (uint8_t)SPI_GetData(sFLASH_SPI);
}
以前使用microlib库编译,运行良好,所以也没有在意问题,现在使用arm-2d,不使用microlib编译,运行,读数据错误了。一开始莫名奇妙的,采用各种组合验证,得出以下结论:
测试情况:
- 优化开-Ofast,使用microlib编译,代码中的i不用volatile修饰,功能正常
- 优化开-Ofast,不使用microlib编译,代码中的i不用volatile修饰,功能异常
- 优化开-O1,不使用microlib编译,代码中的i不用volatile修饰,功能正常
- 优化开-Ofast,不使用microlib编译,代码中的i用volatile修饰,功能正常
初步结论,看起来就是编译器-Ofast,并且不使用microlib库的时候,优化出了问题。代码逻辑上看起来没有任何问题(表面上如此,具体解释在这种情况不同的编译器有不同的说法,编译器说了算)。
追踪了一下反汇编代码:
确实发现了一个优化的问题,如图中所示,R6寄存器没做任何初始化,后面就和R0比较(对应代码i<5000),导致延时时间不够,寄存器数据读取错误。
后来和arm2d的作者讨论了一下,确定了结论:编译器的未定义行为或者说bug
参考:编译器的无副作用代码
为了验证这个问题,安装最新的MDK版本:
还是这一段代码,其他配置一样,编译,链接,下载,测试。运行正常,说明最新的编译器已经修正了这个不是bug的bug。我们看看它生成的代码:
这下子没有问题了,明显看到了对两个变量R6,R4的初始化,运行结果也正确了。
那么对于上面那一段代码,我们要如何来写,才能防止编译器的优化或者不确定行为呢?进而确保我们写出的代码具有健壮性和与编译器的无关性,下面两种改写都有效,共参考:
bash
uint8_t sFLASH_SendByte(uint8_t byte)
{
uint32_t i;
i = 0;
while ((SPI_I2S_GetFlagStatus(sFLASH_SPI, SPI_I2S_FLAG_TXE) == RESET) && (i++ < 5000)) __NOP(); //set timeout
SPI_SendData(sFLASH_SPI, byte);
i = 0;
while ((SPI_I2S_GetFlagStatus(sFLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET) && (i++ < 5000)) _NOP(); //set timeout
return (uint8_t)SPI_GetData(sFLASH_SPI);
}
这一种写法,在while后面增加一条_NOP()指令,阻止编译器对看似无用的i++的优化,保证不被未定义行为的优化。
bash
uint8_t sFLASH_SendByte(uint8_t byte)
{
for(i = 0;i < 1000;i++)
{
if ((sFLASH_SPI->SR & SPI_I2S_FLAG_TXE) != (uint16_t)RESET)
break;
}
SPI_SendData(sFLASH_SPI, byte);
for(i = 0;i < 1000;i++)
{
if ((sFLASH_SPI->SR & SPI_I2S_FLAG_RXNE) != (uint16_t)RESET)
break;
}
return (uint8_t)SPI_GetData(sFLASH_SPI);
这一种写法,用循环方式,明确的告诉编译器,i是有用的,也可以达到这个目的。
还有一种方案也可以解决这个问题,就是定义i变量的时候用volatile修正,但是一般不建议采用这种方式。
bash
volatile uint32_t i;
编译器的坑,很难对付,我们理解一下编译器的行为,站在编译器的角度去思考问题,平时写出更优质的代码,才能完全防止这种情况的发生。
文章原创,欢迎转载,请注明出处,未经书面允许,不得用于商业用途