Jump to content
    

Ошибка в ArmClang?

Использую MDK ARM 5.38.0.0, версия компилятора 6.19, пишу на C++20.

Для контроля за динамическим выделением/освобождением памяти переопределил операции new и delete:

uint32  Alloc_Ctr  = 0;


[[nodiscard]] pvoid  operator new(std::size_t  Size)
{
    pvoid  Ptr = std::malloc(Size);

    if ( Ptr != nullptr )
    {
        Alloc_Ctr = Alloc_Ctr + *(reinterpret_cast<puint32>(Ptr) - 1);
    }
    else
    {
        Set_LED_Status(true);
    }

    return Ptr;
}
          
          
void  operator delete(pvoid  Ptr)  noexcept
{
    Alloc_Ctr -= *(reinterpret_cast<puint32>(Ptr) - 1);
    std::free(Ptr);
}

Как можно видеть, они подсчитывают количество фактически выделенных или освобождённых байтов памяти, пользуясь тем, что размер выделенного блока помещается в слове, непосредственно предшествующем этому блоку. Кроме того, если new не может выделить память, он зажигает светодиод, вызывая функцию Set_LED_Status.

При отключенной оптимизации (-O0) всё работает, как и следовало ожидать, замечательно. А когда включаешь -O2, работать перестаёт 🙂

Я дизассемблировал прошивку и вот что получил. Сначала операция delete:

