Keil C51: Маленькие хитрости Си.
Организация цикла на 256 итераций
Александр Бельченко
28 ноября 2003 года
обновлена 1 декабря 2003 года
Эта заметка рассказывает о том, как можно построить на языке Си цикл на 256 итераций. Как вы догадываетесь, ничего сложно в этом нет. Я собираюсь показать вам, как в этом случае можно использовать байтовую переменную в качестве счетчика цикла. Описанный прием помогает оптимизировать циклы: уменьшается код и время выполнения программы.
Если попросить любого программиста, знакомого с языком Си, написать кусочек программы, в которой содержится цикл на 256 итераций, то он, практически не задумываясь, напишет следующее:
for( i=0; i<256; i++)
{
...
}
Все просто. Но... Что при этом остается «за скобками»?
Прежде всего счетчик цикла i обязательно должен иметь тип (как минимум) int или unsigned int. Т. е. размер переменной i в процессоре семейства MCS-51 будет 16 бит или 2 байта. Если же счетчик цикла i будет иметь тип unsigned char (8 бит беззнаковое целое), то цикл просто никогда не закончится. Тем, кто мне не верит или до сих пор не догадался почему так, предлагаю проделать соответствующий эксперимент в симуляторе µVision2.
Еще одна тонкость компилятора С51 при трансляции инструкций цикла в ассемблерные коды. Если вам надо организовать цикл на число повторений меньше 256 (скажем, 128), то вы можете использовать в качестве счетчика цикла байтовую переменную. Те, кто знакомы с ассемблером семейства MCS-51, могут даже предполагать, что компилятор оттранслирует запись вида
for( i=0; i<128; i++)
{ ... }
в ассемблерный эквивалент, в котором для управления циклом будет использована ассемблерная инструкция DJNZ ведь количество повторов цикла заранее известно. Но, при внимательном рассмотрении листинга компилятора, их постигнет разочарование. Компилятор напрочь отказывается строить цикл на основе инструкции DJNZ, хотя он повторяется фиксированное число раз. И никакой уровень оптимизации (в настройках компилятора) не заставит его сгенерировать код на основе DJNZ. Плохой компилятор?!
Инструкция DJNZ Decrement and Jump if Not Zero (уменьшить переменную и выполнить переход, если переменная не равна нулю).
Как ни странно, но этот пример как раз не показатель качества компилятора. Точнее, в этом конкретном случае, компилятор С51 следует стандарту языка Си, который говорит, что условие завершения цикла проверяется перед каждой итерацией цикла. Что и получается в нашем примере. Перед каждой итерацией цикла производится сравнение переменной i с числом 128. В инструкции ассемблера DJNZ производится сравнение переменной на ноль. К тому же инструкция DJNZ производит декремент переменной, в то время как мы указали, что после каждой итерации цикла, переменную необходимо инкрементировать.
Итак, чтобы достичь желаемого результата, а именно построить Си-цикл на основе ассемблерной инструкции DJNZ, необходимо изменить наш цикл следующим образом:
for( i=128; i!=0; i--)
{ ... }
Теперь наш цикл после трансляции в ассемблер будет организован на основе инструкции DJNZ. Так, как мы и хотели.
По аналогии, для случая с 256-ю итерациями, мы могли бы написать:
for( i=256; i!=0; i--)
{ ... }
Но этот код все равно требует, чтобы переменная i была типа int или unsigned int. Если в качестве счетчика попробовать применить 8-битную переменную (unsigned char), то тело цикла не будет выполнено ни разу. Опять же, предлагаю желающим поэкспериментировать.
Итак, что же? Если мы хотим использовать байтовую переменную для организации цикла, то мы ограничены только 255 итерациями? Или нет?
Вы можете спросить меня: зачем я затеял всю эту мышиную возню вокруг 256 итераций и байтового счетчика цикла?
Дело в том, что при написании программ для встраиваемых систем, программисту необходимо максимально оптимизировать все доступные ресурсы процессора будь то память данных, память программ или, что более важно, время выполнения тех или иных операций. Как высказал эту мысль инженер-разработчик Сергей Марков (aka SM): «Чем больше сэкономишь в одном месте, тем больше останется на другое».
В нашем случае переход от однобайтного счетчика цикла к многобайтному приведет к значительному увеличению времени выполнения цикла. Причем зависимость тут явно будет нелинейная.
«Revenons
à
nous moutons», как говорят французы. Итак, для того, чтобы построить цикл на 256 итераций и при этом исхитриться использовать в качестве цикла байтовую переменную, необходимо сделать 255 итераций и еще одну. Все очень просто.
А сделать это действительно будет просто, если для цикла использовать конструкцию языка Си do ... while. В этом случае, как вы помните, тело цикла выполняется до проверки условия, и выполняется хотя бы один раз. Т.е. мы можем записать наш цикл так:
unsigned char i = 256;
do
{
...
// тело цикла
...
} while ( --i != 0 );
Цикл будет выполнен 256 раз. Как мы и хотели. Переменная i будет иметь размер 1 байт. Чего мы и добивались.
Пусть вас не смущает присвоение байтовой переменной значения 256. Это вовсе не ошибка. Да, я знаю, что байтовая переменная может принимать только значения от 0 до 255. То есть число 256 будет представлять собой два байта старший равен 01, а младший равен 00. Переменной i будет присвоено значение только младшего байта числа 256, т.е. ноль. В данном случае мы имеем дело с неявным приведением типов.
Компилятор Keil C51 спокойно проглатывает конструкцию «unsigned char i=256». И молча берет младший байт. Другие, более «умные» компиляторы могут выдать вам какое-нибудь грозное предупреждение об этом. Всегда следует помнить разницу между предупреждениями и ошибками.
У приведенного кода есть два достоинства.
Первое это сохранение читаемости программы. Сразу видно, что цикл должен быть выполнен 256 раз. Если написать «unsigned char i=0» это сразу снижает понятность программы. Сколько раз должен быть выполнен цикл, если в начале цикла счетчик равен нулю, а в конце тоже должен стать нулем?
Второе этот код достаточно универсален. Переменной i можно присваивать значения от 1 до 256, в зависимости от ваших нужд. И еще если изменить тип переменной i на int, то цикл опять же выполнится 256 раз. Если мы будем «угождать» компилятору и напишем «i=0», то при переходе к типу int необходимо будет менять присваивание.
Остается добавить еще одно. Возможно, для кого-то описанные выше приемы покажутся неприемлемыми, потому что в их программе внутри цикла проводится некоторая обработка данных в массиве (или в нескольких массивах), а счетчик цикла одновременно служит и индексом массива. Однако, наша задача остается прежней максимальная оптимизация алгоритма программы. Так вот, с этой точки зрения, доступ к элементам массива через операцию индексирования значительно проигрывает операции разыменования указателя (на элемент массива). Таким образом, отказ от индексов и переход к работе с указателями даст дополнительный выигрыш в скорости.
К теме оптимизации работы с элементами массивов данных я обязательно вернусь в своих следующих статьях.
30 ноября 2003: После публикации статьи в одной телеконференции разгорелось бурное обсуждение того, что байту нельзя присвоить значение 256. Поэтому я решил добавить в статью объяснение этого спорного вопроса. Мои мысли по этому поводу вы можете прочитать в специальной заметке от 30 ноября 2003 года.
1 декабря 2003: По просьбе SM я добавляю предостережение о возможной непереносимости данного приема. Пользоваться таким приемом можно только в том случае, если вы уверены, что используемый вами компилятор точно следует стандарту ANSI C и при установке какой-нибудь супер-оптимизации не заоптимизирует такой цикл до буквально пустого места. Если вы в этом не уверены, то обязательно посмотрите в листинг компилятора, и проверьте корректность работы полученной программы в симуляторе.
Моя статья рассчитана в первую очередь на пользователей пакета Keil C51 версии 7.х. В этом компиляторе я лично проверял получаемый код, поэтому его работоспособность гарантирую. При использовании других компиляторов настоятельно рекомендую вам проверять получаемый код.
Продолжение с некоторыми объяснениями
«Revenons
à
nous moutons» «вернемся к нашим баранам» (
франц.).