Реализация типа float24_t

чую пояснение как направить компилятор GCC не генерировать ассемблерный говнокод закончится пятницей )))

Сегодня АВР, завтра Qualcomm и STM32. Но если приучить себя писать “правильно“, то может пригодиться.

Помимо кэша (ну хоть какой-то “внутренний“ кэш-то должен быть, буффер какой-нибудь захудалый, не побайтно же из флеша исполняется) уменьшается количество условных переходов.

К тому же у автора не написано про авр. Ну или я не увидел.

Вы никогда самомодифицирующийся код не писали? А хоть даже и 1st stage bootloader.

Если вы, к примеру, записали что-то по адресу в памяти, следом за инструкцией, которая в данный момент исполняется (псевдоассемблер):


mov bx, 0x90
mov [label], bx
label:
halt
..
..

Как вы думаете, что произойдет, если считать, что 0x90 - это машинный код операции NOP ? Правильно, несмотря на то, что в памать по адресу, где находится halt хоть и будет записан nop, но.. но только в памяти, а не в кэше. Поэтому, выполнится хальт. Что нужно сделать, чтобы выполнился nop? Нужно сделать jmp. Любой. И кэш перечитается из памяти, на месте хальта появится ноп и все будет шито крыто.

mov bx, 0x90
mov [label], bx
jmp label
label:
halt
..
..

Поэтому, в процессорах в которых нет branch prediction (а у вас в вашем авр его нет), каждый jxx будет вызывать перечитывание памяти в кэш

так неспеша и до защит от прохода под отладчиком доберёмся )))

1 лайк

а это оно и было :)). ну могу из бутлодера выкорчевать чего-нибудь такое же, из ESP32. но не буду.

На секундочку в авр нельзя простыми методамии записать что либо в область кода !!!

для интел я, не программист, знаю семь разных, а сколько методов знают программисты )))

Нуууу….плохой, негодный процессор, что тут скажешь. Скучный.

отож, чтобы написать программу защиты прошивки придётся сильно сильно постараться, видимо, но это не точно

У PIC -ов, по крайней мере, что я имел дело, вообще всего 35 инструкций.
И ничего, живут люди…

не пример кода конечно, но спс и на этом))) почитаю…

Про Intel. Исполняемый код лежит в кодовой странице, помеченной атрибутом “Executable” и писать в неё бестолку, будет исключение. Точно так же, исполнять код из сегмента данных тож нельзя, если включен DEP в Виндовсе. Самое странное, что сегмент стека в С++ программах имеет атрибут Executable, неизвестно зачем.

Здесь как раз тот случай, когда “правильно” зависит от архитектуры.
А если мы говорим о платформенно независимом “правильно”, то оптимизировать нужно алгоритм, а не код. Хотя бы потому, что кодовая оптимизация, как правило, дает не более десятков процентов, а алгоритмическая оптимизация может обеспечить сотни, тысячи раз, а в отдельных случаях и во много раз больше.

Кому должен?

Нет, конечно. Длина инструкции - 2 байта.

Обратного тоже не написано.
А у контроллеров помощнее - там, как правило, тоже нет кэша команд. Есть только конвейер. Если вспомнить продукцию Intel, то конвейер был уже в 8086/88, а кэш команд появился только в Pentium-4.

Прямо в ПЗУ?

Или мы говорим о какой-то иной архитектуре?
Например, в Intel (если, конечно, нам немного не хватает 1 Мбайта) есть защищенный режим, в котором в сегмент кода как-то не очень запишешь.
В общем, какую бы архитектуру не выбирали, в большинстве случаев будет облом.

Не вижу смысла это комментировать.

Добрый день!

Не написал сразу. Целевая платформа - AVR, среда Arduino IDE без изменения настроек. Но думаю, что ни у кого и так сомнений не возникло на этот счет.
Исправил реализацию сложения / вычитания, заодно и обработку нуля добавил.
Сложение и вычитание в итоге самые сложные для меня получились. Так и не смог обработать возможные варианты переполнения uint16_t, пришлось использовать uint24_t для промежуточных вычислений.
Получилось в итоге следующее:

