Pull to refresh

Comments 59

Не очень понятно почему неиспользуемые функции не могут быть оптимизированы даже если они виртуальные.
gcc "девиртуализирует" функции на -Os/O2

Во первых, девиртуализация — это не выбрасывание неиспользуемых методов, а замена виртуального вызова на невиртуальный.
Сейчас девиртуализация в реальном проекте делается компилятором из большой тройки на должном уровне оптимизации, если граф вызовов не очень сложный, в реальном проекте это если создание объекта и вызов метода находятся в той же функции или во вложенной на один-два уровня. Кроме того, невозможно девиртуализировать вызовы объектов, которые приходят в компонент извне.
А чтобы повыбрасывать неиспользуемый код, надо глобально девиртулизовать все виртуальные вызовы, и уже у тех иерархий, у которых это удалось, удалять неиспользуемые методы. На сегодняшний день компиляторы могут делать такое только в тривиальных случаях.
Ну так «с дуру можно и орган сломать», если у вас в прошивке для МК виртуальные функции в интерфейсе компонентов — вы понимаете и принимаете все риски относительно, как минимум, размера бинарника.
Внутри современные компиляторы типа clang вполне способны на подобные оптимизации для довольно сложных случаев.
Что не отменяет факта, что можно нагородить такого, что компилятор с оптимизатором не смогут ничего сделать.
Выражусь предельно ясно:
— девиртуализация делается для локальных переменных;
— удаления неиспользуемых виртуальных функций не происходит.
Не совсем понял, почему тут зашла речь про девиртуализацию вызовов, уровни оптимизации и компилятор вообще. Компилятор создает объектный файл. А что из объектного файла попадает или не попадает в бинарный образ — прерогатива компоновщика.
Пишу как разработчик Firmware (то есть в данной области разбираюсь не очень). А разве в настройках компилятора нет настроек-«стратегий», чтобы оптимизировать/порезать ненужное? При разработке Hardware это является самым основным в работе.
Конечно, все настройки доступны. Почитать про них можно тут. Они даже выставлены по умолчанию в -Os (оптимизировать размер кода) и -flto (оптимизация при сборке). Но есть подозрение, что вызовы виртуальных функций через таблицу компилятору не так просто отследить. Собственно, проведенный эксперимент говорит ровно об этом.
UFO just landed and posted this here
Есть подозрение, что это просто приведет к размножению кода, так что смысл его выноса в базовый класс потеряется.
Нет. В конце концов, если есть паранойя по этому поводу, можно инстанцировать каждый шаблонный метод вручную. Просто много писанины будет.

А почему код должен размножаться? В базовом классе все методы инлайновые, по сути преобразуются в прямой вызов методов наследников. И оптимизацию тут просто компилятор сделает.


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


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

В данном случае оправдано, так как это библиотека и каким-то макаром нужно все равно интерфейс определить. Другое дело, что, как выше сказали можно было это сделать через Странно рекурсивный шаблон.


Но в любом случае, как бы вы узнали какие функции нужно реализовывать, если нет интерфейса? Методом проб и ошибок?

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

Инстанс то один будет для каждого объекта, который будет содержать в себе и методы базового класса и методы наследника. Но так как методы базового класса инлайн, то они вырождаются в прямые вызовы метода наследника, поэтому их не будет…


Пример

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

Я не много не ту ссылку вам дал:
Правильная ссылка


И если теперь сделать в реализации так:


using BaseUDP = UDP<EthernetUDP>

//то в самой библиотеке это будет использоваться так
BaseUDP.write(..)
А для чего нужен такой базовый класс? Полиморфизм здесь невозможен, поскольку Base(T1) и Base(T2) — это разные типы.

Статический полиморфизм через using


//в вашей реализации вы должны определить псевдоним базового 
//типа, который используется внутри библиотеки.
using BaseUDP = UDP<EthernetUDP> ; 
//или так:
using BaseUDP = UDP<OtherUDP> ;

//в самой библиотеке просто всегда:
BaseUDP udp ;
udp.write(..)
Касаемо интерфейса — он вообще нужен для полиморфизма исключительно, а не для того, чтоб не забыть, какие методы реализовывать. Применение полиморфизма в микроконтроллере вряд ли выйдет за рамки печати и стримов.

Да, все верно, чтобы подставить разную реализацию, но нужно же знать, реализацию чего, если нет интерфейса, что реализовывать то?

Есть один подход, когда дочерние классы используют функционал родительского. Тут все просто — не хочешь, не используй. А бывает наоборот — родительский класс — фрэймворк — использует виртуальные методы, реализованные в дочерних. Здесь да, без виртуальных методов не обойтись, если уж такой подход реально нужен.

