c语言 - Clang 是否误解了'const'指针说明符?



在下面的代码中,我看到clang在没有隐式restrict指针说明符的情况下无法执行更好的优化:

#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版本编译了这个代码111.0.0(clang-1100.0.3.25)

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的回答的评论中与他进行了一些讨论后,我得到的印象是可能会导致重读,因为编译器会假设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指针以防止重新读取(除非它是编译器优化"bug")。

p.S.

我也找不到任何链接来文件/报告clang上不会导致编译器崩溃的问题。。。

[谈原程序]

这是由于Clang生成的TBAA元数据中的不足造成的。

如果你用-S -emit-llvm发射LLVM IR,你会看到(为了简洁起见,剪下):

...
%9 = load i8, i8* %wr_idx, align 1, !tbaa !12
%10 = trunc i32 %8 to i8
%11 = add i8 %10, -1
%conv4 = and i8 %11, %9
store i8 %conv4, i8* %wr_idx, align 1, !tbaa !12
br label %return
...
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 1, !"min_enum_size", i32 4}
!2 = !{!"clang version 10.0.0 (/home/chill/src/llvm-project 07da145039e1a6a688fb2ac2035b7c062cc9d47d)"}
!3 = !{!4, !9, i64 8}
!4 = !{!"queue", !5, i64 0, !8, i64 4, !9, i64 8, !6, i64 10, !6, i64 11}
!5 = !{!"any pointer", !6, i64 0}
!6 = !{!"omnipotent char", !7, i64 0}
!7 = !{!"Simple C/C++ TBAA"}
!8 = !{!"int", !6, i64 0}
!9 = !{!"short", !6, i64 0}
!10 = !{!4, !8, i64 4}
!11 = !{!4, !5, i64 0}
!12 = !{!4, !6, i64 11}

请参阅TBAA元数据!4:这是queue_t的类型描述符(顺便说一句,我向结构添加了名称,例如typedef struct queue ...),您可能会在那里看到空字符串)。描述中的每个元素都对应于结构字段,看看!5,它就是event_t *queue字段:它是"任何指针"!在这一点上,我们已经丢失了关于指针实际类型的所有信息,这告诉我编译器会假设通过这个指针写入可以修改任何内存对象。

也就是说,有一种新的TBAA元数据形式,它更精确(仍然有缺陷,但稍后…)

-Xclang -new-struct-path-tbaa编译原程序。我的确切命令行是(自从没有libc的开发构建以来,我已经包含了stddef.h而不是stdlib.h):

./bin/clang -I lib/clang/10.0.0/include/ -target armv7m-eabi -O2 -Xclang -new-struct-path-tbaa  -S queue.c

由此产生的组件是(再次,一些绒毛被剪断):

queue_enqueue:
push    {r4, r5, r6, r7, lr}
add r7, sp, #12
str r11, [sp, #-4]!
ldrh    r3, [r0, #8]
ldr.w   r12, [r0, #4]
cmp r12, r3
bne .LBB0_2
movs    r0, #1
ldr r11, [sp], #4
pop {r4, r5, r6, r7, pc}
.LBB0_2:
ldrb    r2, [r0, #11]                   ; load `wr_idx`
ldr.w   lr, [r0]                        ; load `queue` member
ldrd    r6, r1, [r1]                    ; load data pointed to by `event_ptr`
add.w   r5, lr, r2, lsl #3              ; compute address to store the event
str r1, [r5, #4]                        ; store `param`
adds    r1, r3, #1                      ; increment `num_of_items`
adds    r4, r2, #1                      ; increment `wr_idx`
str.w   r6, [lr, r2, lsl #3]            ; store `event_type`
strh    r1, [r0, #8]                    ; store new value for `num_of_items`
sub.w   r1, r12, #1                     ; compute size mask
ands    r1, r4                          ; bitwise and size mask with `wr_idx`
strb    r1, [r0, #11]                   ; store new value for `wr_idx`
movs    r0, #0
ldr r11, [sp], #4
pop {r4, r5, r6, r7, pc}

看起来不错,不是吗!:D

我之前提到过"新结构路径"有一些不足之处,但原因是:在邮件列表上。

PS。在这种情况下恐怕没有什么可吸取的教训。原则上,能够向编译器提供的信息越多越好:比如restrict、强类型(而不是无端的强制转换、类型双关等)、相关函数和变量属性。。。但不是在这种情况下,原始程序已经包含了所有必要的信息。这只是编译器的不足,解决这些问题的最佳方法是提高人们的意识:询问邮件列表和/或文件错误报告。

queue_ptrevent_t成员可以指向与event_ptr相同的内存。当编译器不能排除两个指针指向同一内存时,它们往往会生成效率较低的代码。因此,restrict带来更好的代码并没有什么奇怪的。

Const限定符其实并不重要,因为这些限定符是由函数添加的,原始类型可以在其他地方修改。特别是,* const没有添加任何内容,因为指针已经是原始指针的本地副本,所以包括调用方在内的任何人都不关心函数是否修改了该本地副本。

"严格混叠"指的是编译器可以偷工减料的情况,比如假设uint16_t*不能指向uint8_t*等。但在您的情况下,您有两个完全兼容的类型,其中一个仅封装在外部结构中。

据我所知,是的,在您的代码queue_ptr中,指针对象的内容是不能修改的。这是一个优化错误吗?这是一个错失的优化机会,但我不会称之为bug。它没有"误解"const,只是没有/没有进行必要的分析来确定它不能针对这个特定场景进行修改。

附带说明:queue_is_full(queue_ptr)可以修改*queue_ptr的内容,即使它有const queue_t *const参数,因为它可以合法地丢弃常量,因为原始对象不是常量。也就是说,quueue_is_full的定义对编译器来说是可见的和可用的,因此它可以确定它确实不是。

如您所知,您的代码似乎修改了数据,使const状态无效:

queue_ptr->num_of_items++; // this stores data in the memory

如果没有restrict关键字,编译器必须假设这两种类型可能共享相同的内存空间。

这在更新的示例中是必需的,因为event_tqueue_t的成员,并且严格的混叠适用于:

。。。在其成员中包括上述类型之一的聚合或并集类型(递归地包括子聚合或包含并集的成员),或者。。。

在最初的例子中,类型可能被认为是别名的原因有很多,从而导致相同的结果(即,使用char指针,以及类型在某些架构上可能被认为"足够"兼容(如果不是全部的话)。

因此,编译器需要在内存发生突变后重新加载内存,以避免可能的冲突。

const关键字并没有真正进入其中,因为突变是通过可能指向同一内存地址的指针发生的。

(EDIT)为了方便起见,以下是有关访问变量的完整规则:

对象的存储值只能由具有以下类型之一的左值表达式访问(88):

--与对象的有效类型兼容的类型

--与对象的有效类型兼容的类型的合格版本,

--一种类型,是与对象的有效类型相对应的有符号或无符号类型

--一种类型,是与对象的有效类型的合格版本相对应的有符号或无符号类型

--在其成员中包括上述类型之一的聚合或并集类型(递归地包括子聚合或包含并集的成员),或

--字符类型。

(88)此列表的目的是指定对象可能被别名化或不被别名化的情况。

P.S

_t后缀由POSIX保留。您可以考虑使用不同的后缀。

对结构使用_s,对并集使用_u是常见的做法。

相关内容

  • 没有找到相关文章

最新更新