float24.h
#ifndef _FLOAT24_H_
#define _FLOAT24_H_

#include <Arduino.h>
#include <stdint.h>

#undef likely
#undef unlikely
#define unlikely(_X) __builtin_expect(!!(_X), 0)
// #define unlikely(_X)  (_X)
#define likely(_X) __builtin_expect(!!(_X), 1)

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()
    : _mantissa(0), _exponent(0) {}

  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 negate() const {
    uint16_t mantissa = _mantissa;
    bitToggle(mantissa, 15);
    return float24_t(mantissa, _exponent);
  }

  float24_t operator*(const float24_t &other) const {
    // реализация "в лоб" путем перемножения мантисс с использованием переменой двойной разрядности (uint32_t)
    // TODO: сделать проверку на NULL, INF, NAN
    if (unlikely(!_exponent || !other.exponent())) return float24_t(0, 0);

    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
    if (unlikely(!other.exponent())) return float24_t(0, 0xFF);
    if (unlikely(!_exponent)) return float24_t(0, 0);

    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);
  }

  float24_t operator+(const float24_t &other) const {
    // наибольший операнд представлен переменными с префиксом "max_", наименьший - с префиксом "min_"
    const uint16_t &this_mantissa{ mantissa() }, &other_mantissa{ other.mantissa() };
    uint8_t max_sign, max_exponent;
    uint16_t min_mantissa, max_mantissa;

    int8_t delta = static_cast<const int8_t>(exponent() - other.exponent());
    // если разница порядков меньше нуля или при одинаковых порядках мантисса this меньше мантиссы other,
    // то операнд this меньше операнда other
    if (delta < 0 || (!delta && (this_mantissa < other_mantissa))) {
      max_mantissa = other_mantissa;
      max_exponent = other.exponent();
      max_sign = other.sign();
      min_mantissa = this_mantissa;
      delta = -delta;
    } else {
      max_mantissa = this_mantissa;
      max_exponent = exponent();
      max_sign = sign();
      min_mantissa = other_mantissa;
    }

    // если порядки операндов отличаются больше чем на 15, то результатом операции будет наибольший операнд
    if (unlikely(delta > 15)) {
      if (!max_sign) bitClear(max_mantissa, 15);  // если число положительное, то очищаем бит знака (старший бит мантиссы)
      return float24_t(max_mantissa, max_exponent);
    }

    if (delta) min_mantissa >>= delta;

    if (sign() ^ other.sign()) {  // если знаки операндов отличаются, то вычитаем из большего операнда меньший
      max_mantissa -= min_mantissa;
      if (unlikely(!max_mantissa)) return float24_t(0, 0);

      while (!bitRead(max_mantissa, 15)) {  // выравниванием по левому краю (старший бит должен быть равен 1) с корректировкой экспоненты
        max_mantissa <<= 1;
        max_exponent--;
      }
    } else {  // в противном случае складываем операнды
      uint24_t sum = max_mantissa;
      sum += min_mantissa;

      if (bitRead(sum, 16)) {  // выравниванием по левому краю (старший бит должен быть равен 1) с корректировкой экспоненты
        sum >>= 1;
        max_exponent++;
      }
      max_mantissa = sum;
    }

    // устанавливаем знак результата операции
    if (!max_sign) bitClear(max_mantissa, 15);

    return float24_t(max_mantissa, max_exponent);
  }

  float24_t operator-(const float24_t &other) const {
    // реализация основана на инвертировании знака оператора other, после чего операторы складываюися
    // наибольший операнд представлен переменными с префиксом "max_", наименьший - с префиксом "min_"
    const uint16_t &this_mantissa{ mantissa() }, &other_mantissa{ other.mantissa() };
    const uint8_t other_sign = !other.sign();  // инвертируем знак оператора other
    uint8_t max_sign, max_exponent;
    uint16_t min_mantissa, max_mantissa;

    int8_t delta = static_cast<const int8_t>(exponent() - other.exponent());
    // если разница порядков меньше нуля или при одинаковых порядках мантисса this меньше мантиссы other,
    // то операнд this меньше операнда other
    if (delta < 0 || (!delta && (this_mantissa < other_mantissa))) {
      max_mantissa = other_mantissa;
      max_exponent = other.exponent();
      max_sign = other_sign;
      min_mantissa = this_mantissa;
      delta = -delta;
    } else {
      max_mantissa = this_mantissa;
      max_exponent = exponent();
      max_sign = sign();
      min_mantissa = other_mantissa;
    }

    // если порядки операндов отличаются больше чем на 15, то результатом операции будет наибольший операнд
    if (unlikely(delta > 15)) {
      if (!max_sign) bitClear(max_mantissa, 15);  // если число положительное, то очищаем бит знака (старший бит мантиссы)
      return float24_t(max_mantissa, max_exponent);
    }

    if (delta) min_mantissa >>= delta;

    if (sign() ^ other_sign) {  // если знаки операндов отличаются, то вычитаем из большего операнда меньший
      max_mantissa -= min_mantissa;
      if (unlikely(!max_mantissa)) return float24_t(0, 0);

      while (!bitRead(max_mantissa, 15)) {  // выравниванием по левому краю (старший бит должен быть равен 1) с корректировкой экспоненты
        max_mantissa <<= 1;
        max_exponent--;
      }
    } else {  // в противном случае складываем операнды
      uint24_t sum = max_mantissa;
      sum += min_mantissa;

      if (bitRead(sum, 16)) {  // выравниванием по левому краю (старший бит должен быть равен 1) с корректировкой экспоненты
        sum >>= 1;
        max_exponent++;
      }
      max_mantissa = sum;
    }

    // устанавливаем знак результата операции
    if (!max_sign) bitClear(max_mantissa, 15);

    return float24_t(max_mantissa, max_exponent);
  }

