Перейти к содержанию
    

Использую 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 попросту не было, и просто возвращает полученный указатель!

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

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

P = new type;

if ( P == nullptr )...

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

P = new (std::nothrow) type;

if ( P == nullptr )...

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

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

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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, где всё было корректно (я код выше приводил), и в другом месте -- печатал её значение...

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

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 (т.е. волатильными являются не только описатели конечных точек, но и хранящий их адрес регистр, что не соответствует действительности).

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

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

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

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

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

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

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

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

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

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

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

 

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

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

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

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

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

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

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

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

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

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

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

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

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

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Присоединяйтесь к обсуждению

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

Гость
Ответить в этой теме...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...