чую пояснение как направить компилятор 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 будет вызывать перечитывание памяти в кэш
так неспеша и до защит от прохода под отладчиком доберёмся )))
а это оно и было :)). ну могу из бутлодера выкорчевать чего-нибудь такое же, из 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, почему он у вас не используется?
Вот эти методы, вообще-то, если ударяться в пуританство, должны быть написаны с использованием this:
Если вы разнесете объявление и определение методов класса по разным файлам, на многих архитектурах ваш код перестанет компилироваться без явного указания this
Да, я это знаю. Можно, а может даже и нужно
return this->_mantissa | 0x8000;