protected:
  uint16_t _mantissa;
  uint8_t _exponent;
};

#endif  // _FLOAT24_H_

Тестирование выполнения отдельных операций float24_t приведенным в 1 посте скетчем дает выигрыш по времени в 1,5…2 раза по сравнению с float.

Теперь попробовал применть этот класс для чего-то полезного, например, для рассчета величины измеряемого давления мостовым тензодатчиком с коррекцией температурной погрешности.
Модель датчика представляет собой функцию двух параметров P(nP, nT), аппрокисимируется полиномами 2 порядка и имеет 9 коэффициентов.
Проверочный скетч приведен ниже.

float24.ino
#include "float24.h"

void printFloatE(const char *const str, const float &value, const uint8_t digits = 5) {
  char buf[16];
  Serial.print(str);
  Serial.print(" = ");
  dtostre(value, buf, digits, DTOSTR_UPPERCASE);
  Serial.println(buf);
}

float kff[3][3] = { { 9.9237E-13, -1.2702E-09, 3.6321E-07 },
                    { -7.9330E-10, 3.7138E-06, 8.3755E-03 },
                    { 1.7514E-07, -8.0434E-04, -7.9905E-01 } };
float24_t kff24[3][3];


float calculatePressure(const uint16_t &nP, const uint16_t &nT) {
  cli();
  TCCR1A = 0;                          // Normal mode
  TCCR1B = (1 << CS11) | (1 << CS10);  // Prescaler = 64
  TCNT1 = 0;                           // сброс счётчика

  float fT = nT;
  float fT2 = fT * fT;
  float fP = nP;
  float fP2 = fP * fP;

  float a2 = kff[0][0] * fT2 + kff[0][1] * fT + kff[0][2];
  float a1 = kff[1][0] * fT2 + kff[1][1] * fT + kff[1][2];
  float a0 = kff[2][0] * fT2 + kff[2][1] * fT + kff[2][2];

  float result = a2 * fP2 + a1 * fP + a0;

  TCCR1B = 0;  // stop timer
  sei();
  uint32_t usec = TCNT1 * 4;

  Serial.print("duration: ");
  Serial.print(usec, 3);
  Serial.println(" usec");

  return result;
}