Ну, скажем, мок для юнит-теста сделать проще, если класс полиморфный.

Ну, использование С++ не означает ведь автоматически, что все методы виртуальные. Скорее мораль — "знай свои инструменты".


Например, uVision Keil — одна из сред для разработки для ARM Cortex — со "старым" компилятором armcc, который Arm Compiler 5 более-менее справляется с выкидыванием неиспользуемых виртуальных методов — но при смене компилятора на "новый" — Arm Compiler 6 выясняется, что эта оптимизация пропала.


А новый компилятор — это внезапно clang, в котором такая фишка только-только приехала.


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


Писать на чистом С это уж какое-то слишком радикальное решение.

UFO just landed and posted this here

Микроконтроллеры и задачи разные бывают, подчас на десятки и сотни тысяч строк кода счёт идёт.

UFO just landed and posted this here

Embedded сильно разный везде бывает, у всех своя специфика, для каких-то орехов как раз впору. :) И мегабайт для этого тоже не нужен. Вот из своей практики пример — количество in-house кода подбирается к двадцати тысячам строк (в основном C++, немного C, немного ассемблера в узких местах), если весь библиотечный код посчитать (а библиотек там порядком) то почти полмиллиона строк кода. И это компилируется для машинки с 256к флэша (точнее 192к, остальное отведено на загрузчик с обновлением по воздуху) и 32к оперативной памяти.


Когда проект из крупных, C++ очень сильно помогает. Понятно что если с оглядкой использовать — некоторые фичи накладно тащить на контроллер, а другие — сплошной профит с любой стороны.

UFO just landed and posted this here

Вот — у вас самих проект на 19к LoC, а говорите что не до тысяч строк. :) В моём примере это медицина была, там много всего на борту, тот же offline анализ данных.


Понятно что на C тоже можно делать очень крупные проекты для МК. Просто на C++ это быстрее писать, часто легче читать и поддерживать, и, если знать что обходить стороной, то практически забесплатно.

За меня уже ответили, но все же — проекты и правда бывают разные. Вот сейчас у меня проект, который я с трудом упихиваю в 128 Кб.


Лично я попробовал писать ООП на чистом С и весь исплевался. Я не буду распинаться про всякую шаблонную магию, constexpr и тому подобное (но могу, если хотите) — поскольку не так уж часто всем этим пользуюсь, в основном просто "С с классами".


Мне лично остро не хватает каких-то банальных вещей, скажем, возможности сделать enum, который к int'у не приводится неявно. Я уж молчу про ручной подсчет ссылок (привет, pbuf'ы из lwip)!


В целом это дело вкуса, конечно. Вон, целый Линукс на чистом С написан — и ничего, живут люди, ни о каком С++ и слышать не хотят.


Но вариант "ой, в С++ виртуальные методы не соптимизировать, нафиг этот ваш С++, пойду обратно в С" все же звучит слишком радикально.

UFO just landed and posted this here
либо с вычислениями (ну это мимо микроконтроллеров, видимо)

Интересно, почему. Иногда приходится достаточно плотно считать что-нибудь — ЦОС, например. Или вот я недавно делал на контроллере достаточно обильные расчеты из трехмерной геометрии. Просто вам не попадалось таких задач, видимо (ну или я завышаю сложность того, что попадалось мне :).


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


У меня сложность с некоторых пор ассоциируется с девайсами, в которых надо учитывать все возможные ситуации — каждый датчик может отказать, каждый интерфейс, причем в разных вариантах — и на все это надо как-то прореагировать, подстелить соломки, просигнализировать. Тут С++ не особо помогает, честно говоря :)

