【发布时间】:2019-10-16 07:20:49
【问题描述】:
在下面的代码中,我看到在没有隐式restrict 指针说明符的情况下,clang 无法执行更好的优化:
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct {
uint32_t event_type;
uintptr_t param;
} event_t;
typedef struct
{
event_t *queue;
size_t size;
uint16_t num_of_items;
uint8_t rd_idx;
uint8_t wr_idx;
} queue_t;
static bool queue_is_full(const queue_t *const queue_ptr)
{
return queue_ptr->num_of_items == queue_ptr->size;
}
static size_t queue_get_size_mask(const queue_t *const queue_ptr)
{
return queue_ptr->size - 1;
}
int queue_enqueue(queue_t *const queue_ptr, const event_t *const event_ptr)
{
if(queue_is_full(queue_ptr))
{
return 1;
}
queue_ptr->queue[queue_ptr->wr_idx++] = *event_ptr;
queue_ptr->num_of_items++;
queue_ptr->wr_idx &= queue_get_size_mask(queue_ptr);
return 0;
}
我用 clang 版本编译了这段代码11.0.0 (clang-1100.0.32.5)
clang -O2 -arch armv7m -S test.c -o test.s
在反汇编的文件中我看到生成的代码重新读取内存:
_queue_enqueue:
.cfi_startproc
@ %bb.0:
ldrh r2, [r0, #8] ---> reads the queue_ptr->num_of_items
ldr r3, [r0, #4] ---> reads the queue_ptr->size
cmp r3, r2
itt eq
moveq r0, #1
bxeq lr
ldrb r2, [r0, #11] ---> reads the queue_ptr->wr_idx
adds r3, r2, #1
strb r3, [r0, #11] ---> stores the queue_ptr->wr_idx + 1
ldr.w r12, [r1]
ldr r3, [r0]
ldr r1, [r1, #4]
str.w r12, [r3, r2, lsl #3]
add.w r2, r3, r2, lsl #3
str r1, [r2, #4]
ldrh r1, [r0, #8] ---> !!! re-reads the queue_ptr->num_of_items
adds r1, #1
strh r1, [r0, #8]
ldrb r1, [r0, #4] ---> !!! re-reads the queue_ptr->size (only the first byte)
ldrb r2, [r0, #11] ---> !!! re-reads the queue_ptr->wr_idx
subs r1, #1
ands r1, r2
strb r1, [r0, #11] ---> !!! stores the updated queue_ptr->wr_idx once again after applying the mask
movs r0, #0
bx lr
.cfi_endproc
@ -- End function
将restrict 关键字添加到指针后,这些不需要的重新读取就消失了:
int queue_enqueue(queue_t * restrict const queue_ptr, const event_t * restrict const event_ptr)
我知道在 clang 中,默认情况下 严格别名被禁用。但是在这种情况下,event_ptr指针被定义为const,所以它的对象的内容不能被这个指针修改,因此它不会影响queue_ptr指向的内容(假设对象在内存中重叠的情况)对吧?
这是一个编译器优化错误还是确实存在一些奇怪的情况,即queue_ptr 指向的对象可能会受到event_ptr 的影响,假设此声明:
int queue_enqueue(queue_t *const queue_ptr, const event_t *const event_ptr)
顺便说一句,我尝试为 x86 目标编译相同的代码并检查了类似的优化问题。
生成的程序集带有 restrict 关键字,不包含重读:
_queue_enqueue:
.cfi_startproc
@ %bb.0:
ldr r3, [r0, #4]
ldrh r2, [r0, #8]
cmp r3, r2
itt eq
moveq r0, #1
bxeq lr
push {r4, r6, r7, lr}
.cfi_def_cfa_offset 16
.cfi_offset lr, -4
.cfi_offset r7, -8
.cfi_offset r6, -12
.cfi_offset r4, -16
add r7, sp, #8
.cfi_def_cfa r7, 8
ldr.w r12, [r1]
ldr.w lr, [r1, #4]
ldrb r1, [r0, #11]
ldr r4, [r0]
subs r3, #1
str.w r12, [r4, r1, lsl #3]
add.w r4, r4, r1, lsl #3
adds r1, #1
ands r1, r3
str.w lr, [r4, #4]
strb r1, [r0, #11]
adds r1, r2, #1
strh r1, [r0, #8]
movs r0, #0
pop {r4, r6, r7, pc}
.cfi_endproc
@ -- End function
加法:
在与 Lundin 在他的 answer 的 cmets 中进行了一些讨论后,我得到的印象是可能会导致重新读取,因为编译器会假设 queue_ptr->queue 可能指向 *queue_ptr 本身。所以我更改了queue_t 结构以包含数组而不是指针:
typedef struct
{
event_t queue[256]; // changed from pointer to array with max size
size_t size;
uint16_t num_of_items;
uint8_t rd_idx;
uint8_t wr_idx;
} queue_t;
但是,重新读取仍与以前一样。我仍然不明白是什么让编译器认为 queue_t 字段可能被修改,因此需要重新读取......以下声明消除了重新读取:
int queue_enqueue(queue_t * restrict const queue_ptr, const event_t *const event_ptr)
但是为什么queue_ptr 必须声明为restrict 指针以防止我不理解的重新读取(除非它是编译器优化“错误”)。
附言
我也找不到任何指向文件/报告问题的链接clang 不会导致编译器崩溃...
【问题讨论】:
-
const不代表值不能改变;这意味着无法使用const标识符更改该值。int foo; int *a = &foo; int const *b = &foo; *a = 42 /*ok*/; *b = -1 /*nope*/; -
@pmg 我的意思是它不能被
const event_t *指针改变。我将添加此说明 -
@StaceyGirl 你为什么删除你的答案?
-
@AlexLop。这不是注册压力。这在其他架构的汇编中是可见的。我认为您应该检查 IR 以了解发生了什么,因为它具有明确的别名注释 (
!tbaa X)。我检查了一下,发现 Clang 在没有 TBAA 注释的情况下进行事件复制,这可以解释值刷新,但同时我看到不同的编译器配置生成可能llvm.memcpy调用 WITH!tbaa元数据。稍后我可能会尝试检查此问题,但无法从中写出答案。 -
@StaceyGirl 谢谢!我很感激。将等待您的输入。另请注意,可以在 x86 架构上重现类似的行为,而且看起来很奇怪……godbolt.org/z/5OVBEy
标签: c clang compiler-optimization strict-aliasing restrict-qualifier