Перевод статьи "The Bare Metal Enthusiast: I can C clearly".

Просматривая статьи на одном новостном ресурсе, увидел перевод, который мне очень понравился.
Нашел оригинал статьи. Оказалось, в блоге автора были еще несколько статей. Переводом одной из них я и хочу с вами поделиться. Предназначена она в первую очередь для тех, кто только начал писать для микроконтроллеров, написал несколько программ и столкнулся с ощущением «поверхностности» знаний, почерпнутых из руководств вроде «быстрый старт». По крайней мере у меня возникал вопрос: «Ну умею я настраивать АЦП, таймеры, прерывания и дергать ногами МК. А что такое архитектура программы и как её выбирать? Почему код, напечатанный в книгах, отличается от кода, который я вижу в исходниках?».

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

Что лучше использовать: ассемблер или Си? По этому вопросу споры ведутся так давно, что я был удивлен, что его по-прежнему задают. Каждый имеет свою точку зрения, и я собираюсь поделиться с тобой своей. Важнейшим преимуществом использования Си является гибкость. Самой большой проблемой использования Си является гибкость.

Одним из наиболее значимых свойств Си является то, что он не «привязан» ни к одной из аппаратных платформ или систем. Это позволяет писать программы, которые будут выполняться на практически любой платформе без глобального переписывания кода. Когда ты работаешь с микроконтроллерами (МК), то тратишь много времени на написание программы. Я подсчитал, что как минимум 80% времени работы над проектом тратиться на написание, тестирование и отладку программы. Некоторые из нас могут позволить себе тратить время на переписывание кода просто для того, чтобы он запустился на другом МК. Перенос проверенных, надежных алгоритмов на другие МК, обычно называемый «портированием» кода, происходит намного чаще, чем ты думаешь. Это обычно является результатом развития требований к рабочему проекту. Иногда хорошим решением будет использовать текущий МК. Но нежелание наступать на грабли при переносе коде¬ – это плохая причина для отказа. Переписать и протестировать все заново будет дороже – и это факт. (But the inconvenience associated with code migration is not a good reason – given the costs of writing and testing software, it‘s a fact of programming life.)

Си часто называют языком среднего уровня, т.к. он сочетает в себе абстракции языков высокого уровня с функциональностью ассемблера. Си это относительно компактный язык программирования (наверно, имеется в виду малый размер бинарного файла после компиляции), который поддерживает низкоуровневые побитовые манипуляции с данными. Последняя особенность абсолютно необходима для написания программ, в которых нужно изменять биты в регистрах управления встроенной периферией МК.

Я долгое время рабою с 8-битными МК и «на ты» с ассемблером. Я научился методам структурного ассемблера(structured assembly language methods) и есть несколько приемов, которые могут создать потенциальную проблему для Си-компилятора. Выполнение кода, находящегося в стеке или манипуляции с флагом переноса – это только два примера задач, где использовать ассемблер проще, чем Си.

Ассемблер дает тебе огромную свободу действий, и это может быть очень прикольно. Но очень сложно создать программу, которую можно было бы перенести на другую платформу, к примеру, с 8-разрядного МК на 32-разрядный. Программирование на Си не обязательно избавит от необходимости внесения изменений в программу при переносе, но может сделать эту задачу легче.

В сравнение с ассемблером, код, написанный на Си, МОЖЕТ БЫТЬ:

  • более надежным;
  • более расширяемым;
  • легче переносить на другие платформы;
  • легче поддерживать;
  • более продуктивным.

Заметьте, что я написал «может быть». Ни один язык программирования не является «серебряной пулей», которая решит все твои проблемы. Твоя программа не станет лучше только потому, что она написана на Си. Я видел программистов, которые пишут плохой код на ассемблере и ужасный код на Си.

Компилятор языка Си не эффективней, чем хороший программист на ассемблере.(См. комментарий angel5a ) Вполне возможно писать на Си программы сопоставимые по скорости и использованию памяти с такой же программой, написанной на ассемблере, ты просто должен знать, что твой код говорит сделать компилятору. Я регулярно просматриваю дизассемблерный листинг, чтобы увидеть, как на самом деле работает мой код. Это научило меня, как писать эффективный код на Си. Я даже выучил несколько интересных трюков на ассемблере. Я научился думать как компилятор. Когда ты привыкнешь думать таким образом, твой код станет работать эффективней.

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

Существует популярное заблуждение, что программа на Си является самозадокументированной. Это не так. Я слышал много аргументов против комментирования кода – ни один из них не пришелся мне по душе. Нравиться тебе это или нет, программирование требует дисциплины. Имена функций и переменных должны точно описывать своё предназначение. К примеру, имя функции «call_me_a_taxi ()» (вызови мне такси) не передает никакой информации о своей цели, тогда как “toggle_GPIO_for_heater_elements()” (переключить порт ввода/вывода общего назначения для нагревательного элемента) содержит всю необходимую информацию. В большинстве случаев комментарии должны пояснять не что код делает, а почему он это делает. Может потребоваться некоторое время, чтобы написать толковые комментарии, но это сэкономит тебе гораздо больше времени впоследствии. Комментарии отражают твой ход мысли во время написания кода. Они помогут избежать возгласов вроде «О чем я только думал, писавши ЭТО!?!?», когда захочешь спустя 6 месяцев добавить новую «фичу» или исправить ошибку.

Перестань использовать int!


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

Для начала, в МК ресурсы сильно ограничены. Количество памяти ограничено, и тебе лучше бы знать, сколько её точно в твоем распоряжении. Вычислительная мощность ограничена тактовой частотой и разрядностью твоего МК ¬– 8, 16 или 32 бита. Ты должен четко представлять, сколько потребуется времени для выполнения той или иной операции, чтобы не терять связь с реальным временем. В следующих статьях я покажу, как с помощью прерываний синхронизировать твою программу с внешним миром.

