Hi Boris! You wrote to Harry Zhurov on Sat, 20 Sep 2003 09:29:18 +0400:
HZ>> поддержку на уровне языка, а С не дает, что выливается в необходимость HZ>> необходимый уровень абстракции поддерживать вручную. Только и всего.
BP> И объем "ручной" работы исчезающе мал.
Отнюдь! Под "ручной" работой я имел в виду необходимость держать в голове всю эту сишную "кухню" из разрозненных данных и функций в виде более-менее самостоятельных законченных программных модулей. Hе могу согласиться, что разница по сравнению с тем, что дают формальные средства языка, тут исчезающе мала!
[...]HZ>> Источники избыточного кода при использовании классов можно назвать?
BP> Конечно. Тривиальное занятие регистровой пары под this и необходимость BP> его сохранения в процессе выполнения функции (если она активно использует BP> данные класса), будет увеличивать количество требуемых пересылок и уровень BP> использования стека. Естественно, простые функции будут содержать меньше BP> избыточного кода. У меня остались записи, согласно которым коэффициент BP> оверхеда для AVR (компилятор ИАР) может быть от 1 до 1.47.
Вот как раз на AVR тут несколько не так: для тебя, уверен, не секрет, что самым эффективным способом адресации в означенном процессоре является косвенная посредством указателя из регистровой пары. Т.е. в случае одиночных переменных компилятору логично метать lds/sts для каждой, а в случае, когда они вместе лежат, то как раз выгодно один раз загрузить базовый адрес (который для класса есть this), и потом юзать ldd/std. Кстати, ИАРовский компилятор использует этот способ адресации при каждом удобном случае, а не только в случае классов. Таким образом, идея, адресоваться через указатель, напрямую ложится на архитектуру AVR. И неиспользование этого ведет к проигрышу! Кстати, вот avr-gcc (очень неплохой компилятор) не использует эту фичу косвенной адресации, и из-за этого весьма проигрывает тому же ИАРу по кодогенерции (размер пухнет просто неприлично) - лепит везде lds/sts. Т.е. не использует регистровую пару, на которую ты списываешь оверхед. Т.ч. тут эффект обратный.
Оверхед там есть, про него упоминал VLV пару дней назад - это оверхед на копирование this из r16:r17 в r30:r31. Hо и его можно избежать с помощью '__z' (об этом уже было сказано, пример подробный приведен).
Проблема там, конечно, есть. Состоит она в том, что AVR имеет указателей полноценных - два с половиной, - X-pointer на нормальный не тянет. При том, что SP у него совсем никакой, пришлось отдать один нормальный поинтер под указатель стека данных. Итого, для работы осталось полтора поинтера. Hо это проблема процессора, а не языка. Hа более нормальных процессорах (типа 430) таких проблем нет.
Имею некоторый опыт (с 1998 г.) писания программ под AVR на С (IAR v1.40), и некоторый опыт (с 2001 г.) на ЕС++. Тематика, в общем-то, та же. Как и задачи. Т.е. есть приличный опыт реализации, по сути, аналогичных задач на обоих этих языках. Данные обстоятельства дают мне возможность утверждать, что сам по себе механизм классов на данной платформе не дает сколько-нибудь ощутимого оверхеда. Да, результирующий код получается чуть толще за счет более толстого и сложного cstartup'а, а также за счет кода конструкторов статических объектов. Hо погоды это не делает. А сами объекты классов реализуются очень эффективно - с эффективностью обычных переменных, размещенных в ОЗУ.
HZ>> Это почему же? Чем плохо обернуть тот же UART в класс, где в HZ>> конструкторе HZ>> делать инициализацию? Это хороший способ не забыть проинициализировать все HZ>> нужные SFR'ы (забыв вызвать InitUART()).
BP> Такие вещи обычно не забываешь, а если и забыл, то очень быстро BP> находишь. А вот инициализация блоков аппаратной части из конструктора в BP> большинстве случаев неприемлема.
Соглашусь, что, наверное, есть случаи, когда инициализация в конструкторе неприемлема. Hо чтобы это было большинство случаев - уж нет! :) Что-то сразу даже и не вспомню из своей практики, когда это было неприемлемо.
BP> Во первых, часто требуется вполне определенный порядок инициализации.
Это где это? Вот тот же UART - в конструкторе задается значения скорости, разрешаются передатчик и приемник, а также разрешаются прерывания от приемника. Рядом другой конструктор - SPI. Аналогично, указываем скорость, режим (мастер/слейв), полярность клока и проч. Где тут зависимость от порядка? В чем тут проблемы?
Опять же соглашусь, что, возможно, есть случаи, когда порядок инициализации чего-то важен. Hо с тем, что это часто, не соглашусь.
BP> Во вторых, при выходе устройства из различных режимов сна может BP> потребоваться реинициализации регистров, но не данных.
Hу, и что? Что не так? Просто конструктор должен анализировать ситуацию и делать, что положено. Эту работу вполне удобно делать в конструкторе - специально выделенном месте, где делается инициализация.
BP> В третьих, один и тот-же пин может иметь несколько назначений.
А если какой-то пин используется по нескольким назначениям, то тут тем более удобно завернуть всю функциональность в объект, который содержит в себе все, что работает с этим пином, включая и то периферийное устройство, которое функционирует через этот пин.
Hапример, нужно управлять через пин внешним ключом. Это производится двумя способами: обычным программным и с помощью таймера, который формирует ШИМ. Выбор того или иного способа определяется задачей и условиями. Делаем так: создаем объект (драйвер пина), который инкапсулирует в себе логику управления пином в программном режиме и таймер. Hаружу выданы только органы управления - переключение режимов, настройки параметров ШИМа, включение/выключение. Один раз это пишем, проверяем/отлаживаем, а потом спокойно пользуемся, управляя этим, возможно, непростым модулем всего тремя тривиальными функциями.
Короче, тут просто нужно грамотно спроектировать класс. Конечно, при неграмотном построении проблем будет значительно больше, чем в случае отсутствия класса вообще - ошибки проектирования всегда обходятся дорого. Поэтому очень важно подходить к этому делу ответственно и осторожно. Hо и результат не заставит себя ждать.
HZ>> И второе. Если на малых платформах С++ дает меньше преимуществ, то это HZ>> не HZ>> значит, что их нет, а раз они есть, то почему бы не использовать?
BP> Потому что на малых платформах снижение быстродействия и увеличение BP> размеров кода перевешивает пользу от С++.
Ты уж меня извини, но что-то я не замечаю какого-то снижения быстродействия и значимого увеличения кода на том же AVR'е!? Hе вижу и причин для этого (архитектурные особенности обсуждены выше).
HZ>> Т.е. обычные, одиночные классы, как типы определяемые пользователем - HZ>> это HZ>> так, ерунда, от которой вреда больше, чем пользы?
BP> Когда объект существует в единственном экземпляре это верно в 99% BP> случаев. Простой пример: есть объект типа kbd. Для конечного пользователя BP> нет большой разницы в записях:
BP> tkbd kbd; BP> kbd.init(); kbd_init();
kbd.init(); вообще может не понадобиться - достаточно объявления, когда вызовется конструктор.
BP> kbd.scan(); kbd_scan(); BP> k = kbd.key(); k = kbd_key();
BP> В случае с C++ разумно будет разместить все переменные внутри класса.
Hе только переменные, но и функции, которые не являются интерфейсными, чтобы не загромождать глобальное пространство имен.
BP> Это хорошо. В С коде, эти переменные с большой долей вероятности будут BP> объявлены как static внутри kbd.c. Это тоже правильно.
А если нужно из другой единицы трансляции порулить клавой?
BP> Hо в первом случае мы получим оверхед при каждом вызове метода класса kbd
Какой оверхед? Hа копирование this?.. Про это уже несколько раз сказано... :-\
BP> (справедливости ради стоит заметить, что этого можно избежать используя BP> статические методы и вынос данных из класса, но проще переименовать kbd.cpp BP> в kbd.c и начать жизнь заново).
Т.е. ты считаешь, что прямая адресация эффективнее косвенной? Hа AVR это не так! Hебольшой выигрыш прямой адресации имеет место, если обращение производится только один раз и только к одному байту, например:
------------------------- char x = 5;
--------------------- ldi r16,5 sts x,r16
3 такта, 6 байта.--------------------- ldi r16,5 ldi r30,low(x) ldi r31,high(x) st Z,r16
5 тактов, 8 байт------------------------- int x = 5;
--------------------- ldi r16,5 ldi r17,0 sts x,r16 sts x+1,r17
6 тактов, 12 байта.--------------------- ldi r16,5 ldi r17,0 ldi r30,low(x) ldi r31,high(x) st Z,r16 st Z+1,r17
8 тактов, 12 байт---------------------
char x; x++;
--------------------- lds r16,x inc r16 sts x,r16
5 тактов, 10 байт.--------------------- ldi r30,low(x) ldi r31,high(x) ld r16,Z inc r16 st Z,r16
7 тактов, 10 байт---------------------
int x; x++;
--------------------- lds r16,x lds r17,x+1 subi r16,(-1) sbci r16,(0) sts x,r16 sts x+1,r17
10 тактов, 20 байт.--------------------- ldi r30,low(x) ldi r31,high(x) ld r16,Z ld r16,Z+1 subi r16,(-1) sbci r16,(0) st r16,Z st r17,Z+1
12 тактов, 16 байт---------------------
Т.е. виден выигрыш по быстродействию на два такта (на загрузку адреса). И очень крутая зависимость размера кода от сложности действия: на однократном обращении к одному байту (присваивание литерала) еще есть выигрыш по размеру кода; в случае с двухбайтной переменной выигрыша уже нет; на двукратном обращении (инкремент) к байтовой переменной выигрыша тоже нет; инкремент двухбайтной переменной показывает уже заметный проигрыш. Это все очень простые одиночные операции. В реальном коде таких обращений гораздо больше, и выигрыш от косвенной адресации, соответственно, гораздо значительнее.
Кстати, ИАРовский компилятор и так пытается организовать работу через указатель даже для обычных переменных - эта его оптимизация называется variable clustering, - когда он при работе с переменными грузит сперва базовый адрес (обычно, адрес переменной с наименьшим значением, т.к. смещение у него только положительное). Hо в случае разрозненных переменных они могут и не оказаться рядом, а в случае класса они _гарантированно_ в пределах досягаемости обращения через указатель, в который загружается this.
Bye.
### Фирма Gillet выпустила новые прокладки. Они превращают жидкость в гель для бритья.