Добрый день!
В качестве саморазвития написал основу класса float24_t. Он отличается от float урезанной дробной частью мантиссы: 15 бит против 23 бит. Такую разрядность выбрал для того, чтобы мантисса в формате 1.M умещалась в тип uint16_t. Точность в итоге получилась немногим меньше 5 разрядов.
float24.h
#ifndef _FLOAT24_H_
#define _FLOAT24_H_
#include <Arduino.h>
#include <stdint.h>
typedef __uint24 uint24_t;
typedef union {
float f;
struct {
uint32_t mantissa : 23;
uint32_t exponent : 8;
uint32_t sign : 1;
};
} ufloat_t;
class float24_t {
// формат float24_t [ (-1)^S * 1.M * 2^E ] отличается от float размером дробной части мантиссы (15 бит против 23 битов)
// мантисса float24_t получается из мантиссы float путем сдвига на 8 бит вправо,
// экспонента E и знак S (старший байт мантиссы) соответствуют float
public:
float24_t(const float value) {
ufloat_t converter{ value };
_exponent = converter.exponent;
_mantissa = static_cast<uint16_t>(converter.mantissa >> 8) & ~0x8000; // смещаем мантиссу на 8 вправо и маскируем знак числа
if (bitRead(converter.mantissa, 7)) _mantissa++; // если последний отброшенный разряд == 1, то увеличиваем мантиссу на 1 (аналог округления 0,5 до целого)
if (converter.sign) bitSet(_mantissa, 15);
}
float24_t(const uint16_t mantissa, const uint8_t exponent)
: _mantissa(mantissa), _exponent(exponent) {}
float asFloat() const {
// обратное преобразование из float24_t во float
ufloat_t converter;
converter.mantissa = static_cast<uint32_t>(_mantissa & ~0x8000) << 8;
converter.exponent = _exponent;
converter.sign = sign();
return converter.f;
}
uint16_t mantissa() const {
// мантисса в формате 1.M
return _mantissa | 0x8000;
}
uint8_t exponent() const {
return _exponent;
}
uint8_t sign() const {
// знак числа (если старший бит мантиссы == 0, то число положительное, если == 1, то отрицательное)
return bitRead(_mantissa, 15);
}
float24_t operator+(const float24_t &other) const {
return addition(other, 0);
}
float24_t operator-(const float24_t &other) const {
return addition(other, 1);
}
float24_t operator*(const float24_t &other) const {
// реализация "в лоб" путем перемножения мантисс с использованием переменой двойной разрядности (uint32_t)
// TODO: сделать проверку на NULL, INF, NAN
uint32_t product = static_cast<uint32_t>(mantissa()) * other.mantissa(); // перемножаем мантисы, результат в uint32_t
uint8_t exp = exponent() + other.exponent() + 130; // вычисляем экспоненту результата умножения (сумма экспонент) с учетом сдвига результата при приведении к uint16_t и переполнения uint8_t
while (!bitRead(product, 31)) { // выравниванием по левому краю (старший бит должен быть равен 1) с корректировкой экспоненты
product <<= 1;
exp--;
}
uint16_t result = static_cast<uint16_t>(product >> 16); // отсекаем младшие разряды, результат приводим к uin16_t
if (bitRead(product, 15)) result++; // если последний отсеченный бит == 1, то увеличиваем значение результата на 1 (аналог округления 0,5 до целого)
if (!(sign() ^ other.sign())) bitClear(result, 15); // если результат умножениея положительное число, то обнуляем старший бит (бит знака float24_t)
return float24_t(result, exp);
}
float24_t operator/(const float24_t &other) const {
// реализация деления "столбиком" со сдвигом делимого влево
// TODO: сделать проверку на NULL, INF, NAN
const uint16_t divisor = other.mantissa();
uint24_t dividend = mantissa(); // делимое приводим к типу uint24_t чтобы не заморачиваться с переполнением uint16_t при вычитаниях
uint16_t quotient = 0;
while (!bitRead(quotient, 15)) { // частное от деления выравниваем по левому краю (старший бит должен быть равен 1)
quotient <<= 1;
if (dividend >= divisor) {
dividend -= divisor;
quotient |= 1;
}
dividend <<= 1;
}
if (!(sign() ^ other.sign())) bitClear(quotient, 15); // если результат деления положительное число, от обнуляем старший бит (бит знака float24_t)
uint8_t exp = exponent() - other.exponent(); // вычисляем экспоненту (при делении экспоненты вычитаются)
exp += mantissa() < divisor ? 126 : 127; // корректируем экспоненту с учетом дополнительного сдвига (если делимое меньше делителя на 1 шаге)
return float24_t(quotient, exp);
}
protected:
float24_t addition(const float24_t other, const uint8_t minus) const __attribute__((always_inline)) {
// реализация сложения (minus == 0) / вычитания (minus == 1)
// сначала определяем операнд с большим порядком и приводим к нему операнд с меньшим порядком путем сдвига его мантиссы вправо на разницу порядков (delta),
// производим сложение (вычитание) и нормализуем результат операции
// наибольший операнд представлен переменными с префиксом "left_", наименьший - с префиксом "right_"
uint8_t left_exponent, left_sign;
uint16_t right_mantissa, left_mantissa;
int8_t delta = static_cast<const int8_t>(exponent() - other.exponent());
if (delta < 0) {
left_mantissa = other.mantissa();
left_exponent = other.exponent();
left_sign = other.sign();
delta = -delta;
right_mantissa = mantissa();
} else {
left_mantissa = mantissa();
left_exponent = exponent();
left_sign = sign();
right_mantissa = other.mantissa();
}
// если порядки операндов отличаются больше чем на 15, то результатом операции будет наибольший операнд
if (delta > 15) {
if (!left_sign) bitClear(left_mantissa, 15); // если число положительное, то обнуляем старший бит мантиссы
return float24_t(left_mantissa, left_exponent);
}
right_mantissa >>= delta; // сдвигаем мантиссу
if (sign() ^ other.sign() ^ minus) { // тип операции определяется как XOR знаков операндов и значения minus
left_mantissa -= right_mantissa;
if (left_mantissa) {
while (!bitRead(left_mantissa, 15)) { // выравниванием по левому краю (старший бит должен быть равен 1) с корректировкой экспоненты
left_mantissa <<= 1;
left_exponent--;
}
}
} else {
left_mantissa += right_mantissa;
if (!bitRead(left_mantissa, 15)) { // выравниванием по левому краю (старший бит должен быть равен 1) с корректировкой экспоненты
left_mantissa >>= 1;
left_exponent++;
}
}
if (!(left_sign ^ minus)) bitClear(left_mantissa, 15); // если число положительное, то обнуляем старший бит мантиссы
return float24_t(left_mantissa, left_exponent);
}
protected:
uint16_t _mantissa;
uint8_t _exponent;
};
#endif // _FLOAT24_H_
Сравнение с float и измерение времени выполнения операций делал с помощью следующего скетча:
float24.ino
#include "float24.h"
inline void startDurationMeasure() {
TCNT1 = 0;
TCCR1A = TCCR1B = 0;
cli();
TCCR1B = 1; // start timer
}
inline void stopDurationMeasure(Stream &log) {
TCCR1B = 0; // stop timer
sei();
const uint16_t count = TCNT1 - 18; // смещение 18 вычислено путем последовательного вызова startDurationMeasure() и stopDurationMeasure()
const float usec = static_cast<float>(count) * (1000000.0 / F_CPU);
log.print("duration: ");
log.print(usec, 3);
log.print(" usec (");
log.print(count);
log.println(" ticks)");
}
void printFloat(const char *const str, const float &value, const uint8_t digits = 5) {
Serial.print(str);
Serial.print(" = ");
Serial.println(value, digits);
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
}
void loop() {
// put your main code here, to run repeatedly:
for (;;) {
while (!Serial.available()) {}
const float a = Serial.parseFloat();
printFloat("a", a);
while (!Serial.available()) {}
const float b = Serial.parseFloat();
printFloat("b", b);
Serial.println();
const float24_t a24 = a;
const float24_t b24 = b;
// деление
startDurationMeasure();
float c = a / b;
stopDurationMeasure(Serial);
printFloat("a / b", c);
startDurationMeasure();
float24_t c24 = a24 / b24;
stopDurationMeasure(Serial);
printFloat("a24 / b24", c24.asFloat());
Serial.println();
// умножение
startDurationMeasure();
c = a * b;
stopDurationMeasure(Serial);
printFloat("a * b", c);
startDurationMeasure();
c24 = a24 * b24;
stopDurationMeasure(Serial);
printFloat("a24 * b24", c24.asFloat());
Serial.println();
// сложение
startDurationMeasure();
c = a + b;
stopDurationMeasure(Serial);
printFloat("a + b", c);
startDurationMeasure();
c24 = a24 + b24;
stopDurationMeasure(Serial);
printFloat("a24 + b24", c24.asFloat());
Serial.println();
// вычитание
startDurationMeasure();
c = a - b;
stopDurationMeasure(Serial);
printFloat("a - b", c);
startDurationMeasure();
c24 = a24 - b24;
stopDurationMeasure(Serial);
printFloat("a24 - b24", c24.asFloat());
Serial.println();
Serial.println();
}
}
Результат выполнения скетча для чисел 1.123 и 2.234:
В среднем в 2 раза быстрее float.
a = 1.12300
b = 2.23400
duration: 29.875 usec (478 ticks)
a / b = 0.50269
duration: 18.437 usec (295 ticks)
a24 / b24 = 0.50267
duration: 10.375 usec (166 ticks)
a * b = 2.50878
duration: 5.125 usec (82 ticks)
a24 * b24 = 2.50879
duration: 8.187 usec (131 ticks)
a + b = 3.35700
duration: 4.063 usec (65 ticks)
a24 + b24 = 3.35699
duration: 8.375 usec (134 ticks)
a - b = -1.11100
duration: 4.063 usec (65 ticks)
a24 - b24 = -1.11102
Возник ряд вопросов:
- Почему, если в методе addition убрать атрибут attribute((always_inline)),
float24_t addition(const float24_t other, const uint8_t minus) const /*__attribute__((always_inline))*/
то время выполнения операций сложения (вычитания) увеличится на 25-28 тактов? Не уж-то вызов метода такой долгий?
2. Почему, если в метод addition передавать other по ссылке,
float24_t addition(const float24_t &other, const uint8_t minus) const /*__attribute__((always_inline))*/
то время выполнения операций увеличивается еще на 5 тактов? Вроде как должно наоборот уменьшиться…
3. Я ожидал более серьезного улучшения быстродействия, но в данной реализации это самое быстрое, что мне удалось получить. Если добавить поддержку специальных значений (nan, inf, null), проверку некорректных операций (например, деление на 0), то время выполнения еще увеличится.
Может мой код можно как-то оптимизировать? Есть более быстрые алгоритмы? Или это предел для 16 битных операций?