Использование Adafruit RTClib без фрагментации кучи
Я готовлюсь добавить поддержку 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()), или эта библиотека действительно делает что-то табуированное, как я думаю?
@Bitbang3r, 👍2
2 ответа
Лучший ответ:
Я написал минимальный скетч, который вызывает RTC_DS1307::now()
, дизассемблировал
это, и вот что я увидел:
Вызывающий объект выделяет в стеке 6 байт для хранения возврата. значение.
Он записывает адрес этого местоположения стека в пару регистров. r25:r24, согласно соглашению о вызовах AVR, 1 затем вызывает
RTC_DS1307::now()
.Пока вызываемый объект вычисляет поля результирующего
DateTime
(по средствами встроенных вызововbcd2bin()
), он записывает их непосредственно в кадр стека вызывающего абонента без промежуточной копии в оперативной памяти.
На мой взгляд, это похоже на оптимизацию возвращаемого значения.
Однако, чтобы стать свидетелем такого поведения, мне пришлось позвонить
RTC_DS1307::now()
дважды. Если метод вызывается из одного места
внутри скетча компилятор встраивает его, и нет даже
вызов функции для начала.
Тогда, конечно, куча никогда не трогается.
1 Вы ожидаете, что указатель this
будет передан как
первый аргумент в r25:r24. Казалось бы, его оптимизировали,
что имеет смысл, учитывая, что в нем есть только один объект RTC_DS1307
.
весь скетч.
Мне кажется, вы путаете стек и кучу. Чтобы что-то появилось в куче, вы ожидаете увидеть слова 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
- C++ против языка Arduino?
- Как использовать SPI на Arduino?
- Какие накладные расходы и другие соображения существуют при использовании структуры по сравнению с классом?
- Ошибка: expected unqualified-id before 'if'
- Что лучше использовать: #define или const int для констант?
- Функции со строковыми параметрами
- Библиотека DHT.h не импортируется
- ошибка: ожидаемое первичное выражение перед токеном ','
Неужели кучу никогда не трогают в любом случае?, @Nick Gammon
без промежуточной копии в ОЗУ
— использование ОЗУ — это не то же самое, что использование кучи., @Nick Gammon@NickGammon: Куча не затрагивается
RTC_DS1307::now()
. В AVRnew
вызываетmalloc
. Таким образом, если куча когда-либо будет затронута,avr-nm the_sketch.elf | grep malloc
покажет вам:malloc
,__malloc_heap_start
,__malloc_heap_end
и__malloc_margin
., @Edgar BonetМне хотелось бы дать баллы за оба ответа, но я выбираю ответ Эдгара, поскольку в нем есть информация о возможности с уверенностью определить, касается ли куча чего-либо. Я очень хотел найти информацию по этой конкретной теме в течение нескольких месяцев., @Bitbang3r
Ответы Эдгара Боне обычно превосходны. Думаю, мы согласны с тем, что в данном конкретном случае проблем с фрагментацией кучи нет., @Nick Gammon
Интересно, как можно предположить некоторое распределение кучи из кода, представленного в вопросе. Я думаю, возврат объекта по копии не интуитивно понятен. Обычно компилятор даже оптимизирует его, чтобы не было копирования., @Juraj