ARMv7 Linux下Clang++ O2优化时如何标记两指针指向同一内存
问题
我有一个类,包含两个指向外部内存的指针p_DataUser和p_DataUserWrite。多数情况下,这两个指针指向同一块内存,p_DataUser用于读取该内存,p_DataUserWrite用于写入该内存。
当前出现一种场景:先通过p_DataUserWrite写入内存,再通过p_DataUser读取内存,但使用Clang++针对ARMv7 Linux平台以O2优化级别编译时,出现了先读取内存、后写入内存的乱序问题。改用p_DataUser指针进行写入时问题消失,但这不符合项目规范。
此问题仅在ARMv7 Linux平台且优化级别≥O2时出现,x86_64 Linux及x64 Windows平台下用O2及更高优化级别编译均无问题。
示例代码
*((unsigned int *)IN_Class.p_DataUserWrite) = 0x12345678; unsigned short In_Word = *((unsigned short *)IN_Class.p_DataUser + 0); unsigned short Out_Word = (unsigned short)((In_Word >> 8) | (In_Word << 8)); In_Word = *((unsigned short *)IN_Class.p_DataUser + 1); *((unsigned short *)OUT_CLass.p_DataUserWrite + 1) = Out_Word; Out_Word = (In_Word >> 8) | (In_Word << 8); *((unsigned short *)OUT_CLass.p_DataUserWrite) = Out_Word;
编译生成的ARM汇编
; *((unsigned int *)IN_Class.p_DataUserWrite) = 0x12345678; 1f2b8: e5923034 ldr r3, [r2, #0x34] 1f2bc: e5920030 ldr r0, [r2, #0x30] ; *((unsigned short *)OUT_CLass.p_DataUserWrite + 1) = Out_Word; 1f2c0: e5911030 ldr r1, [r1, #0x30] ; unsigned short In_Word = *((unsigned short *)IN_Class.p_DataUser + 0); 1f2c4: e1d320b0 ldrh r2, [r3] ; In_Word = *((unsigned short *)IN_Class.p_DataUser + 1); 1f2c8: e1d330b2 ldrh r3, [r3, #2] ; *((unsigned short *)OUT_CLass.p_DataUserWrite + 1) = Out_Word; 1f2cc: e6bf2fb2 rev16 r2, r2 1f2d0: e1c120b2 strh r2, [r1, #2] 1f2d4: e3052678 movw r2, #0x5678 1f2d8: e3412234 movt r2, #0x1234 ; *((unsigned int *)IN_Class.p_DataUserWrite) = 0x12345678; 1f2dc: e5802000 str r2, [r0] ; *((unsigned short *)OUT_CLass.p_DataUserWrite) = Out_Word; 1f2e0: e6bf0fb3 rev16 r0, r3 1f2e4: e1c100b0 strh r0, [r1] ; } 1f2e8: e12fff1e bx lr
解决方案
核心问题是编译器默认认为p_DataUser和p_DataUserWrite指向不重叠的内存区域,因此在O2优化下对读写操作做了重排。需要明确告知编译器这两个指针可能指向同一内存,或者强制读写操作的顺序。
1. 使用Clang内置函数__builtin_assume声明指针重叠
在写入操作后、读取操作前,添加内置函数告诉编译器两个指针指向的内存可能重叠:
*((unsigned int *)IN_Class.p_DataUserWrite) = 0x12345678; // 告知编译器p_DataUser和p_DataUserWrite指向的内存可能重叠 __builtin_assume(IN_Class.p_DataUser == IN_Class.p_DataUserWrite || ((uintptr_t)IN_Class.p_DataUser >= (uintptr_t)IN_Class.p_DataUserWrite && (uintptr_t)IN_Class.p_DataUser < (uintptr_t)IN_Class.p_DataUserWrite + sizeof(unsigned int)) || ((uintptr_t)IN_Class.p_DataUserWrite >= (uintptr_t)IN_Class.p_DataUser && (uintptr_t)IN_Class.p_DataUserWrite < (uintptr_t)IN_Class.p_DataUser + 2*sizeof(unsigned short))); unsigned short In_Word = *((unsigned short *)IN_Class.p_DataUser + 0); // ... 后续代码不变
这个函数会让编译器放弃对这两个指针指向内存的读写重排优化。
2. 使用std::launder(C++17及以上)
如果指针指向的内存是标准布局类型,std::launder可以帮助编译器识别内存的最新状态,避免优化导致的乱序:
#include <new> auto write_ptr = static_cast<unsigned int*>(IN_Class.p_DataUserWrite); *write_ptr = 0x12345678; // 用launder告知编译器该内存已被修改,读取时必须取最新值 auto read_ptr = std::launder(static_cast<unsigned short*>(IN_Class.p_DataUser)); unsigned short In_Word = *(read_ptr + 0); // ... 后续代码不变
3. 插入内存屏障(Memory Barrier)
ARMv7平台可以使用__sync_synchronize()内置函数强制内存操作的顺序,确保写入完成后再执行读取:
*((unsigned int *)IN_Class.p_DataUserWrite) = 0x12345678; // 插入全内存屏障,阻止编译器和CPU重排读写操作 __sync_synchronize(); unsigned short In_Word = *((unsigned short *)IN_Class.p_DataUser + 0); // ... 后续代码不变
这种方法会强制所有之前的写入操作完成后,才执行后续的读取操作,兼容性好,但可能带来微小的性能开销。
4. 统一指针类型转换
从示例代码看,写入用unsigned int*,读取用unsigned short*,类型不匹配可能加剧编译器的优化误判。可以先统一转换为void*再转成目标类型,帮助编译器识别指针关联:
// 先统一转成void*再转目标类型 void* write_base = IN_Class.p_DataUserWrite; *static_cast<unsigned int*>(write_base) = 0x12345678; void* read_base = IN_Class.p_DataUser; unsigned short In_Word = *static_cast<unsigned short*>(read_base); // ... 后续代码不变
内容的提问来源于stack exchange,提问作者Peter

