Оптимизация места для хранения классов на C в библиотеке Arduino

#c #arduino

#c #arduino

Вопрос:

Я пишу библиотеку Arduino для переноса функций вывода ( digitalRead , digitalWrite , analogRead , и т.д.). Например, у меня есть класс RegularPin, который является сквозным, и класс InvertedPin, который инвертирует логику вывода. Это полезно при переходе от макетной платы со светодиодами к плате реле, которая инвертирует логику схемы. Мне просто нужно поменяться классами. У меня также есть класс DebouncedPin для кнопок, который проверяет, нажимает или отпускает пользователь достаточно долго, чтобы кнопка действительно была нажата / отпущена.

Пример для аналоговых выводов:

 // AnalogInPin ------------------------------
class AnalogInPin
{
  public:
    virtual int read()=0;
    virtual int getNo()=0;
};

// AnalogRegInPin ---------------------------

template<int pinNo>
class AnalogRegInPin : public AnalogInPin
{
  public:
    AnalogRegInPin();
    int read();
    int getNo(){return pinNo;}
};

template<int pinNo>
int AnalogRegInPin<pinNo>::read()
{
    return analogRead(pinNo);
}

template<int pinNo>
AnalogRegInPin<pinNo>::AnalogRegInPin()
{
    pinMode(pinNo, INPUT);
}
 

Как вы можете видеть, я помещаю pin-код в объявление шаблона, потому что его нельзя изменять во время выполнения, и я не хочу, чтобы pin-код использовал память при выделении pin-объекта, как в ванильном коде arduino C. Я знаю, что классы не могут быть нулевого размера, но читайте дальше. Затем я хочу написать класс «AveragedPin», который будет автоматически считывать выбранный вывод несколько раз, и я хотел бы сложить свои шаблонные классы следующим образом :

 AveragedPin<cAnalogRegInPin<A0>, UPDATE_ON_READ|RESET_ON_READ> ava0;
 

или даже :

 RangeCorrectedPin<AveragedPin<cAnalogRegInPin<A0>, 
    UPDATE_ON_READ|RESET_ON_READ,RAW_MIN,RAW_MAX,TARGET_RANGE> rcava0;
 

На данный момент я объявил вложенный pin-код как закрытый элемент, потому что не разрешено использовать объект класса в объявлении шаблона. Но тогда каждый слой вложенности бесполезно съедает несколько байтов в стеке.

Я знаю, что мог бы использовать ссылки в объявлении шаблона, но я не совсем понимаю, как это работает / должно использоваться. Моя проблема выглядит как оптимизация пустых членов, но, похоже, здесь это неприменимо.

Я чувствую, что это скорее вопрос C , чем arduino, и я не эксперт по C . Я предполагаю, что это касается более продвинутых частей C . Может быть, то, что я хочу, невозможно или только с последними версиями C (20?).

Ниже приведен код для класса FixedRangeCorrectedPin.

 template <class P, int rawMin, int rawMax, int targetRange>
class FixedRangeCorrectedPin : public AnalogInPin
{
  public:
    int read();
    int getNo(){return pin.getNo();}
  private:
    P pin;
};

template <class P, int rawMin, int rawMax, int targetRange>
int FixedRangeCorrectedPin<P, rawMin, rawMax, targetRange>::read()
{
    int rawRange = rawMax - rawMin;
    long int result = pin.read() - rawMin;
    if (result < 0) result = 0;
    result = result * targetRange / rawRange;
    if (result > targetRange) result = targetRange;
    return resu<
}
 

Моя проблема в том, что я хотел бы удалить член класса ‘P pin’ и заменить его в объявлении шаблона, например, в template <AnalogInPin pin,int rawMin,int rawMax,int targetRange> , потому что, какой вывод здесь задействован, полностью известен во время компиляции.

Комментарии:

1. шаблон класса создает новый класс для каждого набора параметров типа. используйте параметр конструктора для PIN-кода

2. pinNo известно только во время выполнения? Параметры шаблона должны быть константами времени компиляции

3. arduino совершенно не конкретизирует используемый компилятор или аппаратное обеспечение. На вопросы об оптимизации чего-либо мы должны знать ядро процессора и используемый компилятор. 8-битный avr полностью отличается от 32-битного arm. Например, avr gcc хранит vtables, таблицы переходов для коммутатора / корпуса и другие бесполезные вещи в ОЗУ, что долгое время является ошибкой gcc!

4. @juraj: Я не понимаю, как параметр конструктора не будет заканчиваться в члене класса и, следовательно, в стеке.

5. @largest_prime_is_463035818: я заявил, что «это не должно изменяться во время выполнения» и, следовательно, наверняка известно во время компиляции.

Ответ №1:

Как вы можете видеть, я помещаю pin-код в объявление шаблона, потому что его нельзя изменять во время выполнения, и я не хочу, чтобы pin-код использовал память при выделении pin-объекта, как в ванильном коде arduino C.

Хорошо, если pin-код является константой времени компиляции, как это обычно бывает для Arduino, этот бит подходит.

Однако, делая AnalogInPin базовый класс абстрактным (т. Е. Добавляя virtual методы), На практике будет использоваться как минимум столько же места на объект, сколько вы сэкономили, не сохраняя pin-код как целое число.

