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