Данная задачка возникла при исследовании быстродействия работы Ардуино при выполнении различных команд (об этом в отдельном посте). В процессе исследования возникли сомнения относительно постоянности времени работы отдельных команд при изменении значения операндов (как выяснилось позже, небезосновательные) и было принято решение попытаться оценить время исполнения отдельной команды. Для этого была написана небольшая программа (кто сказал скетч — выйти из класса), которая, на первый взгляд, подтвердила гипотезу. В выводе можно наблюдать значения 16 и 20, но иногда встречаются и 28 и даже 32мксек. Если умножить полученные данные на 16 (тактовую частоту МК), получим время исполнения в тактах МК (от 256 до 512). К сожалению, повторный прогон основного цикла программы (с теми же исходными данными), при сохранении общей картины, дает уже иное распределение времени исполнения, так что действительно имеющие место вариации времени не связаны с исходными данными. Исходная гипотеза опровергнута, но становится интересно, а с чем именно связан столь значительный разброс.

Необходимое примечание — я прекрасно понимаю, что для измерения времени исполнения команд следует использовать более сложные программы, однако для грубой оценки вполне достаточно и такой, что будет продемонстрировано далее.

Итак, время меняется, причем весьма существенно, ищем причины этого явления. Прежде всего, обращаем внимание на кратность полученных величин, смотрим описание на библиотеку работы со временем и видим, что 4мксек — это квант измерения, поэтому лучше перейти к квантам и понимаем, что мы получаем 4 либо 5 (весьма часто) и 6 либо 7 либо 8 (весьма редко) единиц измерения. С первой половиной все легко — если измеряемое значение лежит между 4 и 5 единицами, то разброс становится неизбежным. Более того, считая отсчеты независимыми, мы можем статистическими методами повысить точность измерения, что и делаем, получая приемлемые результаты.

А вот со второй половиной (6,7,8) дела хуже. Мы выяснили, что с исходными данными разброс не коррелирует, значит, это проявление других процессов, влияющих на время исполнения команд. Отметим, что выбросы достаточно редкие и на вычисляемое среднее значение существенным образом не являют. Можно было бы вообще ими пренебречь, но это не наш стиль. Я вообще за годы работы в инженерии осознал, что нельзя оставлять непонятки, сколь бы незначительными они не казались, поскольку они имеют отвратительное свойство бить в спину (ну или еще куда дотянутся) в самый неподходящий момент.

Начинаем выдвигать гипотезу 1 — самая удобная (в удобстве и универсальности она уступает только прямому вмешательству Творца) – глюки программного обеспечения, конечно же, не моего, мои программы никогда не глючат, а подключаемых библиотек (компилятора, операционной системы, браузера и т.д. – нужное подставить). Более того, поскольку я прогоняю программу в эмуляторе на www.tinkercad.com, то можно еще сослаться на баги эмулятора и закрыть тему, ведь исходники нам не доступны. Минусы данной гипотезы:

  1. От цикла к циклу расположение отклонений меняется, что намекает.
  2. Этот сайт все таки поддерживает AutoDesk, хотя аргумент слабоват.
  3. «Мы приняли постулат, что происходящее не является галлюцинацией, иначе было бы просто неинтересно».

Следующая предположение – влияние некоторых фоновых процессов на результат измерения. Вроде бы ничего не делаем, кроме как считаем, хотя … мы же выводим результаты в 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(). Здесь мы не можем ничего поделать, исходный код хотя и доступен, но менять мы его не можем – библиотеки включены в бинарях. Конечно, мы можем написать свои собственные функции работы со временем и смотреть их поведение, но есть путь проще.

Раз возможной причиной увеличения длительности являются «длинная» обработка прерывания, исключим возможность его возникновения в процессе измерения. Для этого дождемся его проявления и только потом этого проведем цикл измерения. Поскольку прерывание возникает намного реже, чем длится наш цикл измерения, то можно гарантировать его отсутствие. Пишем соответствующий фрагмент программы (пользуясь грязными хаками информацией, извлеченной из исходного кода) и, «это такая уличная магия», все становится нормально – мы измеряем время исполнения 4 и 5 квантов со средним значением времени исполнения операции сложения с ПТ в 166 тактов, что соответствует ранее замеренному значению. Гипотезу можно считать подтвержденной.