Детали зависят от реализации, но полиморфизм во время выполнения требует некоторого способа определения для данного объекта производного класса, на который указывает an AnalogInPin* , какую версию виртуальных методов вызывать, и для этого требуется хранилище в каждом объекте производного типа. (Вы можете убедиться, что это правда, просто проверив sizeof(AnalogInPin) и сравнив с sizeof идентичным классом без virtual методов.

Я знаю, что классы не могут иметь нулевой размер, но…

Существует особый случай для базовых классов без элементов данных, который позволяет им не занимать дополнительного размера (экземпляр наиболее производного типа все равно должен занимать не менее одного байта). Это называется оптимизацией пустого базового класса.

На данный момент я объявил вложенный pin-код как закрытый элемент, потому что не разрешено использовать объект класса в объявлении шаблона. Но тогда каждый уровень вложенности бесполезно съедает несколько байтов в стеке.

Мы можем сгладить все это (и в идеале удалить абстрактную базу тоже, если только у вас нет не шаблонного кода, который в этом нуждается).:

 template <int PIN, template <int> class BASE>
struct AveragedPin: public BASE<PIN>
{
    int read() override { /* call BASE<PIN>::read() several times */ }
    int getNo() override { return PIN; }
};
 

Однако обратите внимание, что мы могли бы просто использовать унаследованное getNo , а затем вообще не использовать PIN . Таким образом , вместо объявления усредненного экземпляра pin — кода как AveragedPin<MY_PIN, AnalogInPin> myAveragedPin; , мы могли бы изменить определение на

 template <class BASE>
struct AveragedPin: public BASE
{
    int read() override { /* call BASE::read() several times */ }
    using BASE::getNo; // not really required unless it is hidden
};
 

и объявите экземпляр как AveragedPin<AnalogInPin<MY_PIN>> myAveragedPin; .

PIN-код с поправкой на диапазон может быть аналогичным, но с дополнительными параметрами шаблона для флагов и минимальных / максимальных границ, если они известны во время компиляции.

Аналогично, FixedRangeCorrectPin добавленный к вашему вопросу, не должен выводиться из AnalogInPin , а затем также сохранять другой тип PIN-кода. На самом деле, он может просто наследовать базовый класс

 template <class P,int rawMin,int rawMax,int targetRange>
struct FixedRangeCorrectedPin : public P
{
    int read(); // calls P::read()
    // inherit getNo again
};
 

опять же, объявляя экземпляр, подобный FixedRangeCorrectPin<AnalogInPin<MY_PIN>, RMIN, RMAX, TARGET> myFixedPin;


Редактировать пример среднего значения по переменному числу выводов без дополнительных затрат на хранение, предполагая, что мы изменили virtual методы на static :

 template <class... PINS>
struct AveragedPins
{
  static int read()
  {
    return (PINS::read()   ...) / sizeof...(PINS);
  }
};
 

При этом не имеет значения, какого рода PIN-код является аргументом, если он имеет статический read метод. Вы можете складывать его так, как вам нравится:

 using a1 = FixedRangeCorrectedPin<A_1, 0, 255, 128>;
using a2 = AnalogInPin<A_2>;
using a3 = AnalogInPin<A_3>;
using a4 = AnalogInPin<A_4>;
using a34 = AveragedPins<a3, a4>;
using all = AveragedPins<a1, a2, a34>;

// now a34::read() = (a3::read()   a4::read())/2
// and all::read() = (a1::read()   a2::read()   a34::read())/3
 

и обратите внимание, что все это просто определения типов: мы не выделяем ни одного байта для каких-либо объектов.


Еще одно замечание: я заметил, что я использую один и тот же CLASS::method() синтаксис двумя немного разными способами.

  1. в первых приведенных выше примерах, которые используют наследование, BASE::read() это де-виртуализированный вызов метода экземпляра.

    То есть мы вызываем BASE версию read метода для this объекта. Вы также могли бы писать this->BASE::read() .

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

  2. в последних примерах, где мы перестали использовать наследование и сделали методы статическими, PIN::read() объекта нет this и нет вообще.

    В принципе, это наиболее похоже на вызов свободной функции C, хотя мы заставляем компилятор генерировать новый ее экземпляр для каждого другого PIN значения (а затем ожидаем, что он все равно будет встроен в вызов).

Комментарии:

1. Это здорово, я не знал, что могу использовать параметры шаблона в объявлении наследования

2. Что, если мне нужно будет написать класс, который имеет дело с двумя или более выводами в качестве входных данных? Например, MinorityReportPin, который игнорирует состояние одного вывода из 3, если оно не совпадает. Я захожу слишком далеко :)?

3. Вы определенно можете это сделать, но для этого потребуется много доработок, чтобы избежать хранения объектов pin. Если вам действительно не нужен базовый класс виртуальные функции, вы можете создать оба метода static . Это немного упрощает оценку по пакету переменных параметров без ненужного хранилища.

4. ПРИМЕЧАНИЕ, прежде чем я забуду: если вы генерируете много разных типов выводов, и в них больше кода, чем в простом случае, вы можете потратить больше места на инструкции, чем на данные. Я думаю, компилятор сообщит вам, если вы достигнете предела.