Pull to refresh

Comments 33

Супер. Ещё не хватает бутстрап-кода, который вызовет все конструкторы/деструкторы для статических переменных (они лежат в отдельной секции elf .init/.fini), но он нагугливается
Я правильно понимаю, что под статическими переменными вы понимаете глобальные переменные?

В таком случае, если верить FAQ avrlibc дополнительный код не нужен: «Constructors and destructors are supported though, including global ones.»
А, вероятно, теперь это стало доступно.
В своё время, когда я писал под AVR, то ли по причине отсутствия функционала, то ли по причине переписанной кастомной stdlib и бутстрапперта, это всё не работало, и приходилось закатывать Солнце вручную
Да, судя по спискам рассылки, раньше конструкторы и деструкторы не работали.
Не могу удержаться от того, чтобы не запостить сюда тролейбус.жпг и от того. чтобы сказать что на контроллере с++ совершенно не нужен.

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

Вы думаете, а я мерял. И серьезные люди из ISO в 2006 году меряли и пришли к выводам, что причин C++ коду потреблять больше памяти или работать медленее, чем C код, нет. Исследование проводилось как раз на тему применимости C++ для встраиваемых систем.

Что позволяет достаточно легко дизассемблировать программу и посмотреть сбойный кусок, как такое обстоит с с++?

Точно также. Структуры C++ совершенно аналогичны структурам данных, которые пишут C программисты руками. При нормально знании языка я без труда понимаю, во что он превращается. Правила не сложные.
Тогда мы просим подробный пост о си и с++ на авр с ассемблерными выкладками по возможности, прочту с громадным удовольствием!
Совсем подробный — это TR18015 — 202 страницы с разбором эффективности (скорость, потребление RAM, ROM, скорость компиляции, затраты на разработку и поиск ошибок) C++. В том числе там идет сравнение с C. Дается разбор действий компилятора, необходимых для реализации той или иной фичи, а также даютя реальные замеры производительности 6ти разных компиляторов. Кроме того даются рекомендации по организации интерфейса к железу.

Меня же хватит только на несколько экранов текста.
Ну я не прошу тут давать справочник :) Но общий принцип хотелось бы уловить.
А после всей этой херни у нас остается 20байт флеша и мы можем понтово помигать светодиодиком :)
А вот тут вы не правы. Я приводил оценки в статье.

Использование чисто виртуальных методов — 20 байт флеша и 2 байта оперативки вне зависимости от количества. И то, если такие методы есть в программе.

Статические переменные с инициализацией в рантайме — их вообще лучше не использовать. Поэтому оверхэда нет.

placement new — это inline функции, которые будут убраны компилятором, оверхэда нет.

прочие new, delete — не мерял, но дадут оверхэд в десяток байт, если их использовать.

Так что я наоборот уложился в 20 байт флеша, а остальное оставил на полезные задачи.
Никто не спорит, что на С++ можно исхитриться писать так же эффективно, как и на Си. Вот только проблема в том, что сделав пару #include из STL и написав объектный модуль с иерархией и наследованием объектов событий (был такой протокол), мы поимели увеличение прошивки ровно в два раза с 0,7 МБ до почти 1.4 МБ. Мало того что её ещё приходится заливать дольше, так ещё и компилируется всё это добро крайне небыстро. Пришлось сделать кусок прошивки опциональным, после чего он стал крайне непопулярным и его редко кто включал в сборку.
Функционал С++ никогда на этом уровне не был настолько уж жизненно необходим. Что за такая нелюбовь с Си, что прямо так и тянет потратиться там, где дорог каждый байтик.
#include из STL и написав объектный модуль с иерархией и наследованием объектов событий (был такой протокол), мы поимели увеличение прошивки ровно в два раза с 0,7 МБ до почти 1.4 МБ.

Поэтому я не призывал везде использовать STL. Там есть сложные объекты и есть совсем простые. Сложные безусловно надо мерять и хорошо думать, прежде чем использовать. Но такая элементарщина, как std::min, которая встречается довольно часто, точно не повредит.
Что за такая нелюбовь с Си, что прямо так и тянет потратиться там, где дорог каждый байтик.

