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

Добрый день!

В качестве саморазвития написал основу класса 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

Возник ряд вопросов:

  1. Почему, если в методе 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 битных операций?

С какой целью оптимизация и по каким критериям?

1 лайк

Оптимизация по скорости выполнения. Все-таки хочется довести этот класс до ума. Ну и использовать вместо float где без него не обойтись.

И как это связано с саморазвитием?
Ведь если кто-то оптимизирует Ваш код, это уже будет не саморазвитие.

Чтобы “довести до ума”, нужно сформулировать цели. Саморазвитие - одна цель, скорость выполнения арифметических операций - другая, занимаемый объем памяти - третья, унификация - четвертый. А есть еще точность, диапазон представления и т.д. И по ним тоже можно оптимизировать.

Если “не обойтись”, то “вместо” - никак не получится.

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

да.

Вызов функции как таковой - не быстрый процесс.

Для этого и придумали inline. Параметры в стек положить надо, this положить в стек надо. Дофига дел.

Почему +5 тактов при передаче по ссылке можно гадать, но проще скомпилировать с флагом -S да посмотреть что там компилятор нагенерил

а вам для ардуино и esp(для мк) ? или для чего ?

можно вставить что то на языке ассемблер, и офигевать потом от багов, из за того что часть кода делается сверхбыстро, и другая часть на с++ не успевает…

Видимо вы просто не вкуриваете ассемблер …

есесвенно я его не вкуриваю, я не умею писать на нем, если бы умел не написал такое сообщение выше)))
а зачем вы перефразируете то что я написал выше ?)))



#define LED_PIN 13

void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}

void loop() {
rapidToggle();// Ассемблер переключает пин туда-обратно БЫСТРО
checkImmediately();// C++ проверяет состояние СРАЗУ после переключения
}

void rapidToggle() {
asm volatile (
"ldi r24, 0x20    \n"  // Бит для пина 13
"in r25, 0x05     \n"  // Читаем PORTB
"eor r25, r24     \n"  // Инвертируем (0→1)
"out 0x05, r25    \n"  // Записываем
"eor r25, r24     \n"  // Возвращаем обратно (1→0)
"out 0x05, r25    \n"  // Записываем обратно 
// ВСЁ выполняется за 4 такта (~250 нс при 16 МГц)!
:
:
: "r24", "r25"
);
}

void checkImmediately() {
int state = digitalRead(LED_PIN);
Serial.print("Состояние пина: ");
Serial.println(state);
// C++ ВСЕГДА видит 0, потому что ассемблер успевает:
// - переключить 0→1
// - переключить 1→0  
// - до того как digitalRead() выполнится
}

коллега, подскажите пж как исправить ?
добавить задержку ?))) а если добавить как ее высчитывать ?

еще на этом форуме видел примеры, но не помню где и у кого, где вроде такие же примеры(проблемы, с тем что часть кода не успевает за другой), но чуть сложнее, если не долго, поделитесь пж ссылкой или кодом

вроде я даже видел эту проблему у ua6em на rp2040, может он поделится кодом… но могу и ошибаться

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

#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 {
public:
float24_t(const float value = 0.0f) {
ufloat_t converter{ value };
_exponent = converter.exponent;
_mantissa = static_cast<uint16_t>(converter.mantissa >> 8) & ~0x8000;
if (converter.mantissa & 0x80) _mantissa++;
}

float24_t(const uint16_t mantissa, const uint8_t exponent)
: _mantissa(mantissa), _exponent(exponent) {}

