• Предсказуемость. Это ключевое понятие во встроенных системах. Очевидно, что этот термин имеет много интуитивных толкований, но здесь — в контексте программирования встроенных систем — мы используем лишь техническое значение: операция считается предсказуемой (predictable), если на данном компьютере она всегда выполняется за одно и то же время и если все такие операции выполняются за одно и то же время. Например, если
x
и
y
— целочисленные переменные, то инструкция x+y всегда будет выполняться за фиксированное время, а инструкция
xx+yy
будет выполняться за точно такое же время, при условии,
что
xx
и
yy
— две другие целочисленные переменные. Как правило, можно пренебречь небольшими колебаниями скорости выполнения операции, связанными с машинной архитектурой (например, отклонениями, вызванными особенностями кэширования и конвейерной обработки), и просто ориентироваться на верхний предел заданного времени. Непредсказуемые операции (в данном смысле этого слова) нельзя использовать в системах с жесткими условиями реального времени и можно лишь с очень большой осторожностью применять в остальных системах реального времени. Классическим примером непредсказуемой операции является линейный поиск по списку (например, выполнение функции
find
), если количество элементов списка неизвестно и не может быть легко оценено сверху. Такой поиск можно применять в системах с жесткими условиями реального времени, только если мы можем надежно предсказать количество или хотя бы максимальное количество элементов списка. Иначе говоря, для того чтобы гарантировать, что ответ поступит в течение фиксированного интервала времени, мы должны — возможно, с помощью инструментов анализа кода — вычислить время, необходимое для выполнения любой последовательности команд, приводящих к исчерпанию запаса времени.
• Параллелизм. Встроенные системы обычно реагируют на события, происходящие во внешнем мире. Это значит, что в программе многие события могут происходить одновременно, поскольку они соответствуют событиям в реальном мире, которые могут происходить одновременно. Программа, одновременно выполняющая несколько действий, называется параллельной (concurrent, parallel). К сожалению эта очень интересная, трудная и важная тема выходит за рамки рассмотрения нашей книги.
25.2.1. Предсказуемость
С точки зрения предсказуемости язык С++ очень хорош, но не идеален. Практически все средства языка С++ (включая вызовы виртуальных функций) вполне предсказуемы, за исключением указанных ниже.
• Выделение свободной памяти с помощью операторов
new
и
delete
(см. раздел 25.3).
• Исключения (раздел 19.5).
• Оператор
dynamic_cast
(раздел A.5.7).
В приложениях с жесткими условиями реального времени эти средства использовать не следует. Проблемы, связанные с операторами
new
и
delete
, подробно описаны в разделе 25.3; они носят принципиальный характер. Обратите внимание на то, что класс
string
из стандартной библиотеки и стандартные контейнеры (
vector
,
map
и др.) неявно используют свободную память, поэтому они также непредсказуемы. Проблема с оператором
dynamic_cast
связана с трудностями его параллельной реализации, но не является фундаментальной.
Проблемы с исключениями заключаются в том, что, глядя на конкретный раздел
throw
, программист не может сказать, сколько времени займет поиск соответствующего раздела
catch
и даже существует ли такой раздел
catch
, не проанализировав более крупный фрагмент программы. В программах для встроенных систем лучше было бы, если бы такой раздел
catch
существовал, поскольку мы не можем рассчитывать на то, что программист сможет использовать средства отладки языка С++. В принципе проблемы, связанные с исключениями, можно решить с помощью того же механизма, который определяет, какой именно раздел
catch
будет вызван для конкретного раздела
throw
и как долго ему будет передаваться управление, но в настоящее время эта задача еще исследуется, поэтому, если вам нужна предсказуемость, вы должны обрабатывать ошибки, основываясь на возвращаемых кодах и других устаревших и утомительных,
но вполне предсказуемых методах.
25.2.2. Принципы
При создании программ для встроенных систем существует опасность, что в погоне за высокой производительностью и надежностью программист станет использовать исключительно низкоуровневые средства языка. Эта стратегия вполне оправдана при разработке небольших фрагментов кода. Однако она легко превратит весь проект в непролазное болото, затруднит проверку корректности кода и повысит затраты времени и денег, необходимых для создания системы.
Как всегда, наша цель — работать на как можно более высоком уровне с учетом поставленных ограничений, связанных с нашей задачей. Не позволяйте себе опускаться до хваленого ассемблерного кода! Всегда стремитесь как можно более прямо выражать ваши идеи в программе (при заданных ограничениях). Всегда старайтесь писать ясный, понятный и легкий в сопровождении код. Не оптимизируйте его, пока вас к этому не вынуждают. Эффективность (по времени или по объему памяти) часто имеет большое значение для встроенных систем, но не следует пытаться выжимать максимум возможного из каждого маленького кусочка кода. Кроме того, во многих встроенных системах в первую очередь требуется, чтобы программа работала правильно и достаточно быстро; пока ваша программа работает достаточно быстро, система просто простаивает, ожидая следующего действия. Постоянные попытки написать несколько строчек кода как можно более эффективно занимают много времени, порождают много ошибок и часто затрудняют оптимизацию программ, поскольку алгоритмы и структуры данных становится трудно понимать и модифицировать. Например, при низкоуровневой оптимизации часто невозможно оптимизировать использование памяти, поскольку во многих местах возникает почти одинаковый код, который остальные части программы не могут использовать совместно из-за второстепенных различий. Джон Бентли (John Bentley), известный своими очень эффективными программами, сформулировал два закона оптимизации.
• Первый закон: “Не делай этого!”
• Второй закон (только для экспертов): “Не делай этого пока!”
Перед тем как приступать к оптимизации, следует убедиться в том, что вы понимаете, как работает система. Только когда вы будете уверены в этом, оптимизация станет (или может стать) правильной и надежной. Сосредоточьтесь на алгоритмах и структурах данных. Как только будет запущена первая версия системы, тщательно измерьте ее показатели и настройте как следует. К счастью, часто происходят приятные неожиданности: хороший код иногда работает достаточно быстро и не затрачивает слишком много памяти. Тем не менее не рассчитывайте на это; измеряйте. Неприятные сюрпризы также случаются достаточно часто.
25.2.3. Сохранение работоспособности после сбоя
Представьте себе, что вы должны разработать и реализовать систему, которая не должна выходить из строя. Под словами “не выходить из строя” мы подразумеваем “месяц работать без вмешательства человека”. Какие сбои мы должны предотвратить? Мы можем не беспокоиться о том, что солнце вдруг потухнет или на систему наступит слон. Однако в целом мы не можем предвидеть, что может пойти не так, как надо. Для конкретной системы мы можем и должны выдвигать предположения о наиболее вероятных ошибках. Перечислим типичные примеры.
• Сбой или исчезновение электропитания.
• Вибрация разъема.
• Попадание в систему тяжелого предмета, приводящее к разрушению процессора.
• Падение системы с высоты (от удара диск может быть поврежден).
• Радиоактивное облучение, вызывающее непредсказуемое изменение некоторых значений, записанных в ячейках памяти.
Труднее всего найти преходящие ошибки. Преходящей ошибкой (transient error) мы называем событие, которое случается иногда, а не каждый раз при выполнении программы. Например, процессор может работать неправильно, только если температура превысит 54 °C. Такое событие кажется невозможным, однако однажды оно действительно произошло, когда систему случайно забыли в заводском цехе на полу, хотя в лаборатории ничего подобного никогда не случалось.