从一个crash问题展开,探索gcc编译优化细节
阿里妹导读
背景:一个平平无奇的crash
一、寻找元凶
1.1 一顿分析猛如虎
void* readTileContentIndexCallback(TileContentIndexStruct *tileIndexData, int32_t count) {TileContentIndex* tileContentIndexList = new TileContentIndex[count];for (int32_t index = 0; index < count; index++) {TileContentIndexStruct &inData = tileIndexData[index];TileContentIndex &outData = tileContentIndexList[index];outData.urID = inData.urCode;outData.adcode = inData.adcode;outData.level = inData.levelNumber;outData.southWestTileId = inData.southWestTileId;outData.numRows = inData.numRows;outData.numColumns = inData.numColumns;outData.tileIndex = inData.tileContentIndex;}return tileContentIndexList;}
这里面赋值出了问题,导致上层访问数据的时候地址非法了。
1.2 机智的宗翰
Optimize yet more. -O3 turns on all optimizations specified by -O2 and also turns on the following optimization flags: -fgcse-after-reload -fipa-cp-clone -floop-interchange -floop-unroll-and-jam -fpeel-loops -fpredictive-commoning -fsplit-loops -fsplit-paths -ftree-loop-distribution -ftree-partial-pre -funswitch-loops -fvect-cost-model=dynamic -fversion-loops-for-strides
1.3 问题模拟复现
g++ -O2 -S -o main2.s main.cpp // 这个命令可以生成O2下的汇编文件
1.4 编译优化选项排查
gcc/g++ -Q -O2 --help=optimizers
-ftree-loop-distribute-patterns -ftree-loop-vectorize -finline-functions -ftree-slp-vectorize
-floop-interchange -floop-unroll-and-jam -ftree-loop-distribution -funswitch-loops -fversion-loops-for-strides -ftree-loop-distribute-patterns -ftree-loop-vectorize
g++ -O3 -fno-tree-loop-vectorize -S -o main3t.s main.cpp // 打开O3,但是关闭tree-loop-vectorize
1.5 了解-ftree-loop-vectorize
Perform loop vectorization on trees. This flag is enabled by default at -O2 and by -ftree-vectorize, -fprofile-use, and -fauto-profile.
for (int i = 0; i < 16; i++) {a[i] = b[i];}
可能会被优化成:
for (int i = 0; i < 16; i+=4) {a[i] = b[i];a[i + 1] = b[i + 1];a[i + 2] = b[i + 2];a[i + 3] = b[i + 3];}
通过增加循环的步长减少了循环体执行次数,提高代码效率。上述的例子中一个向量单元内部做了四个赋值,而优化之前需要执行四次循环才可以。
1.6 初获战果&战术性撤退
二、再探作案细节
2.1 重写demo复现问题
struct TileContentIndexStruct {int32_t urCode; // 4字节int32_t adcode; // 4字节int32_t levelNumber; // 4字节int32_t southWestTileId; // 4字节int32_t numRows; // 4字节int32_t numColumns; // 4字节uint8_t* tileContentIndex; // 8字节int32_t dataSize; // 4字节//64位系统8字节对齐,填充4字节}; // 共40字节struct TileContentIndex {uint16_t urID; // 2字节uint16_t level; // 2字节uint32_t adcode; // 4字节uint32_t southWestTileId; // 4字节uint16_t numRows; // 2字节uint16_t numColumns; // 2字节(至此正好8字节对齐,无需填充)uint8_t* tileIndex; // 8字节}; // 共24字节int g_rowCount = 10;TileContentIndex g_tileContentIndexList[10]; // 被复制TileContentIndexStruct g_tileContentVector[10]; // 源数据void* readTileContentIndexCallback(TileContentIndexStruct *tileIndexData, int32_t count){for (int32_t index = 0; index < count; index++) {TileContentIndexStruct &inData = tileIndexData[index];TileContentIndex &outData = g_tileContentIndexList[index];outData.urID = (uint16_t)inData.urCode;outData.adcode = (uint32_t)inData.adcode;outData.level = (uint16_t)inData.levelNumber;outData.southWestTileId = (uint32_t)inData.southWestTileId;outData.numRows = (uint16_t)inData.numRows;outData.numColumns = (uint16_t)inData.numColumns;outData.tileIndex = inData.tileContentIndex;}return g_tileContentIndexList;}// 注:readTileContentIndexCallback调用时入参传的是g_tileContentVector和g_rowCount
2.2 对比优化前后的汇编
_Z28readTileContentIndexCallbackP22TileContentIndexStructi:.LFB48:.cfi_startproccmp w1, 0 // w1寄存器的值和0比较(w1对应入参count)ble .L2 // 比较结果w1 <= 0则跳转到.L2mov x2, x0 // x2 = x0 (x0对应入参tileIndexData)adrp x3, .LANCHOR0 // 将.LANCHOR0高地址(低12bit清零后的地址)load到x3寄存器add x3, x3, :lo12:.LANCHOR0 // 将.LANCHOR0低12位加到x3中,至此x3拥有了完整的.LANCHOR0,即全局变量 g_tileContentIndexList 的地址sub w1, w1, #1 // w1减一后存入w1,w1是x1的低32bitadd x1, x1, x1, lsl 2 // 将寄存器x1的值与寄存器x1左移2位的值相加,并将结果存储回寄存器x1. 相当于将寄存器x1的值乘以5add x0, x0, 40 // x0 = x0 + 40add x0, x0, x1, lsl 3 // x0 = (x1 << 3) + x0// 综合这4行相当于x0 = (x1 - 1) * 40 + (x0 + 40) = x1 * 40 + x0.L3:ldr w1, [x2] // w1 = tileIndexData->urCode,将寄存器x2指向的地址的值加载到寄存器w1strh w1, [x3] // 将寄存器w1的值存储到寄存器x3指向的地址(因为强转成了uint16_t所以是半字存储,只存低16bit)ldr w1, [x2, 4] // w1 = tileIndexData->adcodestr w1, [x3, 4] // [x3 + 4] = w1ldr w1, [x2, 8] // w1 = tileIndexData->levelNumberstrh w1, [x3, 2] // g_tileContentIndexList.level = (uint16_t)inData.levelNumberldr w1, [x2, 12] // w1 = tileIndexData->southWestTileIdstr w1, [x3, 8] // 存到 g_tileContentIndexList.southWestTileIdldr w1, [x2, 16] // tileIndexData->numRowsstrh w1, [x3, 12] // 存到 g_tileContentIndexList.numRowsldr w1, [x2, 20] // tileIndexData->numColumnsstrh w1, [x3, 14] // 存到 g_tileContentIndexList.numColumnsldr x1, [x2, 24] // x1 = tileIndexData->tileContentIndexstr x1, [x3, 16] // 存到 g_tileContentIndexList.tileIndexadd x2, x2, 40 // tileIndexData++(sizeof(TileContentIndexStruct) = 40)add x3, x3, 24 // g_tileContentIndexList++ (sizeof(TileContentIndex) = 24)cmp x2, x0 // 比较x2和x0bne .L3 //若x2不等于x0则跳转到.L3继续循环.L2:adrp x0, .LANCHOR0add x0, x0, :lo12:.LANCHOR0 // 两行指令将.LANCHOR0的地址赋给寄存器x0ret // 返回.cfi_endproc.LFE48:.size _Z28readTileContentIndexCallbackP22TileContentIndexStructi, .-_Z28readTileContentIndexCallbackP22TileContentIndexStructi.align 2.global _Z15readTileContentRiPFPvP22TileContentIndexStructiE.type _Z15readTileContentRiPFPvP22TileContentIndexStructiE, %function
2.3 详解优化后的汇编
2.3.1 感受循环向量化优化的威力
2.3.2 了解汇编指令背后的寄存器逻辑
N Set to 1 when the result of the operation is negative, cleared to 0 otherwise. Z Set to 1 when the result of the operation is zero, cleared to 0 otherwise. C Set to 1 when the operation results in a carry, or when a subtraction results in no borrow, cleared to 0 otherwise. V Set to 1 when the operation causes overflow, cleared to 0 otherwise.
For a subtraction, including the comparison instruction CMP, C is set to 0 if the subtraction produced a borrow (that is, an unsigned underflow), and to 1 otherwise.
(gdb) i reg cpsr
2.3.3 了解NEON
2.3.4 继续看优化后涉及NEON的汇编
.L5: // 之前已经做了mov x2, x0,所以此时x2中存的是入参&g_tileContentVector的地址ldr d0, [x2] // 将x2为起始地址的8字节内存存入d0,即d0存了 g_tileContentVector[0].urCode(4字节), g_tileContentVector[0].adcode(4字节)这2个变量的值ldr d1, [x2, 8] // d1存 g_tileContentVector[0].levelNumber(4字节),g_tileContentVector[0].southWestTileId(4字节)zip1 v0.2s, v0.2s, v1.2s // 见下图
zip1 v0.2s, v0.2s, v1.2s效果如下(注:[0].adcode表示g_tileContentVector[0].adcode):
.L5:...ldr d1, [x2, 40]ldr d2, [x2, 48]zip1 v1.2s, v1.2s, v2.2sins v0.d[1], v1.d[0]xtn v1.4h, v0.4s
同理,.L5片段2,zip1 v1.2s, v1.2s, v2.2s执行之后寄存器v1内容如下(数组g_tileContentVector一个成员size是40):
.L5:...ldr d0, [x2, 80]ldr d2, [x2, 88]zip1 v0.2s, v0.2s, v2.2sldr d2, [x2, 120]ldr d3, [x2, 128]zip1 v2.2s, v2.2s, v3.2smov v0.8b, v0.8bins v0.d[1], v2.d[0]xtn v0.4h, v0.4s
显然,[x2, 80]和[x2, 120]分别是g_tileContentVector[2]和g_tileContentVector[3],因此最终xtn后结果为:
.L5:...mov x7, x4 // 将地址&g_tileContentIndexList存入x7str s1, [x7], 24 // str s1, [x7] 后令 x7 = x7 + 24st1 {v1.s}[1], [x7]str s0, [x4, 48]add x3, x4, 72st1 {v0.s}[1], [x3]
注意,在进入.L5之前,即.LFB48的结尾处,我们已经将全局变量g_tileContentIndexList的地址存入了x4中,它正是出参数组。注意出参数组成员的类型是struct TileContentIndex,大小是24字节。因此片段4执行效果如下,g_tileContentIndexList[0]~g_tileContentIndexList[3]四个元素的urID和level值都完成了赋值:
.L5:...ldr d2, [x2, 40]ldr d0, [x2, 48]zip2 v2.2s, v2.2s, v0.2sldr d1, [x2, 80]ldr d0, [x2, 88]zip2 v1.2s, v1.2s, v0.2sldr d0, [x2, 120]ldr d3, [x2, 128]zip2 v0.2s, v0.2s, v3.2sldr d3, [x2]ldr d4, [x2, 8]zip2 v3.2s, v3.2s, v4.2s
该片段还是从入参数组g_tileContentVector[0]~g_tileContentVector[3]中取数据,我们仅以最后三行为例进行说明即可,其他同理,其效果如下,
.L5:...str d3, [x4, 4]str d2, [x4, 28]str d1, [x4, 52]str d0, [x4, 76]
因为出参的adcode和southWestTileId与入参一样都是uint32_t类型,因此不需要做类似xtn的操作,直接str即可,以str d3, [x4, 4]为例,直接将d3的内容存入了g_tileContentIndexList[0] + 4字节的内存中,因为它的前两个分量urID和level一共占用了4字节,因此需要从x4+4的地方开始存,d3寄存器为8字节,这一条指令就完成了g_tileContentIndexList[0].adcode和g_tileContentIndexList[0].southWestTileId两个变量的赋值,这就是NEON的魅力!片段6执行后g_tileContentIndexList[0]~g_tileContentIndexList[3]这四个元素的前4个分量就都完成赋值了。
.L5:...ldr d1, [x2, 56]ldr d0, [x2, 16]ins v0.d[1], v1.d[0]xtn v1.4h, v0.4sldr d2, [x2, 136]ldr d0, [x2, 96]ins v0.d[1], v2.d[0]xtn v0.4h, v0.4sstr s1, [x4, 12]add x3, x4, 36st1 {v1.s}[1], [x3]str s0, [x4, 60]add x3, x4, 84st1 {v0.s}[1], [x3]
这里又是熟悉的操作,因为出参使用uint16_t类型的numRows和numColumns接入参uint32_t类型的对应变量,因此又有了窄指令xtn,因为不涉及顺序交换,因此没有使用zip1和zip2指令。执行后g_tileContentIndexList[0]~g_tileContentIndexList[3]的成员numRows和numColumns就都完成了赋值,不再赘述。
.L5:...ldr x8, [x6, 32]ldr x7, [x6, 40]ldr x3, [x6, 48]ldr x9, [x6, 24]str x9, [x4, 16]str x8, [x4, 40]str x7, [x4, 64]str x3, [x4, 88]
这里主要是从x6寄存器所示地址附近内存加载数据,存入x4寄存器附近内存中。x6是入参g_tileContentVector的地址,在.LFB48最后一行执行了mov x6, x0将x0赋值给了x6,x0就是第一个入参。x4上面已经说过是出参g_tileContentIndexList的地址。因此这四个ldr和str就是给g_tileContentIndexList[0]~g_tileContentIndexList[3]的最后一个成员uint8_t* tileIndex赋值了。数据源来自g_tileContentVector[0]~g_tileContentVector[3]的uint8_t* tileContentIndex成员。
struct TileContentIndexStruct {int32_t urCode; // 4字节int32_t adcode; // 4字节int32_t levelNumber; // 4字节int32_t southWestTileId; // 4字节int32_t numRows; // 4字节int32_t numColumns; // 4字节uint8_t* tileContentIndex; // 8字节int32_t dataSize; // 4字节//64位系统8字节对齐,填充4字节}; // 共40字节
因此
struct TileContentIndex {uint16_t urID; // 2字节uint16_t level; // 2字节uint32_t adcode; // 4字节uint32_t southWestTileId; // 4字节uint16_t numRows; // 2字节uint16_t numColumns; // 2字节(至此正好8字节对齐,无需填充)uint8_t* tileIndex; // 8字节}; // 共24字节
出参g_tileContentIndexList[0].tileIndex~g_tileContentIndexList[3].tileIndex对应的内存正是[x4, 16],[x4, 40],[x4, 64],[x6, 88],没有问题。
三、验证与结案
3.1 修改问题汇编代码
cmp x6, x10bne .L5
因此x10赋值指令 add x10, x0, x10, lsl 5(x10 = x0 + 64,其中 64 = 8 * 8,也是把size = 40当成了8)也应该修正一下,我们将其改成 add x10, x0, 320
3.2 编译器横向对比
3.2.1 clang没问题
3.2.2 高版本gcc也没问题
3.3 结论
四、未完待续
4.1 编译器优化
4.2 ARM芯片架构指令
4.3 写在最后
附件地址:
https://files.alicdn.com/tpsservice/c9c5f648084bc821509305041ad99bd8.zip
欢迎加入【阿里云开发者公众号】读者群