Архив 9 мая 2011

volatile const

Увидел в Твиттере искреннее удивление по поводу того, что в C-подобных языках программирования вынесеная в заголовок поста конструкция является допустимой. В самом деле, как константа (const) может быть “изменчивой” (volatile)?

Дело в том, что в C нет понятия “константы”. const – если верить Кернигану и Ритчи – не более, чем “квалификатор”, то есть подсказка компилятору, что описываемый объект не может явным образом быть изменен в программе. Это повод ругаться нехорошими словами при попытке скомпилировать программу и не менее хороший повод применить какую-нибудь оптимизацию – Керниган и Ритчи говорят, например, о возможном размещении объекта в памяти, доступной только для чтения. Я даже не озвучиваю очевидного – можно вообще не создавать объект, а при его использовании просто подставлять заданное значение. Впрочем, согласно стандарту C, компилятор должен только ругаться при явных попытках присвоить значение переменной, объявленной как const. Во всех остальных случаях слово const можно игнорировать.

volatile обозначает несколько другое. Точное значение этого “квалификатора” зависит от реализации компилятора, он может быть даже проигнорирован. Обычно volatile – это указание на то, что описанный объект может изменяться не предусмотренным в программе способом, и следовательно, не должен “выбрасываться” при оптимизации. Тут обычно люди сильно удивляются, недоумевают и решают не засорять мозги таким непонятным словом. Как это – “изменяться не предусмотренным в программе способом”? Что же это такое творится, когда компьютер живет своей собственной независимой от программ жизнью и волшебным образом меняет значения переменных в программе?

Сейчас я буквально в двух строчках объясню, что же на самом деле означает volatile. Дело в том, что при программировании “на уровне железа” очень часто применяется memory-mapped IO – ввод-вывод с отображением на память. Допустим, в микроконтролере (наподобие PIC или AVR) байт памяти с номером 0xN содержит значение, которое намерил аналогово-цифровой преобразователь. А теперь давайте напишем цикл, который будет крутиться до тех пор, пока АЦП не намерит превышение параметра.

unsigned char *pADC = 0xN;
while(*pADC <128){
// fuck around for a while
}
// panic and die

Давайте подумаем, что же сделает с нашим циклом оптимизирующий компилятор? А он умный, он превратит все это в такую конструкцию, где значение АЦП будет считываться только один раз (тут я пишу на псевдокоде c GOTO – надо ли напоминать, что в “железе” циклов нет?):

IF memory[0xN] >= 128 GOTO panic;
loop: FUCK AROUND
GOTO loop
panic: ВСЕ ПРОПАЛО!

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

volatile unsigned char *pADC = 0xN;
while(*pADC < 128){
// fuck around for a while
}
// panic and die

К счастью, времена, когда программист должен был знать про MMIO, давно прошли. Алена C++ очень правильно заметила:

вообще можно всю жизнь программировать на C++, но с volatile так и не встретиться

Теперь про const. Прикрутим его к нашему примеру с АЦП. Нетрудно догадаться, что попытка что-то записать в область памяти, отведенную для АЦП, может закончиться весьма печально (а может, и нет). Допустим, что в этом случае процессор просто остановится (вполне разумное, кстати говоря, поведение). Хотелось бы иметь механизм, сигнализирующий программисту о недопустимости выполняемых им действий. Особенно "интересно" такое поведение будет выглядет в том случае, если случайная запись происходит редко при "трагическом стечении обстоятельств". Нет проблем! const как раз вынуждает компилятор ругаться в случае присваивания чего-то объекту с этим квалификатором. Например, программа (с очевидной ошибкой)

volatile unsigned char *pADC = 0xN;
while(*pADC = 128){
// fuck around for a while
}

успешно скомпилируется, но будет работать неправильно. Более того, разные компиляторы могут "наоптимизировать" совершенно по-разному. Впрочем, в случае зависающего при записи процессора это нам совершенно безразлично. Далеко не всегда компилятор будет выдавать warning на конструкцию типа while(*pADC = 128), особенно в случае какой-нибудь экзотики. А обнаружить такого рода ошибку позволит тривиальное исправление:

volatile const unsigned char *pADC = 0xN;
while(*pADC = 128){
// fuck around for a while
}

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

const int a = 0;
*((int*)&a) = 1;
printf("%i", a);

будет печатать либо 1, либо 0 в зависимости от настроек оптимизации. Замечу, что поведение данной конструкции может изменяться и при замене типа переменной a с int на, допустим, float.

Я специально сделал оговорку об i386. Есть архитектуры, где не вся память доступна для записи. Например, вышеупомянутые AVR имеют (семейство Mega) 8 кБ ОЗУ и целых 256 кБ флеш-ПЗУ. Практически все компиляторы для этой архитектуры стараются поместить константы в ПЗУ, иногда, правда, для этого им нужно "ткнуть пальцем", но... Подобная конструкция для засунутой во флеш-память константы должна приводиться не к указателю на целое, а к указателю на целое во флеш-памяти. При этом первый и второй имеют одинаковое представление, но показывают на совершенно разные вещи. Этот код выдаст "0", если затирание единицей какого-то байта памяти не приведет к фатальным последствиям.

В общем, если кто-то считает, что язык Си можно изучать в отрыве от "железа" - продолжайте считать так дальше. А я дам ссылочку на один интересный тест по C применительно ко встроенным системам.

С праздником!

berlin