Существует ли ограничение на количество экземпляров класса `String`?

Платформа

  • Attiny85 с Attinyc
  • 1306 OLED i2c с Tiny4KDisplay

Гол

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

Я пришел из python word, поэтому я использую связанный список для навигации по меню. Я нашел странную ошибку, которая мешает мне отображать часть длинного текста. Я обнаружил, что если определенные узлы превышают определенное количество чисел, то текст элемента более 5 символов отображаться не будет. Я не знаю, почему это связано с количеством экземпляров. Мне удалось сузить диапазон действия жука. В кодексе. если эти 3 строки закомментированы, то все элементы отображаются правильно, если нет, то только до ccccc, но не до dddddd.

without the 3 lines, (commented) with the 3 lines, (uncommented)

Потребление флэш-памяти и оперативной памяти в случае сбоя составляет

Sketch uses 5180 bytes (63%) of program storage space. Maximum is 8192 bytes.
Global variables use 280 bytes (54%) of dynamic memory, leaving 232 bytes for local variables. Maximum is 512 bytes.

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

Спасибо вам за любую информацию.

PS: Я знаю, что есть методы, которые избегают ооп, но я думаю, что эта странная ошибка так близка к исправлению (или оказалась реальной ошибкой в пакете / компиляторе). Поэтому я хотел бы получить ответ на этот вопрос.

Обновление:

Я попытался использовать другой метод, который использует список строк вместо класса для достижения той же функции. Ошибка все еще там. Я думаю, это потому, что существует слишком много экземпляров String . Вопрос не меняется: "Существует ли ограничение на количество экземпляров класса String?".


#include <Tiny4kOLED.h>

// input setup
int x = 512;
int y = 512;
int lastx = 512;
int lasty = 512;

bool left = false;
bool right = false;
bool up = false;
bool down = false;

char input() {
  x = analogRead(A2);
  y = analogRead(A3);
  if( (lastx > 256) & (x <= 256) ){
    left = true;
  }
  if( (lastx < 768) & (x >= 768) ){
    right = true;
  }
  if( (lasty > 256) & (y <= 256) ){
    down = true;
  }
  if( (lasty < 768) & (y >= 768) ){
    up = true;
  }
  lastx = x;
  lasty = y;
}

// item data
class Node  
{  
  public:
    String text; 
    uint8_t id;
    Node * next;
    Node * previous;
    Node * parent;
    Node * child;
    Node(String s, int n){
      text = s;
      id = n;
      next = NULL;
      previous = NULL;
      parent = NULL;
      child = NULL;
    }
    void append_next(Node * other){
      this->next = other;
      other->previous = this;
      other->parent = this->parent;
    }
    void append_child(Node * other){
      this->child = other;
      other->parent = this;
    }
};  

Node main_0("aaa", 1);
Node main_1("bbbb", 2);
Node main_2("ccccc", 3);
Node main_3("dddddd", 4);
Node main_4("eeeeeee", 5);
Node main_5("ffffffff", 6);

Вот как ошибка может быть повторена.

/ * Раскомментируйте следующие 3 строки, это вызовет проблему*/
//Node item_0_0("aaa", 7);
//Node item_0_1("bbb", 8);
//Node item_0_2("ccc", 9);
Node * current_sel = (&main_0);
Node * screen_top = (&main_0);

void setup() {
  // oled setup
  oled.begin(0,0);
  oled.enableChargePump();
  oled.setRotation(1);
  // oled.setInternalIref(true);
  oled.enableZoomIn();
  oled.setFont(FONT6X8);
  oled.clear();
  oled.switchRenderFrame();
  oled.clear();
  oled.switchFrame();
  oled.on();
  // конец настройки oled

  // настройка ввода
  pinMode(A2, INPUT); // X
  pinMode(A3, INPUT); // Y
  // конец настройки ввода

  // data
  main_0.append_next(&main_1);
  main_1.append_next(&main_2);
  main_2.append_next(&main_3);
  main_3.append_next(&main_4);
  main_4.append_next(&main_5);
}