Ну что вы, я люблю C. Но вместо структуры и двух функций init и destroy я предпочту класс. Это полный аналог и даст такой же ассемблерный код (это я проверял), зато компилятор сам расставит вызовы деструктора.
В C++ много вещей, которые стоят ровно столько же, сколько их аналоги на C, только предоставляют больше проверок корректности кода. А если я могу сделать что-то простыми способами на C, то с какой стати я буду делать это сложно на C++?
Интересно, почему компилятор заполняет vtable после вызова конструктора, а не до? Неужели ошибка дизайнера языка и последующее бремя совместимости?

В c#, например, кусок кода с классами B и C из статьи работает.
Да это и удобно, в конструкторе базового класса окна вызвать виртуальный GetTitle(), который определён в потомке, чтобы заполнить заголовок создаваемого окна.
Нет, не ошибка. Ошибка — разрешать вызывать виртуальные функции из конструктора. Дело в том, что дочерний класс в это время не инициализирован, более того, он не знает, какая из его баз уже инициализирована. Поэтому виртуальный метот в этот момент не может использовать ни одного свойства объекта, включая свойства базовых классов. Это очень сильное ограничение, выполнение которого очень трудно проконтролировать.
А vtable заполнять не надо, он один на класс, а не на экземпляр класса. Переключение между vtable — это просто запись константы (адреса vtable) в соответсвующую ячейку.
Понятно. Метод, который не имеет доступа к полям, не очень полезен.
Но можно было бы поменять порядок инициализации. В c#, например, сначала инициализируются члены классов (в произвольном порядке), а потом вызываются конструкторы (от предка к потомку).
Ах, я понял, почему так нельзя.
В с++ есть превосходный способ передать ссылку на класс-контейнер в конструктор члена, при этом базовый класс гарантировано построен:
class MyClass {
    MyClass() : x(this) { }
    MyMember x;
};


Поэтому про члены нельзя сказать, что они ничего не знают о классе и их можно инициализировать в любом порядке до конструкторов.
В с++ есть превосходный способ передать ссылку на класс-контейнер в конструктор члена, при этом базовый класс гарантировано построен:

Как раз наоборот, конструктор MyClass еще не отработал, а вы уже его адрес куда-то передали. Кроме того, получилась циклическая зависимость: два класса знают друг про друга. Это признак плохого дизайна: если такое появляется в коде, на него надо очень пристально посмотреть и подумать, нельзя ли иначе. Как правило, можно.
«Базовый класс гарантировано построен» — конструктор предка MyClass отработал (в примере этот базовый класс не показал)
Да, именно такой трюк используется в QT, чтобы обеспечить удаление дочерних виджетов базовым.
А зачем? Ваш пример с GetTitle() довольно страннен: отрисовка окна — это не то, что должно делаться в конструкторе. Конструктор должен сделать валидный объект, а остальное должны делать другеи методы класса. Причем если базовому классу нужны данные дочернего, то вы допустили ошибку проектирования: данными владеет не тот, кто их использует. Если заголовок есть у базового класса, то и хранить его должен он.
А теперь про порядок инициализации. Инициализировать все переменные в том порядке, в котором они лежат в памяти — это хорошо. У нас эффективно будет работать кэш из-за локальности данных и будет работать prefetch. Причем код списка инициализации дописывается в начало конструктора и мы имеем одну функцию. Если бы нужно было сначала инициализировать все свойства из списка инициализации, а затем вызывать конструкторы, надо было бы делать две отдельные функции и, соответсвенно, удвоилось бы количество вызовов.

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

