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