float asFloat() const {
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 {
return _mantissa | 0x8000;
}

uint8_t exponent() const {
return _exponent;
}

uint8_t sign() const {
return (_mantissa >> 15) & 1;
}

float24_t operator+(const float24_t &other) const {
return addition_optimized(other, 0);
}

float24_t operator-(const float24_t &other) const {
return addition_optimized(other, 1);
}

float24_t operator*(const float24_t &other) const {
return multiplication_optimized(other);
}

float24_t operator/(const float24_t &other) const {
return division_optimized(other);
}

float24_t operator-() const {
return float24_t(_mantissa ^ 0x8000, _exponent);
}

static float24_t sqrt(const float24_t &x);
static float24_t sin(const float24_t &x);
static float24_t cos(const float24_t &x);
static float24_t exp(const float24_t &x);
static float24_t log(const float24_t &x);
static float24_t pow(const float24_t &base, const float24_t &exponent);

float24_t abs() const {
return float24_t(_mantissa & ~0x8000, _exponent);
}

private:
float24_t addition_optimized(const float24_t other, const uint8_t minus) const __attribute__((always_inline)) {
if (_exponent == 0 && _mantissa == 0) {
return minus ? float24_t(other._mantissa ^ 0x8000, other._exponent) : other;
}
if (other._exponent == 0 && other._mantissa == 0) return *this;

int16_t delta = static_cast<int16_t>(_exponent) - other._exponent;
uint16_t left_mant, right_mant;
uint8_t left_exp, left_sign;

if (delta >= 0) {
left_mant = mantissa();
left_exp = _exponent;
left_sign = sign();
right_mant = other.mantissa();
} else {
left_mant = other.mantissa();
left_exp = other._exponent;
left_sign = other.sign();
right_mant = mantissa();
delta = -delta;
}

if (delta > 15) {
if (!(left_sign ^ minus)) left_mant &= ~0x8000;
return float24_t(left_mant, left_exp);
}

right_mant >>= delta;

if (sign() ^ other.sign() ^ minus) {
left_mant -= right_mant;
if (left_mant != 0) {
while ((left_mant & 0x8000) == 0) {
left_mant <<= 1;
left_exp--;
}
}
} else {
left_mant += right_mant;
if ((left_mant & 0x8000) == 0) {
left_mant >>= 1;
left_exp++;
}
}

if (!(left_sign ^ minus)) left_mant &= ~0x8000;
return float24_t(left_mant, left_exp);
}

float24_t multiplication_optimized(const float24_t &other) const __attribute__((always_inline)) {
if (_exponent == 0 && _mantissa == 0) return *this;
if (other._exponent == 0 && other._mantissa == 0) return other;

uint16_t a_mant = mantissa();
uint16_t b_mant = other.mantissa();

uint32_t product;
asm volatile(
"mul %A1, %A2 \n\t"
"movw %A0, r0 \n\t"
"mul %B1, %B2 \n\t"
"mov %B0, r0 \n\t"
"mul %A1, %B2 \n\t"
"add %A0, r0 \n\t"
"adc %B0, r1 \n\t"
"mul %B1, %A2 \n\t"
"add %A0, r0 \n\t"
"adc %B0, r1 \n\t"
"clr %C0 \n\t"
"clr %D0 \n\t"
: "=&r" (product)
: "r" (a_mant), "r" (b_mant)
);

int16_t exp = static_cast<int16_t>(_exponent) + other._exponent - 127;

if ((product & 0x80000000) == 0) {
product <<= 1;
exp--;
}

uint16_t result_mant = (product >> 16);
if (product & 0x8000) result_mant++;

if (exp > 254) exp = 254;
if (exp < 1) exp = 1;

if (sign() ^ other.sign()) result_mant |= 0x8000;
else result_mant &= ~0x8000;

return float24_t(result_mant, static_cast<uint8_t>(exp));
}

float24_t division_optimized(const float24_t &other) const __attribute__((always_inline)) {
if (other._exponent == 0 && other._mantissa == 0) {
uint16_t result = 0x7FFF;
if (sign() ^ other.sign()) result |= 0x8000;
return float24_t(result, 0xFF);
}
if (_exponent == 0 && _mantissa == 0) {
return float24_t(0, 0);
}

uint16_t dividend = mantissa();
uint16_t divisor = other.mantissa();
uint16_t quotient = 0;
uint16_t remainder = 0;

for (int8_t i = 15; i >= 0; i--) {
remainder = (remainder << 1) | ((dividend >> i) & 1);
if (remainder >= divisor) {
remainder -= divisor;
quotient |= (1 << i);
}
}

int16_t exp = static_cast<int16_t>(_exponent) - other._exponent + 127;

if ((quotient & 0x8000) == 0) {
quotient <<= 1;
exp--;
}

if (exp > 254) exp = 254;
if (exp < 1) exp = 1;

if (!(sign() ^ other.sign())) quotient &= ~0x8000;

return float24_t(quotient, static_cast<uint8_t>(exp));
}

protected:
uint16_t _mantissa;
uint8_t _exponent;
};

inline float24_t float24_t::sqrt(const float24_t &x) {
if (x._exponent == 0 && x._mantissa == 0) return x;
if (x.sign()) return float24_t(0x7FFF, 0xFF);

float24_t y = x;
y._exponent = (y._exponent + 127) >> 1;

for (uint8_t i = 0; i < 2; i++) {
y = (y + x / y) * float24_t(0.5f);
}
return y;
}