В Си есть несколько встроенных типов данных, основными являются char, short, int, long, float и double. За исключением типа char, все эти типы по умолчанию знаковые. Стандарт ANSI C99 поддерживает новый тип данных — Boolean, объявляется как bool. Данные, объявленные как bool, могут принимать только одно из двух возможных значений ¬– True = 1 или False = 0. Интересно, что ANSI C определяет только минимальный размер данных для каждого типа, максимальный размер же не определен. В частности, ANSI C гласит, что:

sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long)

Как следствие этого, Си-компилятор для 32-х разрядного МК может зарезервировать 32-х битную область памяти для сохранения логической переменной, объявленной как bool. Смысл этого в том, что Си старается поддерживать «естественный» размер регистров и памяти процессора. Я думаю, ты согласишься, что использование четырех байт памяти лишь для сохранения значений «Истинно» или «Ложно» является ужасно неэкономным подходом.

Для большинства 8-ми и 16-ти разрядных МК компилятор выделяет для хранения переменной типа int 16 бит. 32-х разрядный компилятор обычно резервирует для хранения переменной типа int 32 бита памяти. Эта неоднозначность является неприемлемой, когда мы создаем программное обеспечение для микроконтроллеров, где оперативная память (RAM) является ограниченным ресурсом. В дополнение к этому, если встроенное программное обеспечение использует математические расчеты, разница в размере переменной может стать причиной ошибки. Когда я переношу программу из 8-битного МК на 32-битный, я ожидаю увидеть некоторые изменения в использовании памяти. Но я не хочу видеть огромный скачок в использовании оперативной памяти.

Рассмотрим следующий пример кода:

int n;
for (n = 0; n != 100, n++)
{
  …
}

Это вполне правильный пример цикла, который большинство из нас видели множество раз. В приведенном здесь виде этот код не совсем хорош. Для начала, как я писал выше, размер n зависит от разрядности МК. Нам не нужно использовать 32 бита для того, чтобы досчитать до 100. Во-вторых, по умолчанию int это знаковый тип данных. Знаковые типы используются для вычислений, в которых важен знак. Нам не нужно использовать знаковую 32-битную переменную для того, чтобы досчитать до 100. Один из первых уроков в повышении эффективности кода включает в себя использование переменных соответствующего размера.

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

typedef unsigned char       uint8_t;
typedef unsigned short int  uint16_t;
typedef unsigned long  int  uint32_t;

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

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

Ниже приведена моя более эффективная реализация цикла for:

uint8_t n;
for (n = 100; n != 0, n--)
{
  …
}

Обратите внимание, что мы знаем, что n теперь занимает ровно один байт, независимо от разрядности МК, и что он беззнаковый. Кроме того, я изменил направление счета (теперь значение не увеличивается, а уменьшается). Я сделал так, чтобы в моей версии проверялось, равно ли значение переменной n нулю. Любой хороший программист на ассемблере знает, что все процессоры имеют ассемблерную команду проверки на ноль. Ни один стандартный процессора не имеет команды проверки, равна ли переменная 100, поэтому проверка условия n != 100 требует выполнения нескольких инструкций.

8-битный МК компании Freescale семейства S08 имеет ассемблерную инструкцию, предназначенную специально для того, чтобы сделать циклы как можно более эффективным — DBNZ. Команда DBNZ означает «Уменьшение на единицу и переход, если не нуль». DBNZ будет уменьшать беззнаковую 8-битную переменную и переходить в начало цикла, если результат не равен нулю, а это именно то, что нужно для второй версии реализации цикла. Одна ассемблерная инструкция DBNZ выполняет сразу две команды на Си: «n = 0; n --». Вот это эффективно!(см. комментарий переводчика в конце статьи)

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

Понимание оперативной памяти.


Знание, где хранятся переменные также важно, как и использование правильного (минимально возможного) размера переменной. В Си при объявлении переменным «назначается» место хранения и область видимости: локальная или глобальная. Разница между ними в том, можно ли получить значение переменной из других частей программы. Есть четыре различных типа переменных (применительно к их области видимости):
  • extern — хранятся в «общем хранилище» и имеют глобальную область видимости
  • static — хранятся в «общем хранилище» и имеют локальную область видимости
  • auto — хранятся в «локальном хранилище» и имеют локальную область видимости
  • register — хранятся в регистрах и имеют локальную область видимости

Когда я говорю, что переменная храниться в «общем хранилище», это значит, что такой переменной присваивается абсолютный адрес в оперативной памяти. В ходе работы программы этот адрес остается неизменным. Для переменной, сохраненной в «локальном хранилище», выделяется место в стеке. Когда ты указываешь в качестве места хранения переменной register, ты говоришь компилятору, что он должен сохранить переменную в одном (или нескольких) из регистров МК. Однако если компилятору не удается расположить переменную в регистрах, он переопределяет переменную как auto.

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

Переменная, объявленная как extern, по сути, является глобальной, но не требуют резервирования места в памяти. Такое объявление говорит компилятору, что для этой переменной уже зарезервирована память во время объявления глобальной переменной с этим же именем в другом файле. Глобальные переменные имеют глобальную область видимости и доступны из любого модуля программы. Для того чтобы иметь доступ к переменной, нужно объявить её как extern. Пример будет ниже.

Переменная, объявленная как «авто» обычно классифицируют как локальную переменную. Локальные переменные имеют локальную область видимости, которая ограничивается фигурными скобками (т. е. {...}). К таким переменным не возможен прямой доступ из других функций, которые определены вне фигурных скобок. Переменная, определенная в рамках набора фигурных скобках и без идентификации класса по умолчанию будет объявлена как auto. Это объясняет, почему мы редко видим ключевое слово auto в листингах программ.

