SII 0 3 февраля, 2023 Опубликовано 3 февраля, 2023 · Жалоба Использую 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 попросту не было, и просто возвращает полученный указатель! Есть у кого-нибудь какие-нибудь мысли по этому поводу? Пы.Сы. Вот потому я и люблю ассемблер: что ты написал, то и будет 🙂 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
SII 0 3 февраля, 2023 Опубликовано 3 февраля, 2023 · Жалоба Попробовал на всякий случай переопределить не обычную операцию new (которая, по стандарту, кидает исключение, если не может выделить память), а new(Size, std::nothrow) -- не помогло. Но зато заметил, что при включённой оптимизации компилятор сжирает if в такой конструкции: P = new type; if ( P == nullptr )... А вот в такой -- не сжирает: P = new (std::nothrow) type; if ( P == nullptr )... Но здесь к нему претензий нет: по стандарту, первый вариант никогда nullptr не возвращает, а вот второй -- именно его и возвращает, если выделить не может. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
xvr 12 3 февраля, 2023 Опубликовано 3 февраля, 2023 · Жалоба Предположу что здесь UB - компилятор знает что такое malloc и считает Ptr-1 не инициализированной памятью. Он же не в курсе, что вы в потраха менеджера памяти полезли. Сделайте Ptr volatile переменной, возможно поможет Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Alex11 5 3 февраля, 2023 Опубликовано 3 февраля, 2023 · Жалоба Еще вариант: Вы не используете нигде значение счетчика выделенной памяти. При этом оптимизатор его убивает за полной ненадобностью. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
SII 0 3 февраля, 2023 Опубликовано 3 февраля, 2023 · Жалоба 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, где всё было корректно (я код выше приводил), и в другом месте -- печатал её значение... Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
SII 0 3 февраля, 2023 Опубликовано 3 февраля, 2023 · Жалоба Вообще, эта проблема оказалась не единственной. В другом месте у меня простейшее присваивание (внутри функции, являющейся методом класса): 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 (т.е. волатильными являются не только описатели конечных точек, но и хранящий их адрес регистр, что не соответствует действительности). В общем, очень странное поведение... Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Arlleex 166 3 февраля, 2023 Опубликовано 3 февраля, 2023 · Жалоба Приведите полное описание EP_Desc, HW::USB.DEV.DESCADD и HW::USBD_BUF_DESC. Пока что из описания видно как раз некорректное определение, а не глюк компилятора. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
SII 0 4 февраля, 2023 Опубликовано 4 февраля, 2023 · Жалоба 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. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Сергей Борщ 133 4 февраля, 2023 Опубликовано 4 февраля, 2023 · Жалоба 16 часов назад, SII сказал: (т.е. волатильными являются не только описатели конечных точек, но и хранящий их адрес регистр, что не соответствует действительности). Именно, что соответствует - любое обращение к переменной должно компилироваться в обращение к регистру. Компилятор не может знать, как именно обращение к регистру влияет на состояние абстрактной машины, это забота программиста. Если программист точно знает, что в каком-то месте обращения можно соптимизировать - он может это сделать при помощи временной не-volatile переменной. 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
jcxz 226 4 февраля, 2023 Опубликовано 4 февраля, 2023 · Жалоба 1 час назад, SII сказал: так что приходится First и Last объявлять volatile, Ваш разговор напоминает монолог с самим собой - деклараций сущностей, о которых ведёте речь, вы не приводите, априори считая читателей телепатами. Что они могут вам посоветовать если даже не понимают о чём речь??? Или Вы не хотите совета, а ищете только сочувствия? 1 час назад, SII сказал: древнего volatile, нередко убивающего эффективность, volatile - вещь полезная и незаменимая при работе с периферией из си. Посмотрите хотя бы на определения регистров периферии для разных МК в заголовочных файлах компиляторов. "Эффективность убивает" она только в неумелых руках. Корректно написанный код (с использованием volatile) как правило не содержит ни одной лишней команды, по сравнению с обычным выхлопом компилятора на данном уровне оптимизации (за исключением случаев ошибок оптимизатора си). Хотя ошибки оптимизатора - возможны. ЗЫ: И (имхо) - очень умный оптимизатор должен выкидывать код, который в результате работы прямо или опосредованно не влияет ни на одну из volatile-сущностей в программе (не считая влияния времени исполнения этого кода). Пусть даже окажется выкинутой половина всей программы с целым деревом вызовов функций. 2 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
SII 0 4 февраля, 2023 Опубликовано 4 февраля, 2023 · Жалоба 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 загрузка действительно обязательна. В данном конкретном случае не "любое обращение" должно бы компилироваться в обращение к регистру, а только запись в него; чтение можно было бы опустить, если уже есть копия значения, -- но объяснить это компилятору невозможно. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Сергей Борщ 133 4 февраля, 2023 Опубликовано 4 февраля, 2023 · Жалоба 2 часа назад, SII сказал: В данном конкретном случае не "любое обращение" должно бы компилироваться в обращение к регистру, а только запись в него; чтение можно было бы опустить, если уже есть копия значения, -- но объяснить это компилятору невозможно. Возможно. Заведите временную не-volatile переменную, скопируйте в нее значение указателя и обращайтесь к полям через нее. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
SII 0 4 февраля, 2023 Опубликовано 4 февраля, 2023 · Жалоба 6 минут назад, Сергей Борщ сказал: Возможно. Заведите временную не-volatile переменную, скопируйте в нее значение указателя и обращайтесь к полям через нее. Это уже костыль. Прямым решением был бы атрибут, объясняющий компилятору правила обращения с переменной. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
jcxz 226 4 февраля, 2023 Опубликовано 4 февраля, 2023 · Жалоба 5 часов назад, SII сказал: Так что, извините, Вы не правы, утверждая, что лишних команд не будет. Можно возразить: мол, ты можешь загрузить значение из First во временную переменную и использовать его оттуда. Могу, конечно. Но это -- раздувание текста программы и лишняя работа для программиста на, по сути, пустом месте. Я утверждал, что лишних команд не будет при умелом использовании volatile. Вот как раз работа через временную не-volatile переменную - это и есть умелое использование. Таким образом вы говорите компилятору, что только само чтение First в данном конкретном месте volatile-переменной не нужно оптимизировать. А если пишете: Queue.First->A = .... Queue.First->B = .... то это указание компилятору не оптимизировать вообще все обращения к First в этом фрагменте. Так что компилятор прав. Что вы ему написали, то он и сделал. 5 часов назад, SII сказал: Если идёт присваивание глобальной переменной, я считаю правильным, чтобы компилятор всегда его выполнял, даже если, как ему кажется, она нигде не используется -- на то она и глобальная. Не согласен с вами. Правила должны быть едиными. Хотите чтобы присваивание неиспользуемой переменной не было выкинуто - извольте написать volatile. 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
amaora 23 4 февраля, 2023 Опубликовано 4 февраля, 2023 · Жалоба Есть же барьеры памяти, но во многих случаях и они не нужны. Код приведите в понятный вид, без скрытых сайд-эффектов в фунциях. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться