Выделение строковой памяти Arduino

Когда строковая переменная объявляется и изменяется внутри функции, куда она попадает? В кучу (поскольку объявление строки является частью динамического выделения памяти) или в стек (поскольку это часть функции)? И восстанавливается ли пространство после того, как функция выходит за рамки? Например

void loop()
{
  Serial.println(foo("def"));
  while(1);
}

String foo(String arg1)
{
 String test = "abc";
 test = test + arg1;
 return test;
}

Значит, в данном случае переменная "test" объявлена в стеке или в куче? И это место в конце восстанавливается или нет?

Изменено: Что, если переменная внутри цикла объявлена, как показано ниже?

String foo_test = F("def");
Serial.println(foo(foo_test));

Каков в этом случае будет статус переменных?

, 👍6

Обсуждение

Есть объект String, а затем есть символьные данные, на которые ссылается этот объект. Они не живут вместе., @Edgar Bonet

Он скомпилировался нормально и тоже показал результат. Что в этом плохого?, @goddland_16

@EdgarBonet, вы хотите сказать, что строковый объект будет находиться в стеке, а ссылка на данные char останется в куче? Простите за непонимание в предыдущем комментарии., @goddland_16

Строковый объект может находиться в стеке функции, в стеке вызывающего объекта или в регистрах процессора. Это вопрос оптимизации компилятора. Для упрощения мысленной картины вы можете предположить, что он находится в стеке функции. Все должно работать «как будто», это так., @Edgar Bonet

@EdgarBonet @Mikael@Michel См. измененное, @goddland_16


3 ответа


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

4

Это отличный вопрос, чтобы проиллюстрировать количество операций копирования строк и кучи (malloc/free), происходящих при использовании класса String Arduino.

void loop()
{
  Serial.println(foo("def"));
  while(1);
}

Компилятор сгенерирует loop() примерно так:

void loop()
{
  // Строковый литерал хранится в памяти программы. Нужно скопировать
  // во временную переменную. Это делается до вызова main().
  static const char temp0[4] PROGMEM = "def";
  static const char temp1[4];
  copy_from_program_memory(temp1, temp0);

  // Параметр вызова foo() на самом деле является временной строкой
  // переменная, построенная из строкового литерала. Хранилище выделено
  // в куче и присваивается из значения строкового литерала.
  String temp2;
  temp2.constructor(temp1);

  // Возвращаемое значение вызова foo() — это переданная строка
  // в Serial.println(). Это также временная строковая переменная.
  String temp3;
  temp3.constructor(foo(temp2));
  Serial.println(temp3);

  // Деструктор класса String должен быть вызван для временного
  // Строковая переменная, чтобы освободить строковые значения в куче.
  temp2.destructor();
  temp3.destructor();
}

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

String foo(String arg1)
{
 String test = "abc";
 test = test + arg1;
 return test;
}

Функция foo() должна копировать строки несколько раз, чтобы выполнить конкатенацию строк. Для оператора + потребуется промежуточная копия строки.

Ура!

PS: Дополнительные сведения см. в приведенном ниже листинге сборки с вызовами функций-членов String. Компилятор сокращает встроенные функции-члены и повторно использует временные локальные переменные. Также встроен вызов foo(). Функция-член Reserve() является частью конструктора.

00000198 <main>:
 1de:   60 df           rcall   .-320       ; 0xa0 <String::reserve(unsigned int)>
 1fc:   50 d2           rcall   .+1184      ; 0x69e <strcpy>
 212:   46 df           rcall   .-372       ; 0xa0 <String::reserve(unsigned int)>
 230:   36 d2           rcall   .+1132      ; 0x69e <strcpy>
 248:   62 df           rcall   .-316       ; 0x10e <String::operator=(String const&)>
 26a:   1a df           rcall   .-460       ; 0xa0 <String::reserve(unsigned int)>
 27e:   0f d2           rcall   .+1054      ; 0x69e <strcpy>
 28e:   3f df           rcall   .-386       ; 0x10e <String::operator=(String const&)>
 294:   60 df           rcall   .-320       ; 0x156 <String::~String()>
 29a:   5d df           rcall   .-326       ; 0x156 <String::~String()>
 2a0:   5a df           rcall   .-332       ; 0x156 <String::~String()>

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

,

Я ожидаю, что copy_from_program_memory(temp1, temp0); будет выполнен только один раз, __do_copy_data(), из среды выполнения C., @Edgar Bonet

Да, это также сделало бы temp1 статическим. Следует исправить это., @Mikael Patel

Это хорошее объяснение. Не могли бы вы предложить хорошие ссылки или статьи по этой проблеме с памятью?, @goddland_16

См. https://hackingmajenkoblog.wordpress.com/2016/02/04/the-evils-of-arduino-strings/, @Mikael Patel

См. также http://www.nongnu.org/avr-libc/user-manual/malloc.html., @Mikael Patel

И последний источник https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/WString.h и https://github.com/arduino/Arduino/blob/master/ оборудование/arduino/avr/ядра/arduino/WString.cpp., @Mikael Patel


1

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

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

Обычно для экземпляра строки сохраняется некоторый объем выделенной памяти, при ее увеличении (или слишком большом увеличении) содержимое строки будет скопировано. Все это происходит в куче, и из-за этого могут возникать «дыры» в куче.

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

,

Вы сказали, что это локальная переменная и она будет храниться в стеке. Вы хотели сказать, что переменная «тест» будет храниться в стеке? Но когда происходит манипуляция со строками, т.е. тест += arg1; разве размер теста не увеличится в куче или это стек?, @goddland_16

Как уже писал Эдгар, есть два пункта: экземпляр (сохраняющий администрацию), это, вероятно, указатель и, возможно, размер строки; это будет храниться в стеке. Распределение памяти никогда не изменится. Однако сами символы хранятся в куче, так как это изменится. Когда вы добавляете символ, в зависимости от реализации, возможно, вся строка будет скопирована из одного пространства кучи в другое место в куче (с использованием еще одного байта), а указатель (хранящийся в стеке) указывает на новое выделенное пространство. место в куче., @Michel Keijzers

Также вы можете использовать способ F(" ..." ), это помещает фиксированные строки во флэш-память (конечно, для изменения строк это невозможно)., @Michel Keijzers

Да, объяснение хорошее., @goddland_16


4

Правда здесь оказывается немного сложнее, чем в других ответах. Существует несколько реализаций библиотеки Arduino String, и некоторые из них лучше других. Какой из них вы получите, зависит от того, на какой плате вы развертываете его.

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

Сравните исходную версию библиотеки строк Arduino, одна для AVR, до версия для ESP8266.
(На момент написания этого ответа это был этот снимок и этот снимок соответственно.)

Посмотрев, в частности, на такие функции, как changeBuffer, вы можете увидеть, что версия ESP содержит что-то под названием "SSO" ("оптимизация малых строк"), добавленное с помощью это пиар здесь. По сути, для небольших строк — достаточно маленьких, чтобы поместиться в структуру, которые в противном случае использовались бы для отслеживания строкового буфера в куче — вместо этого он просто сохранит строку в этом пространстве (в самом объекте String).

На практике это означает, что строки длиной менее 12 символов не требуют выделения памяти; они хранятся в стеке внутри объекта String.

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

,