Переход от вычеслений типа float к int (целочисленным)

Вопросы программирования в системе Ардуино
Аватара пользователя
БлудныйКот
Сообщения: 45
Зарегистрирован: 29 дек 2016, 16:19

Переход от вычеслений типа float к int (целочисленным)

Сообщение БлудныйКот » 04 янв 2017, 18:26

Добрый день Эдуард!

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

С сайта ардуино кратко о float:

Описание типа float
Тип данных float служит для хранения чисел с плавающей запятой. Этот тип часто используется для операций с данными, считываемыми с аналоговых входов. Диапазон значений — от -3.4028235E+38 до 3.4028235E+38. Переменная типа float занимает 32 бита (4 байта) в памяти.

Тип float имеет точность 6-7 знаков, имеются ввиду все знаки, а не только мантисса. Обычно для увеличения точности используют другой тип - double, но на платформе Arduino, double и float имеют одинаковую точность.

Хранение в памяти чисел с плавающей точкой в двоичной системе обуславливает потерю точности. Так, например, 6.0 / 3.0 не обязательно равен 2.0. Сравнивая два числа с плавающей точкой следует проверять не точное равенство, а разницу между этими числами, меньше ли она некого выбранной малого порога.

Следует также учитывать, что арифметические операции над числами с плавающей запятой выполняются существенно медленнее, чем над целыми.



http://www.engblaze.com/faster-code-fridays-avoid-floating-point-math/

Кстати вот что пишет автор статьи по ссылке выше:

Now, when I perform further calculations, the Arduino will use integer math, which can be up to 40x (!) faster than it’s floating-point equivalent. Remember you have to account for the scaling factor in all operations:


Хотелось бы получить развернутый ответ. Заранее спасибо!


Эдуард
Администратор
Сообщения: 484
Зарегистрирован: 30 окт 2016, 20:53

Re: Переход от вычеслений типа float к int (целочисленным)

Сообщение Эдуард » 04 янв 2017, 23:12

К целочисленному вычислению имеет смысл переходить только для задач критичных ко времени выполнения. Математические операции над целыми числами выполняются гораздо быстрее операций над числами с плавающей запятой.

Приведу конкретный пример. Я измерил время выполнения двух вариантов суммирования двух чисел разных типов данных. Измерение проводил программой из темы форума Измерение времени выполнения функций и команд Ардуино.
Операция
z= x + y;
требует на выполнение время:
0,44 мкс для volatile byte x=10; volatile byte y=20; volatile byte z;
0,88 мкс для volatile int x=10; volatile int y=20; volatile int z;
1,75 мкс для volatile long x=10; volatile long y=20; volatile long z;
7,94 – 10 мкс для volatile float x=10.2; volatile float y=2.3; volatile float z;

Т.е. сумма данных типа float выполняется в 10 раз медленнее, чем сумма int. Для чисел с большим числом значащих разрядов, особенно в порядках, разница будет еще больше.

Я когда-то занимался разработкой аппаратных вычислительных модулей. С ужасом вспоминаю операцию сумму: денормализация мантиссы, коррекция порядка, сумма мантисс, нормализация мантиссы, коррекция порядка. Денормализация и нормализация это сдвиги 24 разрядных чисел. И это все на 8 разрядном микроконтроллере. Я думаю, что при разных порядках чисел время выполнения суммы значительно увеличится.

Операции с плавающей запятой – просто пожиратели ресурсов 8ми разрядных микроконтроллеров, прежде всего, времени.

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

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

Кстати, есть способы снизить время на выполнение операций с плавющей запятой. Я затрагивал эту тему в конце урока 13.

Аватара пользователя
БлудныйКот
Сообщения: 45
Зарегистрирован: 29 дек 2016, 16:19

Re: Переход от вычеслений типа float к int (целочисленным)

Сообщение БлудныйКот » 05 янв 2017, 11:48

Спасибо за ответ. С пересчетом нескольких коэфициентов в константы полезно, буду знать.

Эдуард
Администратор
Сообщения: 484
Зарегистрирован: 30 окт 2016, 20:53

Re: Переход от вычеслений типа float к int (целочисленным)

Сообщение Эдуард » 05 янв 2017, 23:15

В качестве примера я приведу форматы вычислений для контроллера холодильника на элементе Пельтье. Программа для этой разработки написана на Ассемблере. Команд для работы с плавающей запятой на Ассемблере нет. Тем не менее, задача реализована без каких-либо ограничений.

Число с плавающей запятой состоит из мантиссы и показателя степени – порядка. Для двоичного исчисления:

мантисса * 2 порядок

Мантисса находится в интервале 0…1. У числа с плавающей запятой фиксированная относительная точность и меняющаяся абсолютная точность. В Ардуино тип float состоит из 4 байтов: 3 байта – мантисса и 1 байт порядок. В результате диапазон значений для чисел float составляет -3.4 1038 … +3.4 1038.

Удобно использовать числа с таким широким диапазоном и с такой высокой точностью. Но в большинстве случаев в этом нет необходимости.
Например, в контроллере холодильника ток вряд ли будет выше ампер 10. И точность его определена разрядностью АЦП. Для 10 разрядного АЦП это 0,1% или в абсолютном выражении 0,01 А. В этой разработке не нужны плавающие числа, нужны дробные числа.

Многие считают, что дробные числа представляются только типом данных с плавающей запятой. Из заявленных в языке C данных это правильно. Но в природе существует еще формат с фиксированной запятой.

Типы данных byte, int, long предназначены для целочисленных вычислений. Например, для типа int диапазон чисел составляет -32768 … -1, 0, 1, 2, … 32767. Только целые числа. Это частный случай формата с фиксированной запятой, в котором запятая зафиксирована в младшем разряде.