Статические переменные (объявленные как static) редко используются многими программистами потому, что они не понимают преимуществ, которые дает такое объявление. Такие переменные сохраняются в оперативной памяти, причем их адрес остается неизменным на протяжении всего времени работы программы. При этом область видимости этих переменных ограничена блоком, в котором они объявлены.
Другими словами,
1) к статической переменной, объявленной в пределах функции, можно получить прямой доступ только из этой функции. Еще раз повторюсь, что область видимости ограничивается фигурными скобками;
2) к статической переменной, объявленной в пределах модуля, можно получить прямой доступ только из этого модуля.

Обратите внимание, что я использовал фразу «прямой доступ». Я слышал много раз, что применение глобальных переменных зачастую необходимо. Это может иметь место в некоторых случаях, однако я считаю, что программисты часто упускают из виду мощный инструмент языка Си, предназначенные для доступа к переменным – указатели. К статической переменной, объявленной в пределах модуля, можно получить доступ из внешней функции, если указатель на эту переменную будет передан во внешнюю функции внутренней функцией (внутренней для модуля, в котором объявлена наша статическая переменная).
Разумное использование статических переменных поможет предотвратить непредвиденное изменение глобальной переменной другими функциями и повысить общую надежность кода.

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

Примечание от переводчика.
Статья достаточно поверхностная. Для разных архитектур приведенные выше рекомендации могут быть и не совсем верными. Всегда следует четко понимать, что делает та или иная команда на именно твоем МК. К примеру, совет использовать 8-битную переменную на 32-битной архитектуре может привести не только к уменьшению количества используемой памяти, но и к снижению быстродействия. Произойти это может из-за того, что твоему МК может потребоваться значительно больше инструкций, чтобы получить, к примеру, 3-й байт 32-битного регистра. Т.е. появляется дилемма: низкий расход памяти и низкая производительность или высокий расход памяти и более высокая производительность. Спасибо ks_ , John , e_mc2 , angel5a .

Я не старался переводить дословно текст автора. Перед собой я ставил задачу донести основной смысл. Поэтому возможно не полное соответствие тексту оригинала.

Ссылка на оригинал.
  • +4
  • 14 декабря 2012, 13:38
  • do_sl

Комментарии (89)

RSS свернуть / развернуть
32-битный процессор в принципе может не иметь 1-байтовых регистров и не уметь извлекать из ОЗУ по 1 байту, читает сразу по 4. И инструкций арифметики однобайтовой нет. И получается целая куча команд. Это как если тип boolean хранить в одном бите. Память сэкономишь, а в прозводительности потеряешь сильно. Поэтому boolean и имеет разрядность процессора, прям сразу 32 бита.

Насчет static одобряю. Сам всегда использовал.
+1
  • avatar
  • ks_
  • 14 декабря 2012, 17:00
Про 1-байтовые переменные — как хорошо, что вы ошибаетесь.
0
К сожалению он не заблуждается. Обратите внимание на мой комментарий ниже.
0
Нашел описание, из-за которого у меня и возникли сомнения по поводу 1-байтных регистров, ссылка на сайт бигбоса, но это я не подлизываюсь, это первый адекватный ответ гугла.
Глава 2.3.6.
… Однако ранние версии ЦПУ ARM имеют недостаток в том, что
они могут осуществлять доступ к памяти с выравниванием только по словам и
полусловам.

ЦПУ Cortex имеет режимы адресации для слов, полуслов и байт, но он может
осуществлять и невыровненный доступ к памяти. Это дает компоновщику
дополнительную свободу при размещении данных программы в памяти. За счет
поддержки битовой сегментации, ЦПУ Cortex может упаковывать программные
флаги в переменные формата полуслова и слова и не использовать отдельный
байт для каждого флага.
То есть Cortex'у M3 действительно все равно, что читать-писать — бит, байт, полуслово или слово, или там все же есть разница в скорости доступа?
0
Расскажу по х86 интел: при попадании слова на границу, производится две выборки с памяти (два такта шины данных), и конкатенация результата (это по идеи «бесплатно»). Т.е. время доступа к таким переменным увеличивается.
Старые армы генерировали исключение, и указанная последовательность выполнялась осью програмно. В итоге получали «катастрофическое» снижение производительности.
Cortex же презентует одинаково быструю работу как с выровненными, так и с невыровненными данными. Специально не проверял, а на своем коде разницы не замечал.
0
Несколько дополнений. Типы [u]intN_t определены в стандартном заголовочном файле stdint.h вашего компилятора. Кроме того, там же определены типы [u]int_leastN_t и [u]int_fastN_t.

Тип [u]int_leastN_t используется на платформах, где отсутствует соответствующий тип [u]intN_t. В случае его присутствия, типы [u]intN_t и [u]int_leastN_t эквивалентны.

Типы [u]int_fastN_t на некоторых платформах могут поднять производительность. Обычно используются в критичных для производительности местах, например, в циклах. К примеру на ARM тип uint_fast8_t имеет разрядность 32 бита, в результате не будет лишних маскирований, на AVR переменная такого типа будет иметь разрядность 8 бит, что помогает писать переносимый код, в данном случае не потеряли ни в скорости, ни в размере памяти под переменную.

В типах intN_t для представления отрицательных чисел используется дополнительный код (two’s complement).
0
  • avatar
  • John
  • 14 декабря 2012, 19:21