Кроме того, работает логика, что класс — это законченная сущность, минимизирующая связность с внешними по отношению к классу данными. Поэтому классу должно хватать для инициализации того, что передано через аргументы. Иначе возникает неявные связи, усложняющие логику программы, ухудшающие её читаемость и в конечном итоге приводящие к ошибкам.
Очень недурно, впервые за многое время интересная статья на тему low-level C++.
А почему без placement new нельзя сделать кольцевой буфер? Может я чего-то не понимаю, но ведь кольцевой буфер — это просто массив, «закольцовывается» он чисто логически.
Но он же может быть и пустым. Это значит, что надо сначала выделить место под весь буффер, но не создавать там объекты. Даже не вызывать конструкторы по-умолчанию. А потом, по мере поступления объектов, класть их в буфер, вызывая placement new, а при забирании элемента из буфера надо обязательно вызывать деструктор у удаляемого элемента.
А «просто массив» будет хорошо работать только для POD'ов. Ну еще для классов с конструкторами по-умолчанию, не делающих в конструкторах сложных вещей, когда вам не важно в какой момент отработают конструктор и деструктор.
Я недостаточно хорошо знаю с++ и затрудняюсь представить объект со сложным конструктором, который не использует динамическую память. Вы не могли бы привести пример?
Это, как правило, объекты, представляющие различные ресурсы. Пример не из embedded мира: открытый файл, socket (например в очереди соединений, ожидающих обработки), lock. Я могу представить объект, который на время своего существования держит зажженым индикатор: например длительная операция может зажигать лампу «busy».

Но тут есть еще одна сторона. Без placement new мне надо заполнить буфер объектами, причем мне неоткуда взять валидные объекты. Если я напишу конструктор по-умолчанию, то я допущу существование «пустого» объекта, к тому же потрачу время на инициализацию элементов буфера, скажем, нулями. Или же я не буду писать конструктор и у меня в объектах будет храниться мусор. Это плохо тем, что в другом месте программы я могу случайно создать невалидный объект и компилятор этого не заметит и не предупредит. Вместо этого я запрещу существование невалидных объектов: если он создан, то с ним всегда можно работать. А контейнер будет иметь возможность выделить память, но не класть туда объекты.
Кстати, пример с индикатором не такой уж и странный. У функции может быть много точек выхода, поэтому проще один раз создать объект в начале и знать, что индикатор погаснет в любом случае, чем ставить вызов функции гашения индикатора перед каждым return. Кроме того, мы разделяем логику: моргание лампочкой это одна задача, а бизнес-логика — это другая. Смешивать их не надо, в том числе потому, что при переходе на другой камень я хочу переписать только аппаратно-зависимый класс моргания лампочкой, а универсальную логику оставить без изменений.
При этом не должно возникать overhead'а: конструктор и деструктор заинлайнятся в места вызова и превратятся в одну-две инструкции, устанавливащие бит в нужном регистре.
Спасибо, примерно понял.
Просто я сам юзал кольцевые буферы только для передачи данных по каким-нибудь интерфейсам; т.е. в буферах лежали просто байты (или указатели). То есть кольцевыми буферами вполне можно пользоваться и без new, хоть на чистом Си. Именно поэтому делать буфер из сложных объектов мне кажется немного странным. Но в целом логика ясна, спасибо.
На C кольцевой буфер пишется под каждую конкретную реализацию или хранит нетипизированные блоки байт. На C++ это будет шаблон, учитывающий особенности каждого конкретного типа данных. Причем хорошая реализация для простых типов будет иметь специальную эффективную реализацию. Например move(begin, end, destination) может определить, что тип допускает побайтное копирование и скомпилироваться в такой же код, как и memscp. С поправкой на разные прототипы функций, конечно.
Написать такую реализацию сложно, но зато она кладется в библиотеку и потом используется много раз. В gnu STL я такие оптимизации вижу, а вот в uclibc++ их нет, в том числе потому, что когда писался uclibc++ не все из них можно было сделать.
memscp — забавная опечатка. Вроде как «scp» — unix-утилита «secure copy» (связь защищена SSH), и в то же времям «mem» — не на другую машину, а из памяти в память, по зашифрованному туннелю :)
Sign up to leave a comment.

Articles

Change theme settings