ER_IROM1:00005EA0 ; void __fastcall operator delete(pvoid Ptr)
ER_IROM1:00005EA0                 EXPORT _ZdlPv
ER_IROM1:00005EA0 _ZdlPv                                  ; CODE XREF: operator delete(void *,uint)↓j
ER_IROM1:00005EA0 Ptr = R0                                ; pvoid
ER_IROM1:00005EA0                 MOV             R2, #0x20000060
ER_IROM1:00005EA8                 LDR.W           R1, [Ptr,#-4]
ER_IROM1:00005EAC                 LDR             R3, [R2]
ER_IROM1:00005EAE                 SUBS            R1, R3, R1
ER_IROM1:00005EB0                 STR             R1, [R2]
ER_IROM1:00005EB2                 B.W             free
ER_IROM1:00005EB2 ; End of function operator delete(void *)

Как видимо, всё хорошо: в R2 загружается адрес переменной Alloc_Ctr (20000060); из слова, предшествующего освобождаемому блоку (его адрес -- в R0, дизассемблер влепил ему имя Ptr по названию параметра функции), извлекается размер блока, он вычитается из счётчика, счётчик записывается на место, а потом вызывается обычная функция free, которая и освобождает память. В общем, поведение абсолютно соответствует тому, что написано на C++.

А теперь смотрим на new:

ER_IROM1:00005ED4 ; pvoid operator new(size_t Size)
ER_IROM1:00005ED4                 EXPORT _Znwj
ER_IROM1:00005ED4 _Znwj                                   ; CODE XREF: UART_ATSAMx5::Receive(void *,ushort,void (*)(void const*,ushort,bool,bool,bool,bool,bool),bool)+2A↑p
ER_IROM1:00005ED4                                         ; UART_ATSAMx5::Transmit(void const*,ushort,void (*)(void const*,ushort,bool,bool,bool,bool,bool))+26↑p ...
ER_IROM1:00005ED4                 PUSH            {R4,LR}
ER_IROM1:00005ED6                 BL              malloc
ER_IROM1:00005EDA                 MOV             R4, R0
ER_IROM1:00005EDC                 CBZ             R0, loc_5EE2
ER_IROM1:00005EDE                 MOV             R0, R4
ER_IROM1:00005EE0                 POP             {R4,PC}
ER_IROM1:00005EE2 ; ---------------------------------------------------------------------------
ER_IROM1:00005EE2
ER_IROM1:00005EE2 loc_5EE2                                ; CODE XREF: operator new(uint)+8↑j
ER_IROM1:00005EE2                 MOVS            R0, #1  ; Status
ER_IROM1:00005EE4                 BL              _Z14Set_LED_Statusb ; Set_LED_Status(bool)
ER_IROM1:00005EE8                 MOV             R0, R4
ER_IROM1:00005EEA                 POP             {R4,PC}
ER_IROM1:00005EEA ; End of function operator new(uint)

Как видим, сначала вызывается malloc, затем производится условный переход по значению полученного указателя (CBZ -- переход, если нуль). Ветка для нулевого значения совершенно корректна: вызывается функция зажигания светодиода и возвращается нулевой указатель.

А вот ветка для ненулевого указателя некорректна: компилятор полностью выкинул увеличение счётчика выделенной памяти, как будто строки Alloc_Ctr = Alloc_Ctr + *(reinterpret_cast<puint32>(Ptr) - 1); внутри if попросту не было, и просто возвращает полученный указатель!

Есть у кого-нибудь какие-нибудь мысли по этому поводу?

Пы.Сы. Вот потому я и люблю ассемблер: что ты написал, то и будет 🙂

Share this post


Link to post
Share on other sites

Попробовал на всякий случай переопределить не обычную операцию new (которая, по стандарту, кидает исключение, если не может выделить память), а new(Size, std::nothrow) -- не помогло. Но зато заметил, что при включённой оптимизации компилятор сжирает if  в такой конструкции:

P = new type;

if ( P == nullptr )...

А вот в такой -- не сжирает:

P = new (std::nothrow) type;

if ( P == nullptr )...

Но здесь к нему претензий нет: по стандарту, первый вариант никогда nullptr не возвращает, а вот второй -- именно его и возвращает, если выделить не может.

Share this post


Link to post
Share on other sites

Предположу что здесь UB - компилятор знает что такое malloc и считает Ptr-1 не инициализированной памятью. Он же не в курсе, что вы в потраха менеджера памяти полезли.

Сделайте Ptr volatile переменной, возможно поможет

 

Share this post


Link to post
Share on other sites

Еще вариант: Вы не используете нигде значение счетчика выделенной памяти. При этом оптимизатор его убивает за полной ненадобностью.

Share this post


Link to post
Share on other sites

5 часов назад, xvr сказал:

Предположу что здесь UB - компилятор знает что такое malloc и считает Ptr-1 не инициализированной памятью. Он же не в курсе, что вы в потраха менеджера памяти полезли.

Сделайте Ptr volatile переменной, возможно поможет

Да, volatile помог, хотя сгенерированный код, конечно, стал куда менее эффективным: постоянные загрузки из памяти:

ER_IROM1:00005FA8 ; pvoid operator new(size_t Size, const std::nothrow_t *)
ER_IROM1:00005FA8                 EXPORT _ZnwjRKSt9nothrow_t
ER_IROM1:00005FA8 _ZnwjRKSt9nothrow_t                     ; CODE XREF: UART_ATSAMx5::Receive(void *,ushort,void (*)(void const*,ushort,bool,bool,bool,bool,bool),bool)+2C↑p
ER_IROM1:00005FA8                                         ; UART_ATSAMx5::Transmit(void const*,ushort,void (*)(void const*,ushort,bool,bool,bool,bool,bool))+2C↑p ...
ER_IROM1:00005FA8
ER_IROM1:00005FA8 var_C           = -0xC
ER_IROM1:00005FA8 Ptr             =  4
ER_IROM1:00005FA8
ER_IROM1:00005FA8                 PUSH            {R7,LR}
ER_IROM1:00005FAA                 SUB             SP, SP, #8
ER_IROM1:00005FAC                 BL              malloc
ER_IROM1:00005FB0                 STR             R0, [SP,#0x10+var_C]
ER_IROM1:00005FB2                 LDR             R0, [SP,#0x10+var_C]
ER_IROM1:00005FB4                 CBZ             R0, loc_5FCC
ER_IROM1:00005FB6                 LDR             R0, [SP,#0x10+var_C]
ER_IROM1:00005FB8                 MOV             R1, #0x20000060
ER_IROM1:00005FC0                 LDR.W           R0, [R0,#-4]
ER_IROM1:00005FC4                 LDR             R2, [R1]
ER_IROM1:00005FC6                 ADD             R0, R2
ER_IROM1:00005FC8                 STR             R0, [R1]
ER_IROM1:00005FCA                 B               loc_5FD2
ER_IROM1:00005FCC ; ---------------------------------------------------------------------------
ER_IROM1:00005FCC
ER_IROM1:00005FCC loc_5FCC                                ; CODE XREF: operator new(uint,std::nothrow_t const&)+C↑j
ER_IROM1:00005FCC                 MOVS            R0, #1  ; Status
ER_IROM1:00005FCE                 BL              _Z14Set_LED_Statusb ; Set_LED_Status(bool)
ER_IROM1:00005FD2
ER_IROM1:00005FD2 loc_5FD2                                ; CODE XREF: operator new(uint,std::nothrow_t const&)+22↑j
ER_IROM1:00005FD2                 LDR             R0, [SP,#0x10+var_C]
ER_IROM1:00005FD4                 ADD             SP, SP, #8
ER_IROM1:00005FD6                 POP             {R7,PC}
ER_IROM1:00005FD6 ; End of function operator new(uint,std::nothrow_t const&)

 

3 часа назад, Alex11 сказал:

Еще вариант: Вы не используете нигде значение счетчика выделенной памяти. При этом оптимизатор его убивает за полной ненадобностью.

Нет, она использовалась и далее в delete, где всё было корректно (я код выше приводил), и в другом месте -- печатал её значение...

Share this post


Link to post
Share on other sites

Вообще, эта проблема оказалась не единственной. В другом месте у меня простейшее присваивание (внутри функции, являющейся методом класса):

HW::USB.DEV.DESCADD = EP_Desc;

EP_Desc -- поле этого класса, содержит указатель на структуру типа HW::USBD_BUF_DESC (а точней, на массив таких структур -- это описатели конечных точек контроллера USB). Память под сей массив была выделена ранее и указатель на неё помещён в это поле, ну а в данном методе я переписываю указатель из поля класса в регистр контроллера USB в процессе включения последнего (HW::USB -- "переменная",  на самом деле являющаяся блоком регистров контроллера USB). Так вот, это присваивание компилятором на любой оптимизации, начиная с -O1, просто выбрасывается, хотя весь остальной код функции генерируется правильно. Чтоб оно было, требуется описать поле HW::USB.DEV.DESCADD не как volatile USBD_BUF_DESC *, а как volatile USBD_BUF_DESC * volatile (т.е. волатильными являются не только описатели конечных точек, но и хранящий их адрес регистр, что не соответствует действительности).

В общем, очень странное поведение...

Share this post


Link to post
Share on other sites

Приведите полное описание EP_Desc, HW::USB.DEV.DESCADD и HW::USBD_BUF_DESC.
Пока что из описания видно как раз некорректное определение, а не глюк компилятора.

Share this post


Link to post
Share on other sites

14 часов назад, Arlleex сказал:

Пока что из описания видно как раз некорректное определение, а не глюк компилятора

Судя по всему, компилятор решил, что, раз видимого использования поля нет, то запись в него можно вообще выкинуть -- причём для этого ему нужно было проанализировать код всей программы, а не одного только файла (блоки регистров -- глобальные переменные; соответственно, обращения к их полям могут быть где угодно). Я так и не понял, разрешено ли такое стандартом. volatile исправляет ситуацию, но потенциально ведёт к потере эффективности (в данном конкретном случае -- нет, не ведёт).

Вообще, похоже, разработчики стандарта разрешили крайне агрессивную оптимизацию, но не предложили никаких средств для того, чтоб её ограничивать (кроме древнего volatile, нередко убивающего эффективность, и std::atomic, которые тоже вполне могут быть абсолютно неэффективными, да и предназначены, вообще говоря, для другого). Например:

    IORP  *P = new (std::nothrow) IORP;

    if ( P == nullptr )
    {
        return false;
    }

    P->Buffer = const_cast<pvoid>(Buffer);
    P->Size = Size;
    P->CB = CB;
    P->Ignore_Errors = false;

    Tx_Queue.Append(&P->Link);

    ++Tx_Req_Count;

    if ( Tx_Queue.First == &P->Link )
    {
        Init_Transmit();
    }

При включённой оптимизации компилятор одной командой LDRD грузил оба поля структуры Tx_Queue (First и Last; если очередь пуста, Last указывает на First -- типичное использование подобных очередей, например, в недрах Винды) сразу после выделения памяти, а не в Tx_Queue.Append, где осуществляется первое обращение к Tx_Queue. Позднее, в последнем if, он сравнивал старое (предварительно загруженное) значение поля First, а оно к тому времени может быть изменено в Append (только не прямо, а через указатель -- если очередь была пуста; компилятор, понятно, не видит этой возможности изменения). Никаких стандартных средств сказать компилятору, что Append должна служить "водоразделом", нет, так что приходится First и Last объявлять volatile, хотя в реальности они не меняются произвольным образом и зачастую действительно можно загрузить значение и использовать его несколько раз, что будет запрещено при использовании volatile.

Share this post


Link to post
Share on other sites

16 часов назад, SII сказал:

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

Именно, что соответствует - любое обращение к переменной должно компилироваться в обращение к регистру. Компилятор не может знать, как именно обращение к регистру влияет на состояние абстрактной машины, это забота программиста. Если программист точно знает, что в каком-то месте обращения можно соптимизировать - он может это сделать при помощи временной не-volatile переменной.

 

Share this post


Link to post
Share on other sites

1 час назад, SII сказал:

так что приходится First и Last объявлять volatile,

Ваш разговор напоминает монолог с самим собой - деклараций сущностей, о которых ведёте речь, вы не приводите, априори считая читателей телепатами. Что они могут вам посоветовать если даже не понимают о чём речь???

Или Вы не хотите совета, а ищете только сочувствия?

1 час назад, SII сказал:

древнего volatile, нередко убивающего эффективность,

volatile - вещь полезная и незаменимая при работе с периферией из си. Посмотрите хотя бы на определения регистров периферии для разных МК в заголовочных файлах компиляторов. "Эффективность убивает" она только в неумелых руках.

Корректно написанный код (с использованием volatile) как правило не содержит ни одной лишней команды, по сравнению с обычным выхлопом компилятора на данном уровне оптимизации (за исключением случаев ошибок оптимизатора си). Хотя ошибки оптимизатора - возможны.

 

ЗЫ: И (имхо) - очень умный оптимизатор должен выкидывать код, который в результате работы прямо или опосредованно не влияет ни на одну из volatile-сущностей в программе (не считая влияния времени исполнения этого кода). Пусть даже окажется выкинутой половина всей программы с целым деревом вызовов функций.

Share this post


Link to post
Share on other sites

1 час назад, jcxz сказал:

Ваш разговор напоминает монолог с самим собой - деклараций сущностей, о которых ведёте речь, вы не приводите, априори считая читателей телепатами. Что они могут вам посоветовать если даже не понимают о чём речь???

 То, в чём была проблема изначально, я выложил, подсказки получил -- мне этого достаточно. Дальше в какой-то мере действительно монолог: описываю дальнейшие проблемы и указываю, как их решил.

1 час назад, jcxz сказал:

volatile - вещь полезная и незаменимая при работе с периферией из си. Посмотрите хотя бы на определения регистров периферии для разных МК в заголовочных файлах компиляторов. "Эффективность убивает" она только в неумелых руках.

Я прекрасно знаю, что такое volatile, и использую его, когда надо. А вот что компилятор очень вольно обращается с порядком операций -- это таки да, было для меня новостью.

Эффективность volatile действительно в ряде случаев убивает. Чтоб не было проблем с телепатией, слегка разжую.

struct  FIFO_HDR
{
    // Адрес поля связи первого элемента в очереди или нуль.
    FIFO_LNK  *First;

    // Адрес поля связи последнего элемента в очереди. Если очередь пуста, это
    // поле содержит адрес поля First.
    FIFO_LNK  *Last;
}

Таким было моё первоначальное определение заголовка очереди, с которым компилятор позволил себе предзагрузку значения поля First, что в дальнейшем привело к неправильной работе (он сравнивал старое значение поля First, которое к тому времени изменилось).

struct  FIFO_HDR
{
    FIFO_LNK* volatile  First;
    FIFO_LNK* volatile  Last;
}

Таким стало новое определение. Теперь компилятор никакими предзагрузками не занимается, и код работает правильно. Однако, если в программе будет нечто вроде такого:

Queue.First->A = ....
Queue.First->B = ....

(обращения к нескольким полям структуры, на которую указывает Queue.First), компилятор уже не сможет соптимизировать этот код и загрузить указатель из First только один раз, он будет вынужден загружать First для каждого обращения к тому или иному полю, что эффективность и убивает. Так что, извините, Вы не правы, утверждая, что лишних команд не будет.

Можно возразить: мол, ты можешь загрузить значение из First во временную переменную и использовать его оттуда. Могу, конечно. Но это -- раздувание текста программы и лишняя работа для программиста на, по сути, пустом месте. Правильным было бы дать программисту средства запретить компилятору слишком агрессивную оптимизацию. Например, если бы я мог снабдить мою функцию Append (которая может модифицировать поле First неочевидным для компилятора способом) неким атрибутом, запрещающим компилятору перемещать обращения к памяти до и после её вызова, проблемы бы не возникало и при этом компилятор мог бы свободно оптимизировать код до и после вызова функции.

1 час назад, jcxz сказал:

ЗЫ: И (имхо) - очень умный оптимизатор должен выкидывать код, который в результате работы прямо или опосредованно не влияет ни на одну из volatile-сущностей в программе (не считая влияния времени исполнения этого кода). Пусть даже окажется выкинутой половина всей программы с целым деревом вызовов функций.

Если говорить про влияние на локальные объекты -- в общем, согласен с Вами (за исключением того, что надо бы программисту дать в руки стандартные средства явно указывать компилятору пределы возможной оптимизации, а не заставлять обходить её с помощью временных переменных и т.п. вещей, усложняющих восприятие программы).

А вот если говорить про глобальные объекты -- нет, не согласен. Если идёт присваивание глобальной переменной, я считаю правильным, чтобы компилятор всегда его выполнял, даже если, как ему кажется, она нигде не используется -- на то она и глобальная. (Ну а не злоупотреблять глобальными переменными -- это уже забота программиста, да). volatile, как я уже показал выше, не является полноценным выходом, так как препятствует любой оптимизации. Вот был бы атрибут, говорящий компилятору, что эта переменная используется, и он не должен выкидывать обращения к ней, но в остальном может оптимизировать обращения к ней обычным образом, а не трактовать как volatile...

1 час назад, Сергей Борщ сказал:

Именно, что соответствует - любое обращение к переменной должно компилироваться в обращение к регистру. Компилятор не может знать, как именно обращение к регистру влияет на состояние абстрактной машины, это забота программиста. Если программист точно знает, что в каком-то месте обращения можно соптимизировать - он может это сделать при помощи временной не-volatile переменной.

В данном случае не соглашусь. Данный регистр контроллера никогда не меняется аппаратурой, т.е. его значение абсолютно устойчиво; соответственно, если программа пишет в него что-то, а затем хочет использовать записанное значение, технически совершенно корректным и наиболее эффективным было бы использовать значение, уже находящееся в одном из регистров процессора, а не загружать его каждый раз из регистра контроллера -- а вот в случае с volatile загрузка действительно обязательна. В данном конкретном случае не "любое обращение" должно бы компилироваться в обращение к регистру, а только запись в него; чтение можно было бы опустить, если уже есть копия значения, -- но объяснить это компилятору невозможно.

Share this post


Link to post
Share on other sites

2 часа назад, SII сказал:

В данном конкретном случае не "любое обращение" должно бы компилироваться в обращение к регистру, а только запись в него; чтение можно было бы опустить, если уже есть копия значения, -- но объяснить это компилятору невозможно.

Возможно. Заведите временную не-volatile переменную, скопируйте в нее значение указателя и обращайтесь к полям через нее.

Share this post


Link to post
Share on other sites

6 минут назад, Сергей Борщ сказал:

Возможно. Заведите временную не-volatile переменную, скопируйте в нее значение указателя и обращайтесь к полям через нее.

Это уже костыль. Прямым решением был бы атрибут, объясняющий компилятору правила обращения с переменной.

Share this post


Link to post
Share on other sites

5 часов назад, SII сказал:

Так что, извините, Вы не правы, утверждая, что лишних команд не будет.

Можно возразить: мол, ты можешь загрузить значение из First во временную переменную и использовать его оттуда. Могу, конечно. Но это -- раздувание текста программы и лишняя работа для программиста на, по сути, пустом месте.

Я утверждал, что лишних команд не будет при умелом использовании volatile. Вот как раз работа через временную не-volatile переменную - это и есть умелое использование. Таким образом вы говорите компилятору, что только само чтение First в данном конкретном месте volatile-переменной не нужно оптимизировать. А если пишете:

Queue.First->A = ....
Queue.First->B = ....

то это указание компилятору не оптимизировать вообще все обращения к First в этом фрагменте.

Так что компилятор прав. Что вы ему написали, то он и сделал.

5 часов назад, SII сказал:

Если идёт присваивание глобальной переменной, я считаю правильным, чтобы компилятор всегда его выполнял, даже если, как ему кажется, она нигде не используется -- на то она и глобальная.

Не согласен с вами. Правила должны быть едиными. Хотите чтобы присваивание неиспользуемой переменной не было выкинуто - извольте написать volatile.

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...