А Вы сейчас про какой ARM говорите? В комментарии выше привел цитату из обзора ядра Cortex M3. Я, честно говоря, в изучении контроллеров на М3 дальше пункта «настроить АЦП, таймеры и прерывания» не заходил, но, все же, интересно, верна ли приведенная выше цитата или это просто маркетинговая простыня с большим количеством маленьких звездочек?
0
Про любой ARM. Конечно, в некоторых случаях при размещении типов [u]int_fastN_t в памяти мы можем потерять в размере памяти под переменную, например, на ARM возможно разместить две переменные типа uint8_t по соседним адресам ОЗУ, расход памяти — 2 байта. В случае использования типа uint_fast8_t расход памяти — 4 байта.

Теперь посмотрим на примеры кода и соответствующий им ассемблерный листинг для Cortex-M3 компилятора Keil. Видно, что во втором случае нет лишней команды маскирования:

for (uint8_t counter = 0; counter < 10; ++counter) {}

                  |L1.2|
000002  1c40              ADDS     r0,r0,#1
000004  b2c0              UXTB     r0,r0
000006  280a              CMP      r0,#0xa
000008  d3fb              BCC      |L1.2|
for (uint_fast8_t counter = 0; counter < 10; ++counter) {}

                  |L1.2|
000002  1c40              ADDS     r0,r0,#1
000004  280a              CMP      r0,#0xa
000006  d3fc              BCC      |L1.2|

Стандарт говорит только, что типы [u]int_fastN_t обычно быстрее для работы с собой по сравнению с другими типами, причём это не гарантируется для всех случаев использования.
+2
В случае использования типа uint_fast8_t расход памяти — 4 байта.
8 байт, конечно.
0
Автор заточен на АВР и проводит оптимизацию для конкретной платформы, убыточные для другой.
Ниже приведена моя более эффективная реализация цикла for:
Ложь для STM8 и Cortex-M0/M3/M8… Обоснование:
— STM8 имеет 1 8-битный регистр и 2 16-битных. В итоге любая операция с использованием 8-битных значений вытолкнет счетчик из регистра, а следовательно получаем дополнительные такты на загрузку-выгрузку, и дополнительное место в памяти (а она то «является ограниченным ресурсом»). В то время как int-счетчик может свободно сидеть в регистре при 8-битных операциях (не всех) и даже наличие 16-битных (второй регистр приходит на помощь).
— Cortex — отсылаю к 32-битному быстрому чару, который упомянул John выше (и это даже не х86, где к тому же потребовалось бы расширение переменной).
Любой хороший программист на ассемблере знает, что все процессоры имеют ассемблерную команду проверки на ноль.
А опытный программист, имевший дело с более чем одним компилятором, знает, что:
Компилятор языка Си не эффективней, чем хороший программист на ассемблере.
Но тем не мение способен самостоятельно перевернуть/развернуть/оптимизировать цикл, благодаря наличию в нем оптимизатора, более эффективного. Кроме того прямой цикл может выполняться так же как и обратный (не у всех архитектур есть отдельная команда сравнения на нуль).

Далее автор совершенно не в курсе что бывают платформы, на которых char имеет размер 2 байта. Именно по этому и присутствует неопределенность в размерах типов.
Однако существуют и аппаратные платформы, в которых размер байта равен размеру слова (или, в терминах языка C: sizeof(char) == sizeof(int)). Например, в суперкомпьютерах Cray.
Отсюда (на моей памяти приводился какой-то 16-битный моторолловский проц, но пруф не дам).

Ну и прочая подтасовка фактов в целях собственной выгоды. Аж читать противно.
+3
Автор статьи пишет не о AVR, а о Freescale. Ну да ладно, это мелочь.
Ну и прочая подтасовка фактов в целях собственной выгоды. Аж читать противно.
Ну не будьте настолько категоричны. Вообщем то эта статья рассчитана на начинающих. Гуру Си и так прекрасно знакомы с подобными моментами. Если же начинающий попробуем объявить переменную как uint8_t и попробует посмотреть определение нестандартного и незнакомого ему типа, то IDE покажет ему простынь подобных определений «легко переносимых» типов, как целых, так и с плавающей точкой. Честно говоря, когда я впервые увидел эту кучу определений, у меня возник вопрос «А зачем так много?». Мне кажется, что статья в какой-то мере отвечает на этот вопрос. Ну или хотя бы дает отправную точку для размышлений. Что уже хорошо. А уже потом бывший начинающий уже сможет решить для себя, как ему писать код.

За комментарий спасибо.
0
До ссылок с Freescale я не дочитал, сил не хватило на тот момент. Впрочем и оборот «заточен на АВР» имел смысл «работы с определенной платформой». :)
А вот именно то что статья расчитана на начинающих, и автор, пользуясь положением, выставляет себя гуру и искажает факты — больше всего и раздражает.
И я ведь разделяю взгляды автора (кроме взглядов на компилятор Си):
В сравнение с ассемблером, код, написанный на Си, МОЖЕТ БЫТЬ:
...
Намного проще писать хороший код на Си, который потом будет преобразован в правильный ассемблерный код, чем писать программу сразу на ассемблере.
Но упомянутые раннее моменты очень сильно портят статью. Такого автора я уже не могу посоветовать начинающим программистам.
0
Согласен с Вами. Особенно с замечанием относительно

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