inline float24_t float24_t::sin(const float24_t &x) {
float24_t reduced = x;
float24_t x2 = x * x;
float24_t result = x * float24_t(0.9999966f);
result = result - (x * x2) * float24_t(0.16664824f);
result = result + (x * x2 * x2) * float24_t(0.00830629f);
result = result - (x * x2 * x2 * x2) * float24_t(0.00018363f);
return result;
}

inline float24_t float24_t::cos(const float24_t &x) {
return sin(x + float24_t(1.5707963f));
}

inline float24_t float24_t::exp(const float24_t &x) {
if (x._exponent == 0 && x._mantissa == 0) return float24_t(1.0f);

float24_t result = float24_t(1.0f) + x;
float24_t x2 = x * x;
result = result + x2 * float24_t(0.5f);
result = result + (x2 * x) * float24_t(0.1666667f);
result = result + (x2 * x2) * float24_t(0.0416667f);
return result;
}

inline float24_t float24_t::log(const float24_t &x) {
if (x._exponent == 0 && x._mantissa == 0) {
return float24_t(0x7FFF, 0xFF);
}

float24_t y = (x - float24_t(1.0f)) / (x + float24_t(1.0f));
float24_t y2 = y * y;
float24_t result = y * float24_t(2.0f);
result = result + (y * y2) * float24_t(0.6666667f);
result = result + (y * y2 * y2) * float24_t(0.4f);
return result;
}

inline float24_t float24_t::pow(const float24_t &base, const float24_t &exponent) {
return exp(exponent * log(base));
}

#endif

Для юмора ведь другой раздел есть.

Дим-мычъ ладно последний код не компилируется,(и времени сейчас мало) но а там что ?)
только не надо снова меня перефразировать, что я не умею пользоваться ассемблером, я это и так знаю!)))

лично я не понимаю, вроде как раз в тему, где код на с++ не успевает, но может и ошибаюсь, если что не так подскажите пж))

Извини @BABOS , у меня ещё работа, не до розыгышей

Дим-мычъ так флудить как раз надо строго во время работы!

я часто шучу, но не тут(ну разве чуть чуть) , и мне реально интересно почему ассемблеровская вставка так себя ведет, не успевает же…

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

А что в авр появилась многозадачность ? Вы бы хоть на таймер какой повесили toggle функцию ассемблерную хоть какая то асинхронность появилась бы …

Да. Даже не алгоритмически. У вас слишком много вложенных вызовов. Вам нужно уменьшить кол-во функций.

Второе - помогите компилятору понять, какие условия у вас выполняются редко, а какие - часто:

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

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

if (unlikely(exponent == 0 && mantissa == 0)) {

}

Ну и наоборот, если if скорее исполнмтся, чем не исполнится:

char *mem = (char *)malloc(1024); // скорее всего память выделилась
if (likely(mem != NULL)) {

}

Затем надо понять область применения: если это быстрые вычисления (3д графика, например), то будет одна философия оптимизации, если это экономия памяти - то другая.

Ассемблером обычно (но не всегда) полируют в самом-самом конце.

Сделайте вставку на С++, она точно так же себя вести будет.
Как код написан - так и исполняется…

О как интересно. Не встречал таких квалификаторов раньше. Это в каком-нибудь с++23 появилось? И как они помогут компилятору сделать код быстрее?

Нет, это стародревний прикол компиляторов GCC. В ядре линукса в тыще мест используется, да много где. Главное не переусердствовать и использовать только там, где это реально имеет смысл

Оптимизация достигается за счет того, что компилятор генерирует код, который меньше ветвится при дефолтовом пути исполнения:

могло бы быть, условно, так:

if (ptr != NULL) {
  // pointer is ok
} else {
  // pointer is not ok
}
cmp ax, 0

jne Pointer_Is_Ok

..

…

Pointer_Is_Ok:

…

…

Компилятор сгенерировал jne переход, который почти всегда будет срабатывать. А это плохо для кэша ну и лишние такты процесора.

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

cmp ax, 0
je Pointer_is_bad:
....
....
...
Pointer_is_bad:

Так как ветвление - это частая операция (каждый if, else, switch и еще много чего) превращается в jmp, jne, jge и тому подобное. Частенько при условном переходе происходит сброс кэша инструкций. Для процессоров, которые исполняют из флеша через внутренний кэш это может быть существенно. Большие ЦПУ обычно имею branch prediction unit, который более менее справляется. Но то на больших процессорах. А на наших - каждый jmp = перечитываем кэш инструкций.

Какой кэш инструкций может быть на AVR?

Забористая трава у вас ….