Почему float в контроллерах — плохо.
Иногда мне приходится видеть, как некоторые товарищи, особенно ардуинофилы, бодро используют тип float при вычислениях вроде «y=1+1» на контроллерах типа младших AVR, PIC, MSP430 или других, на которых FPU отродясь не было. Такой код вызывает сочувствие к человеку, его написавшему, ибо написать его может либо совсем зеленый неофит, либо закоренелый дилетант.
Наверное, теоретически могут быть исключения. Но в своей практике я никогда не встречался со случаями, в которых использование плавающей точки в подобных условиях было бы действительно оправдано.
Вообще говоря, поводом к написанию этого поста послужило то, что сегодня я дописал свою реализацию алгоритма Гёрцеля в двух вариантах — с плавающей точкой и с фиксированной. Если у меня выдастся время, про сам алгоритм и про процесс написания еще будут статьи, ибо приключений в процессе было достаточно. Пока же те, кто не знают, что это за алгоритм, могут почитать про него в Википедии. Вкратце — это такой радикально упрощенный вид ДПФ, позволяющий вычислить значение всего одного выбранного частотного компонента. В силу своей простоты он пригоден для выполнения на мелких микроконтроллерах. Однако он, конечно, требует работы с вещественными числами.
Итак, дописал я его, и решил скомпилировать в AVR Studio обе версии и сравнить их быстродействие с помощью встроенного симулятора. Ниже приведены результаты, полученные в следующих услових:
1. Учитывалось только время работы основного алгоритма (путем замера в симуляторе времени выполнения функции в тактах), без вычисления используемого коэффициента.
2. Размер буфера данных для обоих версий составлял 80 байт.
3. Программы идентичны с точностью до названия функции и типов переменных. Интересующиеся могут поглядеть на листинги внизу статьи.
Итак, для версии с фиксированной точкой:
Размер скомпилированной программы: 628 байт.
Время работы алгоритма: 13516 тактов, 1,69 мс при тактовой частоте 8 МГц.
Для версии с плавающей точкой:
Размер скомпилированной программы: 3740 байт.
Время работы алгоритма: 166001 такт, 20,75 мс при тактовой частоте 8 МГц.
По размеру разница в шесть (!) раз, по скорости — в двенадцать раз! Именно столько теряют любители float в контроллерах. Практически это означает, что реализацию с фиксированной точкой можно использовать, например, в ATmega48, и еще останется больше трех килобайт для остального кода, а вот версия с плавающей точкой туда просто не уместится, для нее потребуется как минимум ATmega88, и то будет съедена почти половина памяти.
Листинги тестовых программ.
С фиксированной точкой:
#include <avr/io.h> #include <stdint.h> #include "goertzel.h" #define FREQ_CONST fix_fill(1,414) volatile uint8_t buffer[BLOCK_SIZE]; volatile fixed result; void main(void) { result=0; result=GzFix_GetFreqMagnitude(buffer,FREQ_CONST); result=0; while (1); }
fixed — мой самописный тип данных с фиксированной точкой. Из тридцати двух бит под дробную часть отдается десять.
fix_fill() — самописный макрос, формирующий число с фиксированной точкой из целой и дробной части.
С плавающей точкой:
#include <avr/io.h> #include <stdint.h> #include "goertzel.h" #define FREQ_CONST 1.414 volatile uint8_t buffer[BLOCK_SIZE]; volatile float result; void main(void) { result=0; result=Gz_GetFreqMagnitude(buffer,FREQ_CONST); result=0; while (1); }
Нельзя ли подробнее? Параметры компиляции и линковки.
А то мой опыт с float на AVR подсказывает, что на такой короткой программе, демонстрирующей только вычисление, разница в размере должна быть в диапазоне 2..3 раза, а не шесть. Ну а в реальных программах разница ещё меньше. В сравнении — float-арифметика (без коренй/синусов) добавляет к программе меньше, чем printf без float-форматов. В каком-то из технологических пультов, где во float статистика считалась, текстовые строки больше занимали :-), а время по сравнению с выводом на ЖКИ тоже не катастрофа.
Да и время-то тоже не на порядок. У AVR и 32-битной арифметики родной нет.
Да, действительно, статья короче, чем ей стоило бы быть. Просто сейчас на меня навалилась куча всякой работы (да еще и сессия), и потому растекаться мысию по древу особо некогда — подготовка качественного материала требует усилий…
В скором времени я надеюсь написать пространную статью про свою реализацию алгоритма, где и привести все исходники и тесты. Пока же могу выслать Вам свои исходники полностью, чтобы Вы смогли проверить все сами. Ну или указать мне, где я не прав.
Мельком глянул. Что видно сразу по полученных исходниках:
— Во float-варианте из энергии извлекается корень, в целочисленном этого нет, вот уже кусочек разницы.
— В fixed макрос надо так
#define fix_fill(wpart,fpart) (((int32_t)(wpart) << FIX_BITS) | (((int32_t)(fpart) << FIX_BITS) / DEC_DENOM))
без приведения fix_fill(512,0) даст int константу 0, которая приведется к int32 (и местами пойдёт деление на 0). При правильном макросе несколько увеличится fixed-вариант.
— Во float из исходников не выбрасывается GzFix_GetFreqMagnitude (тут пока неуверенно, ключи компиляции/линковки не смотрел).
Но это всё брызги, ну станет float-вариант не в 6 раз длиннее, а в 5, всё равно не то. Хотя второй пункт с макросами может что-то сыграть с выбрасыванием напрочь из кода подпрограмм 32-битного целочисленного деления, это уже весомее.
И у меня в исходниках нет
#define FREQ_CONST fix_fill(1,414)
так что может я немного не то смотрю.
Кстати, какая версия avr-gcc использовалась? Вечером вернусь, запущу в виртуалке студию, пролинкую нужный каталог на C:\WinAVR и тогда уже и покомпилирую, и на все ключи гляну.
— поскольку энергия меряется в попугаях, достаточно подогнать выходные значения так, чтобы они влезали в диапазон, корень извлекать необязательно.
— с макросом понял. Спасибо.
— GzFix_GetFreqMagnitude просто закомментирована.
— одна версия превращалась в другую редактированием непосредственно перед компиляцией.
— версию gcc так не помню, надо смотреть.
Прошу прощения, в письме было указано WinAVR-20100110, просто я не обратил внимания. Им же и работал. Исходники модифицировал, теперь всё переключается при помощи USE_FLOATING_POINT, которая задаётся в проекте AVRStudio в конфигурации float. Сверяю размер
fixed, not patched macro 628
float, sqrt 3740
Теперь убираем из float корень, всё равно для сравнивания амплитуд на больше-меньше он не нужен:
fixed, patched macro 682
float, no sqrt 3490
И в этой точке не заглядывая в ключи уже вспоминаю, в чём дело.
Дело в том, что в libgcc, идущей в комплекте с avr-gcc, поддержка плавающей точки выписана… короче, никак не выписана. Там стоят универсальные заглушки soft-float, написанные на С и единые для всех систем (для которых это не заменено в порте / поддержке). Поэтому при работе с float нужно подключать библиотеку libm из avr-libc. Без неё время даже не смотрел, нет смысла. Размеры с libm:
float, sqrt 1510
float, no sqrt 1334
А теперь, внимание, заменяем в плавающих вычислениях деление на 255.0 умножением на (1.0/255.0)
float, *(1.0/256.0) 1126
Времена для fixed с некорректированным макросом и для float на пустом (нулевом) массиве сверять почти не имеет смысла.
Оптимизированная float-библиотека проверяет многое на 0 в самом начале. Время в циклах:
fixed, not patched macro 13516
float, no sqrt 23380
А вот по честному, с пилой в буфере данных и с исправленным макро вышло нос в нос:
fixed, patched macro 74822
float, no sqrt 70279
По сравнению с обычной целочисленной арифметикой в fixed добавляются сдвиги, а float-библиотека в avr-libc вылизана на асме.
Ну и герой дня — вариант float без деления. На треть быстрее fixed-варианта с делением (таки деление, что float, xnj int32 — больная тема для AVR).
float, *(1.0/256.0) 44053
Но уже fixed-вариант взывает к справедливой оценке. Вместо fix_div на 512 делаем fix_mul на 1/512, которая влазит при FIX_BITS==10 просто чудесно. Уходим от деления.
Размер / время в циклах
fixed, (1<<FIX_BITS)/512 542 / 14212
float, *(1.0/256.0) 1126 / 44053
Теперь моя душенька довольна, float проигрывает приблизительно в два раза по размеру и в три по скорости 🙂
Конечно, это предварительные результаты, окончательно можно сказать только после прогона на данных, для которых известен (посчитан на персоналке) результат.
Надеюсь, я не привнёс своих ошибок.
Кстати, в avr-gcc толи 4.7, толи 4.8 появилась поддержка int24_t, для fixed-point при аккуратной работе с диапазонами может быть интересно. А также где-то там появилась поддержка _Fixed, но там нельзя выбирать положение точки, может оказаться ненеинтересно.
Круто, спасибо, я так далеко не залезал. 🙂 Тем не менее, мое утверждение не теряет смысла — если я не дошел до подключения нестандартной библиотеки с плавающей точкой, то ардуинофанат тем более вряд ли до этого дойдет. 🙂 Так что общий совет начинающим, тем не менее, — «float — плохо».
Получил Ваше письмо, на досуге поразбираю подробно.
Так я и не спорю с тем, что «с PC-шного дуру пихать везде float это плохо».
Но совет я бы переформулировал в «не знаешь точно зачем оно тебе нужно — не лезь».
А ардуины не зря быстро от mega8 до армов дошли, с их подходами всегда будет мало места. Но мне кажется, если так подходить к задаче, на армах уже .net micro framework будет и не хуже по поедаемым ресурсам, и удобнее для работы.