1

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定義されているため、どのポイントへのコンテンツに影響を与えることはできません (オブジェクトがメモリ内でオーバーラップする場合を想定して) ですよね?constqueue_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の問題を報告しました...

4

4 に答える 4

1

私が知る限り、はい、あなたのコードではqueue_ptrポインティの内容を変更することはできません。最適化バグですか?最適化の機会を逃していますが、バグとは言いません。const を「誤解」しているのではなく、この特定のシナリオでは変更できないと判断するために必要な分析を行っていない/行っていないだけです。

補足として、元のオブジェクトが const ではないため、constness を合法的にキャストできるため、param がある場合でもqueue_is_full(queue_ptr)の内容を変更できます。そうは言っても、 の定義はコンパイラに表示され、利用可能であるため、実際にそうではないことを確認できます。*queue_ptrconst queue_t *constquueue_is_full

于 2019-10-16T07:36:52.550 に答える