Остается еще один вопрос – а что так долго делается в прерываниях, что это занимает
(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)


  1. Lerk
    18.10.2018 18:06

    А зачем собственно проводить такие исследования, если есть замечательный документ: avr-instruction-set-manual, таблицы с 4-2 по 4-6. И никаких таинств.

    Или вы нашли несоответствия документа реальному поведению?


  1. GarryC Автор
    18.10.2018 18:09

    Там не плавающей точки, это программная часть.


    1. Lerk
      18.10.2018 18:15

      Это не важно. В конечном итоге все сводится к командам с четко детерминированным поведением. В чем смысл гадать на кофейной гуще, если вам реально важно быстродействие, и есть гарантированный способ узнать причину расхождений?


      1. GarryC Автор
        18.10.2018 21:41

        А не все так просто. Исходники библиотек далеко не все доступны, поэтому рассчитать не удастся, ну не дизассемблировать же. И при чем здесь гадание, если можно запустить программу и увидеть конкретные результаты?
        Вообще были получены интересные результаты после отработки методологии измерения. Например, время умножения ФТ 8*8 — 4 такта (в полном соответствии с КД), а 16*16 — не 4*4+4*4=32, как можно было бы ожидать, а около 20, видимо, оптимизации алгоритмов.


        1. Alex_Sa
          19.10.2018 01:07

          А можно уточнить — какие именно ардуино библиотеки недоступны в исходном коде? Ну хотя бы из тех что вас интересуют.


          1. GarryC Автор
            19.10.2018 11:38

            Библиотека операций с ПТ является частью компилятора (в расширенном смысле этго понятия) и в Ардуино никак не входит, хотя используется при линковке. Ее исходников я не видел вообще никогда, не уверен, что они есть помимо ассемблера.


            1. Alex_Sa
              19.10.2018 19:16
              +1

              Ардуиновские библиотеки базируются на avr-libc. Исходники можно посмотреть, например, здесь — download.savannah.gnu.org/releases/avr-libc
              Плавучку смотреть в libm. И да, она на ассемблере.

              PS А вообще, использовать float арифметику на 8-и битных микроконтроллерах — дурной тон. Из того что я видел, подавляющее большинство случаев применения float достаточно легко переносится в целочисленную арифметику.


        1. Vanellope
          19.10.2018 03:02

          А вот если один из операндов 0, как время сократится относительно ненулевых? Интересно, на этот случай оптимизирована библиотека?


          1. GarryC Автор
            19.10.2018 11:39

            Для первого операнда 0 и 1/170 видимой разницы нет в скорости исполнения нет, видимо, такая оптимизация для сложения не предусмотрена.


  1. shiru8bit
    18.10.2018 18:23

    Разумеется время вывода в Serial, как и время работы других обработчиков прерываний, добавляется ко времени выполнения теста, и это происходит асинхронно выполнению кода. На время измерения времени работы кода нужно запрещать все прерывания. А Arduino и для отсчёта времени (micros/millis) использует прерывания. Т.е. для теста нужно измерять время не силами Arduino, а извне.


    1. GarryC Автор
      18.10.2018 21:42

      Ну я вроде как показал, что можно аккуратно померить и силами Ардуино, это и была цель.


    1. Alex_Sa
      18.10.2018 23:56

      Хотите точно померять время силами Ардуино?
      — Останавливаете Timer1
      — Сбрасываете его в 0
      — Запрещаете прерывания
      — Запускаете Timer1 в Normal mode с делителем = 1
      — Выполняете операцию
      — считваете Timer1
      — Разрешаете прерывания


      1. GarryC Автор
        19.10.2018 11:44

        Это уже не силами Ардуино, если мы вкладываем в это понятие использование определенных библиотек. Хотя я тоже довольно таки в своем решении от «чистой» Ардуино удалился.


        1. Alex_Sa
          19.10.2018 19:02

          Ардуино это физическое устройство. Если вы " вкладываете в это понятие использование определенных библиотек" то для избежания неоднозначностей надо говорить не «с помощью ардуино», а «с помощью ардуиновских библиотек»


  1. masb
    18.10.2018 21:36

    А то что тактовый сигнал имеет джиттер, имеет влияние на ваши наблюдения?


    1. GarryC Автор
      18.10.2018 21:36

      маловероятно, на фоне общей погрешности он просто неразличим


    1. Alex_Sa
      19.10.2018 00:02

      Так таймеры же работают от того же тактового сигнала. Так что от джиттера «попугаи» не зависят даже если он и значителен.


  1. Karlson_rwa
    18.10.2018 22:53

    Что-то мне всё это напоминает.
    Фам Нювен несколько лет провел, обучаясь программировать и исследовать. Программирование восходило к началу времен. Как та навозная куча за замком отца. Когда ее промыло ручьем на десять метров в глубь, обнаружились искореженные корпуса машин – летающих машин, как говорили крестьяне, еще от тех великих дней колонизации Канберры. Но та навозная куча была чистой и свежей по сравнению с тем, что лежало в локальной сети «Репризы». Были программы, написанные пять тысяч лет назад, когда человечество еще не покинуло Землю. И самое чудесное (самое ужасное, как говорила Сура) было то, что, в отличие от бесполезных обломков прошлого Канберры, эти программы все еще работали! И через миллион миллионов запутанных нитей наследования многие из старейших программ все еще выполнялись во внутренностях системы Кенг Хо. Например, методы слежения за временем у торговцев. Поправки вносились неимоверно сложно – но на самом дне лежала крошечная программа, которая гоняла счетчик. Секунду за секундой отсчитывала система Кенг Хо с того момента, как нога человек ступила на Луну Старой Земли. Но если приглядеться еще пристальнее… начальный момент был миллионов на сотню секунд позже; момент «ноль» одной из первых компьютерных операционных систем Человечества.
    Значит, под всеми интерфейсами верхнего уровня лежат уровни поддержки, слой на слое. Какая-то часть этих программ была создана для совершенно иных ситуаций. То и дело несоответствие рождало фатальные инциденты. Вопреки всей романтике космических полетов, чаще всего катастрофы вызывались древними забытыми программами, которым удавалось взять реванш.


    1. Alex_ME
      19.10.2018 01:46

      Вернор Виндж «Глубина в небе». Спасибо, теперь я знаю, что почитать.