float24_t calculatePressure24(const uint16_t &nP, const uint16_t &nT) {
  cli();
  TCCR1A = 0;                          // Normal mode
  TCCR1B = (1 << CS11) | (1 << CS10);  // Prescaler = 64
  TCNT1 = 0;                           // сброс счётчика

  float24_t fT = nT;
  float24_t fT2 = fT * fT;
  float24_t fP = nP;
  float24_t fP2 = fP * fP;

  float24_t a2 = kff24[0][0] * fT2 + kff24[0][1] * fT + kff24[0][2];
  float24_t a1 = kff24[1][0] * fT2 + kff24[1][1] * fT + kff24[1][2];
  float24_t a0 = kff24[2][0] * fT2 + kff24[2][1] * fT + kff24[2][2];

  float24_t result = a2 * fP2 + a1 * fP + a0;

  TCCR1B = 0;  // stop timer
  sei();
  uint32_t usec = TCNT1 * 4;

  Serial.print("duration: ");
  Serial.print(usec, 3);
  Serial.println(" usec");

  return result;
}

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  for (uint8_t i = 0; i < 3; i++) {
    for (uint8_t j = 0; j < 3; j++) {
      kff24[i][j] = kff[i][j];
      char buf[16];
      dtostre(kff24[i][j].asFloat(), buf, 5, DTOSTR_UPPERCASE);
      Serial.print(buf);
      Serial.print(" ");
    }
    Serial.println();
  }
  Serial.println();

  for (;;) {
    while (!Serial.available()) {}
    const uint16_t nP = Serial.parseInt();
    Serial.print("nP = ");
    Serial.println(nP);

    while (!Serial.available()) {}
    const uint16_t nT = Serial.parseInt();
    Serial.print("nT = ");
    Serial.println(nT);
    Serial.println();

    float press = calculatePressure(nP, nT);
    Serial.print("Press = ");
    Serial.println(press, 5);
    Serial.println();

    float24_t press24 = calculatePressure24(nP, nT);
    Serial.print("Press24 = ");
    Serial.println(press24.asFloat(), 5);
    Serial.println();
    Serial.println();
  }
}

void loop() {
  // put your main code here, to run repeatedly:
}

Результат меня не порадовал. Время рассчета значения функции P(nP, nT) с использованием float24_t практически идентично расчетам с float. Примерно 12 мс.
Почему так происходит я не понял.
По совету @vvb333007 попробовал применить макрос unlikely. Эффекта не получил.

Теперь про цели.
Сначала просто было интересно попробовать написать класс с перегрузками операторов, потом появилась мысль сделать из него класс обработки чисел с плавающей запятой быстрее, чем встроенный float.
Размер кода особо не волновал, а вот память раздувать не хотелось. Поэтому ограничился хранением всех параметров класса в 24 битах. Точности в 4-5 разрядов вполне достаточно.

Кстати, sizeof(float24_t) дает результат 3 байта, хотя я ожидал что будет еще + 2 байта для this. Пока не разобрался почему так получается.

В общем опыт получен, но сам класс в таком виде для применения в практических задачах типа описанной выше не годится.
“Иссяк” у меня немного “запал”. В будущем возможно попробую переписать этот класс на ассемблере (хотя бы для получения опыта работы с ассемблером), хотя ощутимого практического эффекта не ожидаю.

Что??
Откуда там this?

Откуда там this?

Ну это же класс, а экземпляр класса по моему мнению имеет this.
sitzeof(kff24) дает 27, т.е. по 3 байта на элемент. Опять ничего лишнего. Я не понимаю почему так.

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

Понятно, спасибо! Неправильное у меня было представление об этом.

Кстати, если вы в курсе про this, почему он у вас не используется?:slight_smile:
Вот эти методы, вообще-то, если ударяться в пуританство, должны быть написаны с использованием this:

Если вы разнесете объявление и определение методов класса по разным файлам, на многих архитектурах ваш код перестанет компилироваться без явного указания this

1 лайк

Да, я это знаю. Можно, а может даже и нужно

return this->_mantissa | 0x8000;