В первых Pentium появился U и V конвейер, притом U был основным, а V как-бы «вспомогательным». Теоретически это позволяло писать код, который бы «распараллеливался» при определенных условиях (на самом деле, речь идет об оптимизации выборки инструкций, а не о распараллеливании как в многоядерных профессорах).
Но набор правил и исключений для распараллеливания инструкций был насколько большой, что удерживать это все, в голове, программисту на ASM было нереально. А вот компиляторы запросто справлялись с этим множеством правил и исключений и результат компилятора намного превосходил результаты хорошего программиста на ASM.
+1
Хороший пример.
А ещё можно вспомнить про баги процессоров навроде: команда не может находиться в первых 8 байтах от границы страницы, что-то там с условными переходами. Это конечно шедевр, но и других хватает.
Но всё же что бы быть честным, автор тоже был прав. Первые компилятору достаточно неоптимальны, и создавали не лучший код. Но они то развиваются, в отличии от написанной статьи. Та же, некогда невероятно полезная, книга про алгоритмические трюки — сейчас компиляторы практически всё это делают самостоятельно. А вот программист не каждый её читал.
+1
и это даже не х86, где к тому же потребовалось бы расширение переменной
У x86 же вроде предусмотрена работа с байтами? По крайней мере к регистру x86_32 ЕМНИП можно обращаться как к байту, слову или двойному слову.
0
Да, х86 может загружать байты/слова. Но для проведения операций с ними (сравнить с константой в частности) надо «расширить значение» (придуманный термин) — вписать в старшие байты регистра нули, что есть дополнительная комманда.
Впрочем мои знания в этой области уже устарели. Последний раз с ассемблером х86 имел дело лет 5-7 назад. Поэтому где-то могу и приврать :)
0
статью не читал, прочел каменты и так и не понял
у меня все витал вопрос про чар и инт в 32 битных процессорах
автор там сразу же начинает про инт на обычных х86
быдлодуинщики тоже в циклах ставят инт

вот я и подумал, а для 32 битных наверное нет разницы 8 или 32, ведь за раз он обрабатывает все равно 32 бит
или я чего не понял?
0
зависит от архитектуры. на х86 можно использовать команды типа mov{b,w,l}, которые явно указывают размер операнда(8,16,32 бита) и можно обращаться к регистрам разного размера — RAX > EAX > AX > AL (A). За ARM я такого не помню, хотя не копал глубоко
0
Этих писателей развелось… Нельзя давать такие однозначные советы про размерность данных, выравнивание итп, потому, что все зависит от компилятора и линкера. Хотите оптимального расхода памяти — читайте доку на ваш тулчейн, где все подробненько описано. Обычно, по умолчанию, линкер выравнивает адресацию на границу слова, чтоб избежать двойных выборок.
Про статик. Этот весьма смелый совет, что его нужно везде использовать, может дать только полный дебил. Использование статических переменных внутри функций, равно как и бездумное использование глобальных переменных ведет к переасходу оперативной памяти, которой в МК и так не много. Статические переменные нужно использовать только в случае необходимости. Если писать программы для ПК с 100500 гиг оперативной памяти, можно не заморачиваться, а вот когда ее (памяти) всего 8 килобайт…
Про ассемблер. Знать его необходимо, ибо незнание этой основы привело к появлению армии быдлокодеров, которые даже понятия не имеют что и как работает.
0
Статик весьма хорошая вещь для локализации глобальных переменных и функций внутри модуля. В функции её пихать стоит только при особой необходимости данного действа…

