![](https://habrastorage.org/webt/kx/oe/cl/kxoecl7ngezzej1ftzcleeswzde.jpeg)
Данная задачка возникла при исследовании быстродействия работы Ардуино при выполнении различных команд (об этом в отдельном посте). В процессе исследования возникли сомнения относительно постоянности времени работы отдельных команд при изменении значения операндов (как выяснилось позже, небезосновательные) и было принято решение попытаться оценить время исполнения отдельной команды. Для этого была написана небольшая программа (кто сказал скетч — выйти из класса), которая, на первый взгляд, подтвердила гипотезу. В выводе можно наблюдать значения 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(); // здесь ставим точку останова, чтобы посмотреть графики вывода
}
Комментарии (19)
GarryC Автор
18.10.2018 18:09Там не плавающей точки, это программная часть.
Lerk
18.10.2018 18:15Это не важно. В конечном итоге все сводится к командам с четко детерминированным поведением. В чем смысл гадать на кофейной гуще, если вам реально важно быстродействие, и есть гарантированный способ узнать причину расхождений?
GarryC Автор
18.10.2018 21:41А не все так просто. Исходники библиотек далеко не все доступны, поэтому рассчитать не удастся, ну не дизассемблировать же. И при чем здесь гадание, если можно запустить программу и увидеть конкретные результаты?
Вообще были получены интересные результаты после отработки методологии измерения. Например, время умножения ФТ 8*8 — 4 такта (в полном соответствии с КД), а 16*16 — не 4*4+4*4=32, как можно было бы ожидать, а около 20, видимо, оптимизации алгоритмов.Alex_Sa
19.10.2018 01:07А можно уточнить — какие именно ардуино библиотеки недоступны в исходном коде? Ну хотя бы из тех что вас интересуют.
GarryC Автор
19.10.2018 11:38Библиотека операций с ПТ является частью компилятора (в расширенном смысле этго понятия) и в Ардуино никак не входит, хотя используется при линковке. Ее исходников я не видел вообще никогда, не уверен, что они есть помимо ассемблера.
Alex_Sa
19.10.2018 19:16+1Ардуиновские библиотеки базируются на avr-libc. Исходники можно посмотреть, например, здесь — download.savannah.gnu.org/releases/avr-libc
Плавучку смотреть в libm. И да, она на ассемблере.
PS А вообще, использовать float арифметику на 8-и битных микроконтроллерах — дурной тон. Из того что я видел, подавляющее большинство случаев применения float достаточно легко переносится в целочисленную арифметику.
Vanellope
19.10.2018 03:02А вот если один из операндов 0, как время сократится относительно ненулевых? Интересно, на этот случай оптимизирована библиотека?
GarryC Автор
19.10.2018 11:39Для первого операнда 0 и 1/170 видимой разницы нет в скорости исполнения нет, видимо, такая оптимизация для сложения не предусмотрена.
shiru8bit
18.10.2018 18:23Разумеется время вывода в Serial, как и время работы других обработчиков прерываний, добавляется ко времени выполнения теста, и это происходит асинхронно выполнению кода. На время измерения времени работы кода нужно запрещать все прерывания. А Arduino и для отсчёта времени (micros/millis) использует прерывания. Т.е. для теста нужно измерять время не силами Arduino, а извне.
GarryC Автор
18.10.2018 21:42Ну я вроде как показал, что можно аккуратно померить и силами Ардуино, это и была цель.
Alex_Sa
18.10.2018 23:56Хотите точно померять время силами Ардуино?
— Останавливаете Timer1
— Сбрасываете его в 0
— Запрещаете прерывания
— Запускаете Timer1 в Normal mode с делителем = 1
— Выполняете операцию
— считваете Timer1
— Разрешаете прерыванияGarryC Автор
19.10.2018 11:44Это уже не силами Ардуино, если мы вкладываем в это понятие использование определенных библиотек. Хотя я тоже довольно таки в своем решении от «чистой» Ардуино удалился.
Alex_Sa
19.10.2018 19:02Ардуино это физическое устройство. Если вы " вкладываете в это понятие использование определенных библиотек" то для избежания неоднозначностей надо говорить не «с помощью ардуино», а «с помощью ардуиновских библиотек»
Karlson_rwa
18.10.2018 22:53Что-то мне всё это напоминает.Фам Нювен несколько лет провел, обучаясь программировать и исследовать. Программирование восходило к началу времен. Как та навозная куча за замком отца. Когда ее промыло ручьем на десять метров в глубь, обнаружились искореженные корпуса машин – летающих машин, как говорили крестьяне, еще от тех великих дней колонизации Канберры. Но та навозная куча была чистой и свежей по сравнению с тем, что лежало в локальной сети «Репризы». Были программы, написанные пять тысяч лет назад, когда человечество еще не покинуло Землю. И самое чудесное (самое ужасное, как говорила Сура) было то, что, в отличие от бесполезных обломков прошлого Канберры, эти программы все еще работали! И через миллион миллионов запутанных нитей наследования многие из старейших программ все еще выполнялись во внутренностях системы Кенг Хо. Например, методы слежения за временем у торговцев. Поправки вносились неимоверно сложно – но на самом дне лежала крошечная программа, которая гоняла счетчик. Секунду за секундой отсчитывала система Кенг Хо с того момента, как нога человек ступила на Луну Старой Земли. Но если приглядеться еще пристальнее… начальный момент был миллионов на сотню секунд позже; момент «ноль» одной из первых компьютерных операционных систем Человечества.
Значит, под всеми интерфейсами верхнего уровня лежат уровни поддержки, слой на слое. Какая-то часть этих программ была создана для совершенно иных ситуаций. То и дело несоответствие рождало фатальные инциденты. Вопреки всей романтике космических полетов, чаще всего катастрофы вызывались древними забытыми программами, которым удавалось взять реванш.
Lerk
А зачем собственно проводить такие исследования, если есть замечательный документ: avr-instruction-set-manual, таблицы с 4-2 по 4-6. И никаких таинств.
Или вы нашли несоответствия документа реальному поведению?