Использование Adafruit RTClib без фрагментации кучи

c++

Я готовлюсь добавить поддержку DS1307 в свое приложение Arduino и был в ужасе, когда посмотрел на исходный код класса RTC_DS1307 в библиотеке RTClib от Adafruit...

DateTime RTC_DS1307::now() {
  uint8_t buffer[7];
  buffer[0] = 0;
  i2c_dev->write_then_read(buffer, 1, buffer, 7);

  return DateTime(bcd2bin(buffer[6]) + 2000U, bcd2bin(buffer[5]),
                  bcd2bin(buffer[4]), bcd2bin(buffer[2]), bcd2bin(buffer[1]),
                  bcd2bin(buffer[0] & 0x7F));
}

... и понял, что каждый раз, когда вы вызываете метод now(), создается экземпляр нового выбрасываемого объекта DateTime в куче... что, AFAIK, является абсолютным табу Arduino C++. .

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

На самом ли деле это сценарий, в котором RVO автоматически сработает, чтобы спасти ситуацию и предотвратить выделение кучи (путем предварительного выделения места для объекта DateTime в стеке вызывающего объекта перед вызовом now()), или эта библиотека действительно делает что-то табуированное, как я думаю?

, 👍2


2 ответа


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

5

Я написал минимальный скетч, который вызывает RTC_DS1307::now(), дизассемблировал это, и вот что я увидел:

  • Вызывающий объект выделяет в стеке 6 байт для хранения возврата. значение.

  • Он записывает адрес этого местоположения стека в пару регистров. r25:r24, согласно соглашению о вызовах AVR, 1 затем вызывает RTC_DS1307::now().

  • Пока вызываемый объект вычисляет поля результирующего DateTime (по средствами встроенных вызовов bcd2bin()), он записывает их непосредственно в кадр стека вызывающего абонента без промежуточной копии в оперативной памяти.

На мой взгляд, это похоже на оптимизацию возвращаемого значения.

Однако, чтобы стать свидетелем такого поведения, мне пришлось позвонить RTC_DS1307::now() дважды. Если метод вызывается из одного места внутри скетча компилятор встраивает его, и нет даже вызов функции для начала.

Тогда, конечно, куча никогда не трогается.

1 Вы ожидаете, что указатель this будет передан как первый аргумент в r25:r24. Казалось бы, его оптимизировали, что имеет смысл, учитывая, что в нем есть только один объект RTC_DS1307. весь скетч.

,

Неужели кучу никогда не трогают в любом случае?, @Nick Gammon

без промежуточной копии в ОЗУ — использование ОЗУ — это не то же самое, что использование кучи., @Nick Gammon

@NickGammon: Куча не затрагивается RTC_DS1307::now(). В AVR new вызывает malloc. Таким образом, если куча когда-либо будет затронута, avr-nm the_sketch.elf | grep malloc покажет вам: malloc, __malloc_heap_start, __malloc_heap_end и __malloc_margin., @Edgar Bonet

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

Ответы Эдгара Боне обычно превосходны. Думаю, мы согласны с тем, что в данном конкретном случае проблем с фрагментацией кучи нет., @Nick Gammon

Интересно, как можно предположить некоторое распределение кучи из кода, представленного в вопросе. Я думаю, возврат объекта по копии не интуитивно понятен. Обычно компилятор даже оптимизирует его, чтобы не было копирования., @Juraj


4

Мне кажется, вы путаете стек и кучу. Чтобы что-то появилось в куче, вы ожидаете увидеть слова new или malloc, которых я не вижу.

Здесь происходит то, что объект DateTime создается (в кадре стека текущей функции) с помощью RTC_DS1307::now, который (предположительно) копируется в переменную этого типа в кадре стека вызывающей стороны, когда функция возвращает значение.

Здесь не тратится память.


похоже, что буквально создается новый одноразовый объект DateTime в куче

Что натолкнуло вас на эту идею? Куча не задействована без использования new или malloc.


Куча — это блок памяти, используемый для динамического выделения памяти с помощью malloc или new, вообще говоря. Обычно он выделяется за местом в ОЗУ, где выделены переменные (т. е. после всех переменных), и растет вверх, тогда как стек выделяется в конце доступной памяти и растет вниз. Таким образом, они не конфликтуют до тех пор, пока не столкнутся, либо из-за использования слишком большого стека (например, из-за рекурсивных вызовов функций), либо из-за выделения слишком большого объема кучи (путем многократного вызова malloc и без вызова ). бесплатно).

Когда вы используете кучу, используя malloc, вы получаете указатель, который затем можно использовать и передавать. Эта память, которую вы зарезервировали, принадлежит вам, пока вы не освободите ее, буквально вызвав free. В C++ нечто очень похожее делается с помощью new и delete.

Если вы не вызываете malloc или не используете new, то вы не используете кучу, конец истории.

,

Примерно час назад я не осознавал, что в C++ существует механизм создания объекта в стеке подпрограммы/метода и атомарного копирования его в стек вызывающего объекта по возвращении. Я думал, что стек вызванного метода/подпрограммы исчез при мгновенном выполнении return, поэтому объект, созданный и возвращенный методом/подпрограммой, *должен* был либо попасть в кучу, либо использовать RVO. До сих пор я всегда обходил это на цыпочках, выполняя «Android», передавая свои контейнеры возвращаемых объектов методам и повторно используя их., @Bitbang3r

@ Bitbang3r Ну да, вы всегда можете передать его по ссылке., @Nick Gammon

@ Bitbang3r См. измененный ответ для получения дополнительных объяснений о куче., @Nick Gammon

В большинстве случаев благодаря [оптимизации возвращаемого значения](https://en.wikipedia.org/wiki/Copy_elision#Return_value_optimization) возвращаемый объект создается во фрейме стека вызывающего объекта, избегая этапа копирования., @jpa