Кланг неправильно понимает спецификатор указателя const? - PullRequest
1 голос
/ 16 октября 2019

В приведенном ниже коде я увидел, что 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 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)

Я знаю, что в лязг , по умолчанию строгий алиасинг отключен . Но в этом случае указатель 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

Добавление:

После некоторого обсуждения с Лундиным в комментариях к его ответу , яу меня сложилось впечатление, что повторные чтения могут быть вызваны, потому что компилятор предполагает, что 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 , которая не вызывает сбой компилятора ...

Ответы [ 3 ]

2 голосов
/ 16 октября 2019

Элемент event_t в queue_ptr может указывать на ту же память, что и event_ptr. Компиляторы, как правило, создают менее эффективный код, когда они не могут исключить, что два указателя указывают на одну и ту же память. Так что нет ничего странного в том, что restrict ведет к улучшению кода.

Определители Const на самом деле не имеют значения, потому что они были добавлены функцией, а исходный тип мог быть изменен в другом месте. В частности, * const ничего не добавляет, потому что указатель уже является локальной копией исходного указателя, поэтому никто, включая вызывающего, не заботится, изменяет ли функция эту локальную копию или нет.

"Строгий псевдоним«скорее относится к случаям, когда компилятор может срезать углы, как, например, если предположить, что uint16_t* не может указывать на uint8_t* и т. д. Но в вашем случае у вас есть два полностью совместимых типа, один из них просто оборачивается ввнешняя структура.

1 голос
/ 25 октября 2019

Как вы знаете, ваш код, по-видимому, изменяет данные, аннулируя состояние const:

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

Без ключевого слова restrict компилятор должен предполагать, чтодва типа могут совместно использовать одно и то же пространство памяти.

Это требуется в обновленном примере, поскольку event_t является членом queue_t и строгое псевдоним применяется к:

.. .. агрегатный или объединенный тип, который включает один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член субагрегированного или автономного объединения), или ...

В исходном примере естьряд причин, по которым типы могут рассматриваться как псевдонимы, приводящие к одному и тому же результату (т. е. использование указателя char и тот факт, что типы могут считаться совместимыми «достаточно» в некоторых архитектурах, если не во всех).

Следовательно, компилятору требуется для перезагрузки памяти после того, как она была изменена, чтобы избежать возможных конфликтов.

Ключевое слово const на самом деле не входит в этопотому что мутация происходит через указатель, который может указывать на тот же адрес памяти.

( EDIT ) Для вашего удобства, вот полное правило относительно доступа к переменной:

Объект должен иметь свое сохраненное значение, доступ к которому осуществляется только через выражение lvalue, имеющее один из следующих типов (88):

- тип, совместимый с действующим типом объекта,

- квалифицированная версия типа, совместимого с эффективным типом объекта,

- тип, представляющий собой тип со знаком или без знака, соответствующий действующему типу объекта,

- тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующего типа объекта,

- тип агрегата или объединения, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно,член субагрегата или автономного объединения) или

- тип символа.

(88) Цель этогосписок должен указывать те обстоятельства, при которых объект может иметь или не иметь псевдоним.

PS

Суффикс _t зарезервирован POSIX . Вы можете рассмотреть возможность использования другого суффикса.

Обычной практикой является использование _s для структур и _u для объединений.

1 голос
/ 16 октября 2019

Насколько я могу сказать, да, в вашем коде queue_ptr содержимое pointee не может быть изменено. Это ошибка оптимизации? Это упущенная возможность оптимизации, но я бы не назвал это ошибкой. Он не «неправильно понимает» const, он просто не имеет / не проводит необходимый анализ, чтобы определить, что его нельзя изменить для этого конкретного сценария.

В качестве примечания: queue_is_full(queue_ptr) может изменитьсодержимое *queue_ptr, даже если оно имеет const queue_t *const param, потому что оно может по закону отбросить константу, поскольку исходный объект не является const. При этом определение quueue_is_full является видимым и доступным для компилятора, поэтому он может удостовериться, что это действительно не так.

...