static и extern ещё понятно (хотя я extern уже редко использую), но что дают спецификаторы register и auto? Есть дли смысл их использовать в коде?
0
Глобальные переменные сами по себе статические, те объявление глобальных переменных, как статик, не делает ровным счетом ничего, тк память однозначно выделяется. В функциях переменные, которые не являются статическими, могут размещаться в стеке, таким образом происходит экономия памяти. На самом деле механизм сильно зависит от платформы, а также от реализации компилятора и его настроек.
Спецификация register заставляет компилятор хранить переменную в регистре. Имеет смысл для переменных, которые очень интенсивно используются, чтоб избежать генерации кода перегрузок память-регистр-память. Особого смысла в этом, скорее всего нет, тк компиляторы сейчас достаточно «умные» и прекрасно разбируться сами, как оптимальнее распределить переменные. Не факт, что на ассемблере можно сделать лучше, по крайней мере — значительно.
0
те объявление глобальных переменных, как статик, не делает ровным счетом ничего, тк память однозначно выделяется
static — сомнительной удачности слово. На самом деле он означает «локально видимая глобальная переменная». Т.е. глобальная static переменная не экспортируется из скомпилированного файла и является глобальной только для файла где объявлена. Так что смысл есть, еще какой.
0
Ага, для этого и использую.
0
я про символические имена ничего не писал, лишь о выделении памяти. при чем здесь область видимости?
0
При том, что речь о модификаторе static, а это модификатор именно области видимости и только ее.
0
Правда? Особенно для локальных переменных функций?
0
Static по определению локальным не бывает. Это глобальная переменная с ограничением видимости.
0
Откуда такие глубокие познания? Прочтите сначала это en.wikipedia.org/wiki/Static_variable
0
Там сказано то же самое, пусть и в других формулировках.
0
Я в шоке. Вы где учились? Читать не умеем? Статические переменные изначально предназначены для ЛОКАЛЬНЫХ переменных, лишь только в С был рсширен смысл спецификатора на глобальные идентификаторы переменных и функций.
Static local variables: variables declared as static inside a function are statically allocated, thus keep their memory cell thoughout all program execution, while having the same scope of visibility as automatic local variables, meaning remain local to the function. Hence whatever values the function puts into its static local variables during one call will still be present when the function is called again.
Это понятно?
0
Если мне не верите, почитайте Кернигана и Ричи. Им доверяете?
0
Дай ссылку или выдержку. А заодно — что они говорят о модификаторе static для глобальных переменных и выборе столь странного ключевого слова для задачи ограничения видимости глобальной переменной.
0
Жжжжжешь!
Static по определению локальным не бывает. Это глобальная переменная с ограничением видимости.
Это кто сказал? По определению не бывает…
Учитесь признавать свои ошибки.
0
ОК, я выразился недостаточно однозначно. static не бывает локальным в понятиях времени жизни и области хранения в языке С.
Вот ссылка lib.ru/CTOTOR/kernigan.txt ищем ключевое слово статик
А там то же самое по сути говорится — статическая переменная по свойствам аналогична глобальной (точнее, extern, понятия «глобальная переменная» так как такового нет, как впрочем и «локальная» — вместо нее auto), но с ограниченной областью видимости. Кроме того, я совсем забыл о такой прелести как static функции, где это ключевое слово в принципе не может выполнять иные функции, чем модификацию области видимости.
0
P.S. К вопросу о признании ошибок.
0
Вот ссылка lib.ru/CTOTOR/kernigan.txt ищем ключевое слово статик
0
Мы вроде как говорим не о статических переменных вообще, а о ключевом слове static применительно к С. Если говорить о static переменных в функциях — это определение вполне подходит (хотя понятие локальных/глобальных переменных обычно включает не только область видимости, но и срок жизни), но в С есть также статические глобальные переменные (хотя они по определению статичны). Кроме того, применительно к С, статическая переменная функции ведет себя абсолютно идентично глобальной переменной, за тем исключением, что у нее ограничена область видимости. Таким образом приходим к тому, что приходится или отдельно определять static для каждого варианта использования, или определить его как модификатор видимости глобальной переменной. Последний вариант заодно охватывает и статические переменные класса (которые в остальных языках вообще называют «переменная класса» и к статическим не относят).
Использование же только твоего определения приводит к заявам в духе «делать глобальные переменные статическими нет никакого смысла — они и так статические», что явно неверно (область видимости всегда желательно ограничивать минимально необходимой).
0
Использование же только твоего определения приводит к заявам в духе «делать глобальные переменные статическими нет никакого смысла — они и так статические», что явно неверно (область видимости всегда желательно ограничивать минимально необходимой).
Кстати, насколько я вижу, автор советует использовать static именно для этой цели, и он прав. Вот только или он, или переводчик изложил это недостаточно ясно, можно подумать что автор предлагает заменять static'ами не глобальные переменные, а локальные.
0
Я просто хочу сказать, что вот из-за таких мелочей появляются ложные убеждения. Один неточно выразился, другой перепечатал, потом и в учебники может попасть. А все потому, что авторы не сильно разбираясь в сути вопроса, перепечатывают ошибки друг у друга, даже не подозревая, что это ложь.
Без обид:) Я просто такого насмотрелся… Особенно жгут преподы вузов в своих методичках. У нас парень на работе учится, приносил, весной еще, сильно доставил :)
0
Твое утверждение было еще более ложно, чем авторское. Его утверждение было верным, но неясным, твое ясное, но неверное.
0
Классно вывернулся, вопросов нет :) Расслабся, не на экзамене ведь. Те твое утверждение, что «static локальным не бывает» еще в силе?
0
Применительно к С и в плане времени жизни/выделения памяти/поведения — да. Не в плане области видимости (из-за того, что в современных языках много градаций по области видимости и переменные бывают «совсем локальные», «локальные», «не совсем локальные», «локальные, но немного глобальные», «почти глобальные, но не совсем», «стопудово глобальные» привык понимать под локальными стековые, а под глобальными — выделенные в пуле).
0
Тогда прокомментируй это:
int test(void)
{
static int i=0;
return i++;
}

int main()
{
printf("%d\n", test());
printf("%d\n", test());
printf("%d\n", test());
printf("%d\n", test());
return 1;
}
0
И что тут комментировать? i — глобальная (по размещению) переменная, видимая только в функции test и инициализируемая нулем. Программа выведет последовательность «0 1 2 3» и отчитается ОС о завершении с ошибкой.
0
Глобальная? Садись 2
0
Спасибо, мне хватило предыдущего экзаменатора с его «байт — единица информации? садись, два». Я специально уточнил, в каком смысле глобальная, потому как понятие это давно уже не однозначное.
0
Хорошо, тогда кто здесь кто?
static int i;

int test(void)
{
static int i=0;
return i++;
}

int main()
{
i = 99;
printf("%d %d\n", test(),i++);
printf("%d %d\n", test(),i++);
printf("%d %d\n", test(),i++);
printf("%d %d\n", test(),i++);
return 1;
}
0
Две глобальных (по размещению) переменных с разными областями видимости. Вывод 0 99, 1 100, 2 101, 3 102.
0
P.S. А по области видимости в обоих примерах все переменные локальные. Одна локальна для функции, другая локальна для файла.
0
Вот соль. Глобальная/локальная — это какраз и есть область видимости. Зря авторы языка использовали «статик» для ограничения видимости идентификаторов. Надо было применить что-то вроде local. Тогда и путаницы меньше было-бы.
0
Вот насчет путаницы — это точно. Ну и статическая переменная строго говоря локальной не является — локальная переменная в языках с рекурсией выдается новая при каждом входе в функцию.
0
Давайте, чтоб не было путаницы, прекратим бесполезный спор.
Первая переменная — глобальная (относительно текущего файла), тк к ней может получить доступ любая функция из файла.
А вторая — локальная, к ней можно получить доступ только из функции test().
Так меня в школе учили.
А что касается размещения, то лежать в памяти они все будут рядом, особенно в системах с разными областями памяти программ и данных.
0
Первая переменная — глобальная (относительно текущего файла)
Но относительно проекта в целом она локальна.
А что касается размещения, то лежать в памяти они все будут рядом, особенно в системах с разными областями памяти программ и данных.
Ага. И глобальная переменная extern int i будет лежать там же. А вот объявленная в функции локальная переменная int i будет выделяться в рантайме на стеке, и в этом плане static'и явно ближе к глобальным.
0
Относительно проекта? Да возможно, если он не состоит из одного файла :)
Вы не сможете объявить static int i и external int i в одном файле. Посему практика весьма порочная, как я и писал раньше. Хорошим тоном будет использование уникальных имен. И ваще, использовать глобальные переменные желательно только в случае необходимости. Это в МК пошла мода срать на хороший тон, ввиду ограниченных ресурсов.
Вы только не обижайтесь. Спор — штука нужная :)
0
В одном файле — нет. А вот написать в одном int i, а в другом static int i по идее (на практике не проверял) вполне можно. «Не совсем глобальная» static int i перекроет «труЪ глобальную» в том файле, где объявлена.
Да возможно, если он не состоит из одного файла :)
Ну в этом случае и разницы между переменной файла (так ее вообще-то и следовало бы формально называть, но в С/С++ их все скопом в статические записали) и глобальной нет.
0
это правильно, но таких ситуаций лучше избегать. локальная переменная создает так называемую область затенения, закрывая собой видимость глобальной переменной. В идеале, глобальных переменных быть не должно. Однако в МК — это не всегда возможно, особенно в обработке прерываний.
0
Однозначно плюсую все Вашы коментарии.
0
Интересно, если static — это модификатор именно области видимости, то как он модифицирует область видимости в следующих примерах:

