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 と議論した後、再読み込みが発生する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
PS
ファイルへのリンクも見つかりませんでした/コンパイラがクラッシュする原因とならないclangの問題を報告しました...