UFO just landed and posted this here
UFO just landed and posted this here
Если МК выбирать под задачу, проблемы с «упс, флеш кончилась» не будет.
Вообще, если не писать во флешь сотню разных шрифтов, крупные куски изображений и гигантские веб-страницы, то как он может «внезапно» кончиться? Скажем, в МК за 1 бакс 128кБ флеша; я даже не представляю, что нужно туда написать (помимо перечисленного выше), чтобы он кончился. А если взять что-то покруче, то там мегабайт и больше…
P.S. А насчет C vs C++, я тоже долго думал, что кресты — оверхед для микроконтроллеров, пока один товарищ не доказал мне (с представлением ассемблерных листингов), что никакого оверхеда нет, если руки прямые! Зато его код инициализации и работы с устройствами получается чуть ли не втрое короче моего…
Из дешевых AVR сложно что то выбрать, кроме Atmega328.
STM32 — да, есть где разгуляться
Ну так аврки же — прошлый век! Зачем в наше время выбирать для разработки их, если есть более удачные STM32? Главное — не браться за неудачный STM32F103 (да и вообще почти всю первую серию), а в остальных сериях есть очень много хороших МК. Я одно время порывался для «удешевления» пользоваться STM8 кое-где, но понял, что мнимые 30 рублей экономии не стоят того, чтобы мучиться с восьмибитками! Тем более, что в 60-рублевом STM32F072 есть еще и USB…
По моим наблюдениям встроенный USB в STM32 для продакшена не годится, так что пользы от его наличия мало. Кстати, а что не так с STM32F103?
Кто вам такой бред сказал? Очень даже годится. У меня несколько разных устройств никогда не сбоили по аппаратной вине.
А если руки кривые, то другое дело…
Насчет STM32F103, советую errata на него почитать. Ну и просто RM. Он был первым блином у ST, поэтому там огромное количество косяков (даже настройки GPIO кривые, чего стоит хотя бы невозможность включения подтяжки при работе порта в режиме OUT; ну и других косяков немало).
Вы свои устройства не тестировали особо наверное. Проблема в том, что STM не реализовал корректную обработку ошибок в канале передачи. Помеха по землям приводит к тому, что USB перестает работать. C FTDI такого не происходит или происходит, но на несколько порядков реже. Так что встроенный USB годится только для устройств с питанием от USB.
Что значит «не тестировал особо»? У меня на телескопе второй год работает система термомониторинга главного зеркала (там, между прочим, одновременно работают USB и CAN!). Не одно соревнование по даунхиллу без проблем отработали хронометры. И т.д…
Как при питании от USB, так и при внешнем питании проблем нет.
FTDI я никогда не пользовался — зачем такую дорогую дуру брать, когда есть дешевые pl2303 и ch340 (последний хорош более простой схемой включения). Но эта фигня ушла в прошлое после полного отказа от МК без встроенного USB!
Ну а меня проблемы ушли после полного отказа от использования встроенного USB. Тестировал очень просто — USB устройство заземляем, а компьютер нет, в соседнюю розетку включаем генератор помех — пусковое устройство для люминесцентных ламп последовательно с дросселем для них же. Все проблемы с помехозащищенностью проявляются в течение нескольких секунд. Проблемы, кстати, впервые обнаружили реальные пользователи с теми же люминесцентными лампами на потолке.
Если реально жесточайшие помехи влияют на USB, то FTDI тоже ничем не поможет.
В этом случае нужно RS485 или CAN использовать.
Говорю же, FTDI помогает. В USB протоколе есть средства для детектирования ошибок и ретрансмита пакетов. Но не все вендоры эти возможности реализуют.
UFO just landed and posted this here
Да, только клиенты хотят совместимости с десктопным железом. Ну и кривизну реализаций USB в микроконтроллере это не отменяет ведь.
UFO just landed and posted this here
UFO just landed and posted this here
На алиэкспрессе разницы в цене между 042 и 072 практически нет.
Если же брать официально, то да — 042 несколько дешевле за счет более слабой периферии. Но, скажем, как USB-CAN — отличное решение!
UFO just landed and posted this here
Они абсолютно одинаковы в этом смысле. И у обоих CAN и USB могут работать одновременно. Вот и вот. В обоих случаях код проверял и на STM32F042, и на 072.
UFO just landed and posted this here
Да, буфер общий — как и в STM32F103 и прочих. Вот только, в отличие от STM32F103, у STM32F0x2 буфер CAN'а идет в самом хвосте общей области, поэтому если от USB не требуется выжимать по полной программе, то с CAN оно отлично уживается.

Когда-то в GCC 3 была -fvtable-gc как раз для этого, но она была очень глючной и была удалена до лучших времён. А теперь уже -flto даёт компоновщику куда больше информации чем в своё время сохраняла -fvtable-gc, так что компилятору ничто не мешает всё девиртуализировать и удалить неиспользуемое.

Как показал эксперимент, — ничего не мешает, но ничего и не помогает особо)

А вы с -flto проверили? Или просто в статье не упомянуто? Я ещё раз с испугу перечитал, вдруг, думаю, в первый раз проглядел. :)

Да, в комментах отписался. Эта опция стоит по умолчанию вместе с -Os

Может быть, стоит посмотреть на участок, где создаются объекты производных классов? Это не критика, я верю что вы дотошно всё рассмотрели, так что это мысли вслух; я пробовал, и неиспользованные виртуальные функции GCC с -flto удалять может, но спугнуть его может что угодно — скажем, если заключить создание объекта производного класса в if то, даже если там выше константа и можно этот if выбросить в ходе оптимизации, то всё равно откажется верить что никаких других конкретных классов из этой иерархии не создаётся.


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

Sign up to leave a comment.

Articles