单线程代码中是否需要Compiler barrier?链表优化后断言失败排查
单线程下Linux风格双向链表-O2优化断言失败问题分析
问题描述
- 实现了借鉴Linux的双向链表,关闭编译器优化时运行正常,开启
-O2后断言触发失败 - 核心逻辑:向链表添加元素,循环遍历非空链表,每次取出首元素后断言该元素仍在链表中,随后删除元素
- 添加编译屏障
asm volatile("": : :"memory");后异常消失 - 观察编译后的汇编代码,主函数无分支逻辑,直接调用
__assert_fail - 疑惑:单线程代码为何需要编译屏障
代码片段
#include <stdlib.h> #include <stddef.h> #include <stdio.h> #include <assert.h> struct list_elem { struct list_elem *next; struct list_elem *prev; }; struct list { struct list_elem *next; struct list_elem *prev; }; static inline void init_list_head(struct list *list) { list->next = (struct list_elem *)list; list->prev = (struct list_elem *)list; } static inline void init_list_elem(struct list_elem *elem) { elem->next = NULL; elem->prev = NULL; } static inline void *list_entry(struct list_elem *elem, size_t offset) { return (char *)(elem) - offset; } static inline int list_empty(struct list *list) { return list->next == (struct list_elem *)list; } static inline struct list_elem *first_list_elem(struct list *list) { return list->next; } static inline int elem_not_in_list(struct list_elem *elem) { return elem->next == NULL; } static inline void __list_add(struct list_elem *elem, struct list_elem *prev, struct list_elem *next) { next->prev = elem; elem->next = next; elem->prev = prev; prev->next = elem; } static inline void list_add(struct list_elem *elem, struct list *list) { __list_add(elem, (struct list_elem *)list, list->next); } static inline void list_del(struct list_elem *elem) { elem->prev->next = elem->next; elem->next->prev = elem->prev; elem->next = NULL; elem->prev = NULL; } struct record { int value; struct list_elem link; }; struct record r1; struct list li; int main(int argc, char *argv[]) { init_list_head(&li); init_list_elem(&r1.link); r1.value = 1; list_add(&r1.link, &li); while (!list_empty(&li)) { struct record *r = list_entry(first_list_elem(&li), offsetof(struct record, link)); assert(!elem_not_in_list(&r->link)); list_del(&r->link); r1.value += argc; // compiler barrier //asm volatile("": : :"memory"); } return r1.value; }
编译信息
gcc version 11.2.0 (GCC) Target: arm-linux-musleabihf
编译命令:
gcc -O2 -o testbarrier testbarrier.c
编译后的汇编代码
00010290 <main>: 10290: e59f1030 ldr r1, [pc, #48] ; 102c8 <main+0x38> 10294: e3a0c000 mov ip, #0 10298: e2800001 add r0, r0, #1 1029c: e92d4010 push {r4, lr} 102a0: e3a02057 mov r2, #87 ; 0x57 102a4: e5810008 str r0, [r1, #8] 102a8: e5811000 str r1, [r1] 102ac: e5811004 str r1, [r1, #4] 102b0: e581c00c str ip, [r1, #12] 102b4: e581c010 str ip, [r1, #16] 102b8: e59f300c ldr r3, [pc, #12] ; 102cc <main+0x3c> 102bc: e59f100c ldr r1, [pc, #12] ; 102d0 <main+0x40> 102c0: e59f000c ldr r0, [pc, #12] ; 102d4 <main+0x44> 102c4: ebffffe8 bl 1026c <__assert_fail@plt> 102c8: 0002103c .word 0x0002103c 102cc: 000104dc .word 0x000104dc 102d0: 000104b0 .word 0x000104b0 102d4: 000104c0 .word 0x000104c0
问题根源
核心问题是非法类型转换导致的未定义行为:
- Linux内核的链表头与链表元素使用同一结构体类型(
struct list_head),因此指针转换合法;但你的代码中,struct list和struct list_elem是两个不同的结构体类型,尽管成员布局一致,强制转换(struct list_elem *)list仍违反C标准类型规则。 - 编译器开启-O2优化时,会基于类型别名规则做优化:认为
struct list *和struct list_elem *指向不同内存区域,不会互相干扰,因此错误地缓存了r->link.next的初始值(NULL),忽略了list_add对它的修改,最终判定断言条件不成立。
修复方案
将链表头类型与链表元素类型统一,消除非法类型转换,示例修改如下:
// 移除独立的struct list,直接用struct list_elem作为链表头 struct list_elem { struct list_elem *next; struct list_elem *prev; }; // 初始化链表头的函数适配新类型 static inline void init_list_head(struct list_elem *list) { list->next = list; list->prev = list; } // 全局链表头改为struct list_elem类型 struct list_elem li; // 适配其他函数的参数类型 static inline int list_empty(struct list_elem *list) { return list->next == list; } static inline void list_add(struct list_elem *elem, struct list_elem *list) { __list_add(elem, list, list->next); } // main函数中的初始化代码修改 int main(int argc, char *argv[]) { init_list_head(&li); init_list_elem(&r1.link); r1.value = 1; list_add(&r1.link, &li); while (!list_empty(&li)) { struct record *r = list_entry(first_list_elem(&li), offsetof(struct record, link)); assert(!elem_not_in_list(&r->link)); list_del(&r->link); r1.value += argc; } return r1.value; }
总结
单线程代码确实不需要内存屏障,问题出在代码本身的未定义行为——非法类型转换导致编译器优化时做出错误假设。修复类型一致性后,编译器能正确跟踪内存修改,断言即可正常通过。
内容的提问来源于stack exchange,提问作者ErwinP
相关产品推荐
相关产品推荐

