Данная задачка возникла при исследовании быстродействия работы Ардуино при выполнении различных команд (об этом в отдельном посте). В процессе исследования возникли сомнения относительно постоянности времени работы отдельных команд при изменении значения операндов (как выяснилось позже, небезосновательные) и было принято решение попытаться оценить время исполнения отдельной команды. Для этого была написана небольшая программа (кто сказал скетч — выйти из класса), которая, на первый взгляд, подтвердила гипотезу. В выводе можно наблюдать значения 16 и 20, но иногда встречаются и 28 и даже 32мксек. Если умножить полученные данные на 16 (тактовую частоту МК), получим время исполнения в тактах МК (от 256 до 512). К сожалению, повторный прогон основного цикла программы (с теми же исходными данными), при сохранении общей картины, дает уже иное распределение времени исполнения, так что действительно имеющие место вариации времени не связаны с исходными данными. Исходная гипотеза опровергнута, но становится интересно, а с чем именно связан столь значительный разброс.
Необходимое примечание — я прекрасно понимаю, что для измерения времени исполнения команд следует использовать более сложные программы, однако для грубой оценки вполне достаточно и такой, что будет продемонстрировано далее.
Итак, время меняется, причем весьма существенно, ищем причины этого явления. Прежде всего, обращаем внимание на кратность полученных величин, смотрим описание на библиотеку работы со временем и видим, что 4мксек — это квант измерения, поэтому лучше перейти к квантам и понимаем, что мы получаем 4 либо 5 (весьма часто) и 6 либо 7 либо 8 (весьма редко) единиц измерения. С первой половиной все легко — если измеряемое значение лежит между 4 и 5 единицами, то разброс становится неизбежным. Более того, считая отсчеты независимыми, мы можем статистическими методами повысить точность измерения, что и делаем, получая приемлемые результаты.
А вот со второй половиной (6,7,8) дела хуже. Мы выяснили, что с исходными данными разброс не коррелирует, значит, это проявление других процессов, влияющих на время исполнения команд. Отметим, что выбросы достаточно редкие и на вычисляемое среднее значение существенным образом не являют. Можно было бы вообще ими пренебречь, но это не наш стиль. Я вообще за годы работы в инженерии осознал, что нельзя оставлять непонятки, сколь бы незначительными они не казались, поскольку они имеют отвратительное свойство бить в спину (ну или еще куда дотянутся) в самый неподходящий момент.
Начинаем выдвигать гипотезу 1 — самая удобная (в удобстве и универсальности она уступает только прямому вмешательству Творца) – глюки программного обеспечения, конечно же, не моего, мои программы никогда не глючат, а подключаемых библиотек (компилятора, операционной системы, браузера и т.д. – нужное подставить). Более того, поскольку я прогоняю программу в эмуляторе на www.tinkercad.com, то можно еще сослаться на баги эмулятора и закрыть тему, ведь исходники нам не доступны. Минусы данной гипотезы:
- От цикла к циклу расположение отклонений меняется, что намекает.
- Этот сайт все таки поддерживает AutoDesk, хотя аргумент слабоват.
- «Мы приняли постулат, что происходящее не является галлюцинацией, иначе было бы просто неинтересно».
Следующая предположение – влияние некоторых фоновых процессов на результат измерения. Вроде бы ничего не делаем, кроме как считаем, хотя … мы же выводим результаты в Serial. Возникает гипотеза 2 – время вывода иногда (странно как то… но все бывает) добавляется к времени выполнения команды. Хотя сомнительно, сколько там того вывода, но все равно – добавляем Flush и не помогло, добавляем задержку на завершение вывода и не помогло, вообще выносим вывод за пределы цикла – все равно время прыгает – это точно не Serial.
Ладно, что осталось – собственно организация цикла ( с какого перепугу ей менять свою длительность, не понятно) и все … хотя остался micros(). Я подразумевал, что время выполнения первого вызова этой функции и второго одинаково и при вычитании этих двух значений получу ноль, но если это предположение неверно?
Гипотеза 3 – иногда второй вызов отсчета времени выполняется дольше, нежели первый либо действия, связанные с отсчетом времени, иногда влияют на результат. Смотрим исходный код функции работы со временем (arduino-1.8.4\hardware\arduino\avr\cores\arduino\wiring.c – я уже неоднократно выражал свое отношение к подобным вещам, повторяться не буду) и видим, что 1 раз из 256 циклов аппаратного увеличения младшей части счетчика происходит прерывание для инкрементирования старшей части счетчика.
Наше время исполнения цикла от 4 до 5, поэтому можно ожидать 170*(4..5)/256 = от трех до четырех аномальных значений на отрезке из 170 измерений. Смотрим – очень похоже, их действительно 4 штуки. Чтобы разделить первую и вторую причину, делаем вычисления критической секцией с запрещенными прерываниями. Результат особо не меняется, выбросы все равно имеют место быть, значит, дополнительное время вносит вызов micros(). Здесь мы не можем ничего поделать, исходный код хотя и доступен, но менять мы его не можем – библиотеки включены в бинарях. Конечно, мы можем написать свои собственные функции работы со временем и смотреть их поведение, но есть путь проще.
Раз возможной причиной увеличения длительности являются «длинная» обработка прерывания, исключим возможность его возникновения в процессе измерения. Для этого дождемся его проявления и только потом этого проведем цикл измерения. Поскольку прерывание возникает намного реже, чем длится наш цикл измерения, то можно гарантировать его отсутствие. Пишем соответствующий фрагмент программы (пользуясь
Остается еще один вопрос – а что так долго делается в прерываниях, что это занимает
(7,8) – (5) ~ 2 кванта = *4 = 8мксек *16 = 128 тактов процессора? Обращаемся к исходному коду (то есть к ассемблерному коду, сформированному компилятором на сайте godbolt.com) и видим, что собственно прерывание исполняется приблизительно 70 тактов, из них 60 постоянно, а при считывании имеются дополнительные расходы в 10 тактов, итого 70 при попадании на прерывание – меньше, чем получено, но достаточно близко. Разницу отнесем на различие компиляторов либо режимов их использования.
Ну и теперь мы можем замерить собственно время исполнения команды сложения ПТ с различными аргументами и убедиться, что оно действительно сильно меняется при изменении аргументов: от 136 тактов для 0.0 до 190 для 0.63 (магическое число), причем составляет всего 162 для 10.63. С вероятностью 99.9% это связано с необходимостью выравнивания и особенностями его реализации в данной конкретной библиотеке, но это исследование явно выходит за пределы рассматриваемой задачи.
Приложение – текст программы:
void setup()
{
Serial.begin(9600);
}
volatile float t; // так надо
void loop()
{
int d[170];
unsigned long time,time1;
float dt=1/170.;
for (int i=0; i<170; ++i) {
{
// ждем переполнения счетчика и обработки прерывания
time1=micros();
long time2;
do { time2=micros(); }
while ((time2 & ~0xFF) == (time1 & ~0xFF));
};
/**/
time1=micros(); // засекаем время
/*
cli(); // тут был вход в критическую секцию - не помогло
*/
t=10.63; // начальное значение для операции
t=t+dt; // измеряемая операция
/*
sei(); // завершение критической секции
*/
time = micros(); // время окончания
time1=time-time1;
d[i]=time1/4;
/*
Serial.print(time1); // вот тут результаты и прыгали
Serial.flush(); // не помогло убрать выбросы
Delay(20); // тоже не помогло
*/
};
// выводим запомненные результаты, считаем и выводим среднее
float sum=0;
for (int i=0; i<170; ++i) {
sum+=d[i];
Serial.println(d[i]);
};
Serial.println((sum/170-2.11)*4*16); //2.11 – получается при пустой операции
Serial.flush(); // здесь ставим точку останова, чтобы посмотреть графики вывода
}