void loop() {
  // input read
  input();

  // перемещение тока;
  if(up){
    up = false;
    if(current_sel->previous != NULL){
      if(screen_top == current_sel){
        screen_top = screen_top->previous;
      }
      current_sel = current_sel->previous;
    }
  }

  if(down){
    down = false;
    if(current_sel->next != NULL){
      if(screen_top->next->next->next == current_sel){
        screen_top = screen_top->next;
      }
      current_sel = current_sel->next;
    }
  }

  // обновление
  oled.clear();
  
  oled.setCursor(64, 0);
  oled.print(current_sel->text);

  Node * current_draw = screen_top;
  int i = 0;
  while(true){
  
    oled.setCursor(0,i);
    if(current_draw == current_sel){
      oled.invertOutput(true);
    }
    oled.print(current_draw->text);
    oled.invertOutput(false);
    
    if(current_draw->next == NULL){
      break;
    }
    
    current_draw = current_draw->next;
    i++;
    if(i >= 4){
      break;
    }
  }
  
  oled.switchFrame();
  delay(50);
}

, 👍0

Обсуждение

каково потребление памяти, о котором сообщает Arduino IDE в конце компиляции?, @Juraj

@Juraj Спасибо за ваш комментарий. Отчет о неудаче: "Sketch использует 5180 байт (63%) пространства для хранения программы. Максимум - 8192 байта. Глобальные переменные используют 280 байт (54%) динамической памяти, оставляя 232 байта для локальных переменных. Максимум - 512 байт. ", @River


2 ответа


Лучший ответ:

1

Каждый экземпляр вашего класса использует оперативную память для хранения своих данных. Всегда существует ограничение на количество экземпляров; не фиксированное число, а ограничение, налагаемое количеством доступной оперативной памяти. То, что Майенко писал о строкеs, в равной степени относится и к экземплярам класса, если экземпляры создаются и уничтожаются во время выполнения.

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

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

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

Обновить: Я действительно замалчивал некоторые детали, на что любезно указали два комментатора. Да, куча - память, выделяемая динамически во время выполнения, - занимает пространство чуть выше глобальных и других статических выделений памяти. Пространство кучи может расти, тем больше оно используется для выделения и удаления памяти, и, будучи низким в памяти, должно было бы расти вверх, к стеку. Это куча, где строковые данные выделяются и перераспределяются, и это одна из причин предупреждения Майенко об использовании строкового типа - особенно если строки будут редактироваться во время выполнения, что приводит к перераспределению, которое может быть неочевидным.

Также стоит отметить, что любой класс использует память, будь то строки или пользовательские классы, такие как узлы OP. Они оба являются классами, и их экземпляры будут обрабатываться аналогично.

Также в случае OP обратите внимание, что экземпляры класса распределяются "статически" - все экземпляры известны и распределены во время установки запуска и не изменены во время запуска. В "кавычках", потому что распределение действительно происходит во время выполнения, но в самом начале запуска и до того, как пользовательский скетч получит управление.

,

Отличный ответ. Однако, вероятно, следует также упомянуть фрагментацию кучи. (Похоже, что ОП использует фиксированное количество статически выделенных объектов, так что это может быть неуместно здесь, но если вы собираетесь дать хороший полный ответ ...), @Duncan C

Стек столкнется с кучей, прежде чем достигнет .bss или .data., @Edgar Bonet


2

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

String использует динамическое распределение для хранения строковых данных в оперативной памяти. Это очень неэффективно, особенно когда вы работаете со строковыми литералами.

Эти литералы должны храниться во флэш-памяти, а не в оперативной памяти, поэтому вам нужно работать с const char * и PROGMEM, а также с макросом F().

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


В качестве примера давайте посмотрим, что это делает:

Node main_5("ffffffff", 6);

У вас есть строка из 8 символов плюс НУЛЕВОЙ завершающий символ. Это 9 байт. Эти 9 байтов копируются в оперативную память во время загрузки. Текущий общий объем 9 байт. Затем вы преобразуете его в строку и копируете символьные данные в этот временный объект - теперь это 18 байт плюс размер строкового объекта. Затем:

text = s;

Вы выделяете 9 байт внутри объекта текстовой строки и копируете туда данные. Теперь у вас есть 27 байт плюс два строковых объекта (один из которых вы создали ранее в своем классе). Чуть более 5% всей вашей памяти просто для перемещения текста по месту. Треть этого вы выцарапываете из своей кучи, когда объекты покидают область видимости, но тогда у вас есть дыра в вашей куче. И это последний набор операций после того, как вы уже полностью испортили свою кучу предыдущими.

Принимая во внимание

const char main_5[] PROGMEM = "ffffffff";

использует только 2 байта оперативной памяти для хранения указателя и ничего больше.

,

Большое вам спасибо за ваш ответ. Я принял другой вариант, потому что он показывает больше понимания того, почему происходит эта ошибка. Но ваш ответ действительно привел меня в правильном направлении. В итоге я пишу код python, имеющий дело со связанным списком, и генерирую связанный код C, который определяет данные во флэш-памяти., @River