func()
{
    char var;
    ...
}

func()
{
    static char var;
    ...
}

Или, то, что во втором варианте можно из функции вернуть указатель и дальше использовать его где угодно и является, тем, что вы называете модификацией области видимости используя static?
0
Ну тут оно как раз сильнее всего ограничивает область видимости — одной лишь функцией, хотя физически var — вполне себе глобальная переменная.
0
Получается, что в первом случае то, что область видимости переменной одна функция это заслуга ее объявления внутри функции, а во втором случае это заслуга наличия static?
0
Судя по нормальным книгам, как это понял я — в данном примере static объявляет переменную как глобальную но ограничивает ее видимость только в функции. То есть значение переменной сохраняется между вызовами функции но получить прямой (еще раз прямой) доступ к ней за пределами функции нельзя.
0
Но почему вы ограничение области видимости статических переменных делаете заслугой модификатора static?
Почему во втором примере, тот факт, что переменная так и осталась видима внутри функции уже не является следствием места ее определения, а начинает считаться заслугой static?
Вот еще два участка кода:

func()
{
    static char var;
    ...
}


static char var;
func()
{
    ...
}

Определение переменной не изменилось, но поменялась область видимости. Разве наличие static меняет область видимости между этими примерами?
0
Но почему вы ограничение области видимости статических переменных делаете заслугой модификатора static?
Это трюк, чтобы уяснить смысл модификатора static в остальных случаях его применения. Вот только в первом комменте на эту тему я выразился слишком безапеляционно и с достаточно неудачной формулировкой)
0
А можно привести какой-нибудь простейший пример кода, где не хватило бы обычного определения статической переменной типа такого
static variable is a variable that has been allocated statically — whose lifetime extends across the entire run of the program.
И для понимания смысла потребовалось бы еще дополнение о изменении области видимости?
0
Да пожалуйста.
static in i = 0;

int foo(void){
  return ++i;
}

static void bar(int _i){
  i = _i;
}

Зачем (и куда) расширять lifetime переменной, которая и без того существует все время жизни программы? И, тем паче, функции-то куда lifetime расширять?
0
Расширять конечно же некуда т.к. уже достигнут предел расширяемости. Но где тут модификация области видимости переменной i относительно этого же фрагмента но без static? Или вы имеете ввиду, что если присутствует static то нельзя сделать обратиться к этой переменной из другого файла через extern?

Насчет функции, я вопросов не имею, там действительно static модифицирует область видимости и больше ничего не делает.
0
Или вы имеете ввиду, что если присутствует static то нельзя сделать обратиться к этой переменной из другого файла через extern?
Именно.
0
теперь понятно, что имеется ввиду.
но тогда получается, добавление об ограничении области видимости для статических переменных объявленных внутри функций является избыточным т.к. само место их определения уже ограничивает область видимости.
0
Ну, static переменная внутри функции не стековая, как обычная локальная, она выделяется глобально. Но ее область видимости ограничена функцией. Т.е. static переменные — это статически выделенные переменные с ограниченной областью видимости.
0
Запомните несколько правил «хорошего тона»
1. Нужно стараться объявлять переменные только внутри функций, если это возможно.
2. Если нужно, чтоб переменная сохраняла свое значение после выхода из функции, объявляем ее как static.
3. Если переменная объявлена вне функции (глобально), но мы хотим ее сделать видимой только внутри текущего файла — тоже объявляем ее static. Выглядит немного нелогично, но все вопросы к авторам стандарта. Я бы применил что-то типа «local», но я не автор стандарта :)
0
Если бы static использовался только для статических переменных — его было бы разумней рассматривать как модификатор времени жизни. Но
Таким образом приходим к тому, что приходится или отдельно определять static для каждого варианта использования, или определить его как модификатор видимости глобальной переменной.
0
Делает, но об этом уже сказали =)

Спецификацию я знаю относительно них, накой это надо, но тоже сомневаюсь в пользе от их использования. Вряд ли компилятор захочет запихать итератор цикла в стек…
0
смотря на какой архитектуре. в принципе может, если регистров мало.
0
Этот весьма смелый совет, что его нужно везде использовать, может дать только полный дебил.
Извините, а где Вы нашли такой совет в статье? Там вроде написано, что объявлять переменные как static нужно с умом. По поводу Вашего комментария: а Вы не подумали, что
Я слышал много раз, что применение глобальных переменных зачастую необходимо. Это может иметь место в некоторых случаях, однако я считаю, что программисты часто упускают из виду мощный инструмент языка Си, предназначенные для доступа к переменным – указатели. К статической переменной, объявленной в пределах модуля, можно получить доступ из внешней функции, если указатель на эту переменную будет передан во внешнюю функции внутренней функцией (внутренней для модуля, в котором объявлена наша статическая переменная).
 Разумное использование статических переменных поможет предотвратить непредвиденное изменение глобальной переменной другими функциями и повысить общую надежность кода.