Но в принципе запятую можно сдвинуть. Вот пример числа с фиксированной запятой в 11 разряде.


Разряды для числа INT Разряды для числа с фиксированной запятой Десятичный вес разряда числа с фиксированной запятой Пример числа
С десятичным эквивалентом
1,375
0.-11 0,00048828125 0
1 -10 0,0009765625 0
2 -90,001953125 0
3 -80,00390625 0
4 -7 0,0078125 0
5 -60,015625 0
6 -5 0,03125 0
7 -40,0625 0
8 -3 0,125 1
9 -2 0,25 1
10 -1 0,5 0
11 0. 1 1
12 1 2 0
13 2 4 0
14 3 8 0
15 4 16 0


Я в программе объявил переменную типа unsigned int и решил, что для этой конкретной переменной 0й разряд соответствует 11 разряду формата int. Формат переменной с фиксированной запятой никак не отражается в программе. Просто надо это помнить и указать в комментариях.

Диапазон значений числа из моего примера составляет 0 … 31,999, абсолютная точность 0,00048828125. Для нашей задачи больше чем достаточно.
    Если не хватает точности, то можно запятую сдвинуть в сторону старших разрядов.
    Если не хватает диапазона – то сдвинуть в сторону младших разрядов.
    Для большинства практических вычислений вполне достаточно данного типа int. Если использовать long, то диапазон и точность будут на много выше.
В данном примере я сдвинул запятую на 11 разрядов. Это равнозначно, что я умножил число на 211, т.е. на 2048.

unsigned int measureCurrent; // измеренный ток (*2048)

При объявлении переменной я указал это в комментариях и должен помнить, то переменная measureCurrent содержит значение реального тока, умноженное на 2048.

Как правило, разные переменные с фиксированной запятой могут содержать запятую в разных разрядах. Например, этот измеренный ток мы хотим умножить на корректирующий коэффициент в диапазоне 0 .. 2. Тогда мы выберем запятую в 15 разряде и получим диапазон 0 … 1,99999 (0 … 65535 / 32768). Точность будет максимальная.

unsigned int coeffCurrent; // масштабный коэффициент тока (*32768)

Теперь об операциях с такими форматами. Для языка C это будут обычные математические операции с целыми числами типа int.
Но мы должны помнить, что в результате произведения чисел с фиксированными запятыми получится число с запятой в позиции равной сумме сдвигов запятой для каждого множителя. Проще использовать множители, которые мы написали в комментариях. Например:

long corrCurrent; // откорректированный ток (* 67 108 864)
corrCurrent = measureCurrent * coeffCurrent;

Мы умножили два числа int, получили результат типа long и в нем запятая сдвинута на 26 (11+15) разрядов. Это равносильно, что мы умножили его на 67 108 864.
Скорее всего такая точность не нужна. Поэтому мы можем отбросить 16 разрядов, получить число int, в котором запятая сдвинута на 10 (26-16) разрядов.

unsigned int realCurrent; // откорректированный ток (* 1024)
realCurrent = (unsigned int) ( corrCurrent >> 16);

Отбросить 16 разрядов можно более эффективным способом за счет применения указателей. Сдвиг данных типа long требует очень много времени. Но это другой вопрос.

В результате мы, используя только целочисленную математику, получили дробный результат.

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

Остался вопрос, как вводить и выводить числа с фиксированной запятой. Это надо делать, учитывая множители, которые мы написали в комментариях. Если мы хотим получить реальное десятичное число из переменной

unsigned int realCurrent; // откорректированный ток (* 1024)

то надо разделить его на 1024. Эта операция может вызвать некоторые сложности. Вариантов много.
Если информация выводится программой верхнего уровня на компьютере, то можно там и разделить. В резидентной программе времени это не потребует совсем.
Если мы хотим выдать число в монитор последовательного порта в формате float, то делать нечего. Надо преобразовывать в float и делить. Но все равно операций с типами float будет намного меньше.
Но главное, что мы стремимся не уничтожить все операции с числами float из программы, а убрать их с критичных ко времени программных блоках. Вывод данных через последовательный интерфейс занимает много времени и применение в нем операций над числами float ничего не изменит. А вот в быстром регуляторе тока или напряжения вычисления с фиксированной значительно изменят ситуацию.

При вводе данных надо выполнять обратную операцию. Т.е. если мы хотим занести из компьютера значение коэффициента

unsigned int coeffCurrent; // масштабный коэффициент тока (*32768)

равное 1,025832, то мы должны умножить его на 32768. В результате мы должны занести число 33614 (1,025832 * 32768).

Если данное считывается из АЦП, то запятую лучше привязать к данному из АЦП, т.е. к нулевому разряду. Число останется целочисленным, а при умножении на первый коэффициент запятая сместится.

Думаю, мало кто дочитал до конца. Тот, кто дочитал, наверное, поклялся никогда не использовать числа с фиксированной запятой. Но, поверьте, что есть ситуации, когда без подобных вычислений обойтись нельзя. Они значительно сокращают время выполнения программы. Надо только применять их там, где действительно без них не обойтись. В локальных кусках программы сделать это не так сложно.

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

Аватара пользователя
БлудныйКот
Сообщения: 45
Зарегистрирован: 29 дек 2016, 16:19

Re: Переход от вычеслений типа float к int (целочисленным)

Сообщение БлудныйКот » 06 янв 2017, 10:47

Спасибо информативно, но пока пытаюсь понять) Нужно немного времени. Я честно говоря не знаком с таким типом вычислений. Перечитал несколько раз)


Вернуться в «Программирование Ардуино»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 4 гостя