как раз про это?

Да, я не спорю, что статья далеко не однозначна. Иногда даже не только не полезна, но и вредна. Как раз поэтому я и добавил ссылки на комментарии ниже. Последний абзац «примечание переводчика тоже никто не отменял».
0
Эту статью еще не читал, но полистал первую часть на лоцмане, еще когда она в рассылке пришла. Честно говоря, не понравилось, я ее даже не дочитал. В общем, «не читал, но осуждаю»)
0
  • avatar
  • Vga
  • 15 декабря 2012, 14:35
Впрочем, я был неправ. Статья вполне неплохая. Только у нее серьезные проблемы с ясностью изложения, плюс к тому в неоднозначных случаях автор безапеляционно советует один конкретный вариант (например, предпочесть расход ОЗУ скорости), хотя этот выбор сильно зависит от задачи.
0
А вот перевод так себе. Если и оригинал не отличался ясностью изложения, то в переводе стало еще хуже.
Часть про портирование — очень кривая. Но я затруднюсь сформулировать правильный перевод. Впрочем, там еще много кривых мест, для которых я затруднюсь подсказать нормальный перевод, хотя и вполне понял, о чем говорит автор.
(наверно, имеется в виду малый размер бинарного файла после компиляции)
Сильно сомневаюсь. Строго говоря, это свойство не языка, а компилятора. К тому же, ты потерял кусок фразы. Там сказано «Язык С — относительно компактный, имеет слабую типизацию и поддерживает низкоуровневые побитовые манипуляции с данными».
0
Вы правы, с некоторыми фразами возникали затруднения. Что-то мог и потерять. Добавлю Ваш перевод.
0
Ни один стандартный процессора не имеет команды проверки, равна ли переменная 100
51 имеет (cjne).
0
  • avatar
  • Katz
  • 17 декабря 2012, 12:01
не знаю, как это в 51 реализовано, но в х68, к примеру JZ/JNZ действительно эффективнее будет, т.к. чтобы вычесть 1 из числа, надо погрузить его в регистр. При этом если результат стал нулем, взведется флаг Z и JZ сработает, в то время как JE/JNE и другие сначала должен будет выполнить сравнение (CMP)
0
JE/JNE
Вообще-то это и есть JZ/JNZ, только под другой мнемоникой. CMP нужен тогда, когда не требуется реальное вычитание. Но да, для цикла вверх потребуются команды INC EAX; CMP EAX, 100; JNZ LOOPSTART, а для цикла вниз только DEC EAX; JNZ LOOPSTART (а то и вовсе через специальную команду, которая декрементирует ECX и прыгает на метку, если ECX не 0). По этой причине компиляторы для x86, обнаружив что цикл можно мотать в любую сторону разворачивают его.
0
Очень хочется поверхностно понять принцип работы компиляторов. Если у кого есть хорошая книжка для нубов в области компиляции — подкиньте пожалуйста!!!
Только огромна просьба не кидать талмуд на 2,3 тома или умничать (ой еще один выскочка хочет пернуть и не обосраться) — лучше промолчать.
Благодарю!
0
Хорошие книжки в области компиляции как раз и будут на 2-3 тома. Ахо и Ульман, например. Есть у меня одна очень толковая (не смотря на возраст) книжка размером в один том, но название и автора я сейчас на вскидку не вспомню, а посмотреть смогу только на выходных.
0
Буду очень благодарен за Автора и название книги!!!
Про хорошие книги я в курсе. Но вот пока хочу только познакомится что да как. Изучать все основательно нет времени, и если честно, пока не вижу смысла так глубоко копать.
0
Глубоко копать, по большому счету, особо и не получится, даже Ахо и Ульман это только основы, хотя и весьма основательные. С тех времен теория и практика здорово продвинулись, а JIT-компиляция (тем более отложенная, как в хотспоте и, подозреваю, некоторых других ВМ-ах) здорово все перемешала. Если не терпится, то советую почитать, для начала, что-нибудь о формальных грамматиках и их разборе. Иначе слова типа LR(1), LL(k) или LALR(1) будут ничего не значащими аббревиатурами, а словосочетания типа «табличный разбор» или «рекурсивный спуск» будут вводить в ступор :)
0
Ну, во первых — уже порекомендованные Ахо, Ульман, Сети. Двухтомник «Теория синтаксического анализа, перевода и компиляции» и книга «Компиляторы: принципы, технологии, инструменты», известная также как «книга красного дракона». Объемное, но самое правильное и каноничное.
Во вторых… Есть еще какая-то книга по построению компиляторов, довольно простая, но не помню автора и названия. Ща гляну… Не нашел. Возможно это «Лекции по построению компилятора на Pascal. Автор неизвестен», а может и что-то другое.
Ну и в третьих — С. З. Свердлов (З. — Залманович :) — «Языки программирования и методы трансляции».
Есть еще книга Compiler Construction за авторством Никлауса Вирта (Niklaus Wirth), но ее я не читал. Ее можно официально бесплатно скачать в виде PDF'ки, не помню только где.
Нечто по теме обещает и Кнут, но это еще неизвестно когда (и еще неизвестно, успеет ли вообще написать, такими-то темпами).
0
Робин Хантер «Основные концепции компиляторов» 256 страниц (ну дык, для прграммистов же писанная).
0
А сразу на «скачать» нельзя было линк дать?
0
Ссылка на озон дана, что бы однозначно определить книгу. На «скачать» нельзя — у меня бумажная версия.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.