2

I'm getting ready to add DS1307 support to my Arduino app, and was horrified when I looked at the sourcecode for the RTC_DS1307 class in Adafruit's RTClib library...

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));
}

... and realized that it appears to be instantiating a new throw-away DateTime object on the heap every time you call now()... which AFAIK, is the ultimate Arduino C++ taboo.

I was about to tear into the class and rewrite it to reuse a singleton DateTime object that gets created once and reused forever... but then I remembered about return value optimization.

Is this actually a scenario where RVO will automagically kick in to save the day and prevent a heap-allocation (by pre-allocating space for the DateTime object on the caller's stack prior to calling now()), or is this library indeed doing something as taboo as I think it is?

0

2 Answers 2

5

I wrote a minimal sketch that calls RTC_DS1307::now(), disassembled it, and here is what I saw:

  • The caller allocates 6 bytes on the stack for holding the return value.

  • It writes the address of this stack location in the register pair r25:r24, as per the AVR calling convention,1 then calls RTC_DS1307::now().

  • As the callee computes the fields of the resulting DateTime (by means of inlined calls to bcd2bin()), it writes them directly into the caller's stack frame, with no intermediate copy in RAM.

This looks to me like return value optimization.

However, in order to witness this behavior, I had to call RTC_DS1307::now() twice. If the method is called from a single place within the sketch, the compiler inlines it, and there isn't even a function call to begin with.

Then, of course, the heap is never touched.

1 You would expect the this pointer to be passed as the first argument, in r25:r24. It would seem it has been optimized out, which makes sense given that there is only one RTC_DS1307 object in the whole sketch.

6
  • Surely the heap is never touched in any case?
    – Nick Gammon
    Nov 7, 2023 at 10:37
  • with no intermediate copy in RAM - using RAM is not the same thing as using the heap.
    – Nick Gammon
    Nov 7, 2023 at 10:41
  • 1
    @NickGammon: The heap is not touched by RTC_DS1307::now(). On AVR, new calls malloc. Thus, if the heap is ever touched, avr-nm the_sketch.elf | grep malloc will show you: malloc, __malloc_heap_start, __malloc_heap_end and __malloc_margin. Nov 7, 2023 at 15:10
  • I wish I could give points for both answers, but I'm going with Edgar's due to the info about being able to determine with certainty whether the heap is being touched by something. I've been dying to find info about that particular topic for months.
    – Bitbang3r
    Nov 7, 2023 at 17:27
  • Edgar Bonet's answers are usually excellent. I think we are in agreement that there is no problem with heap fragmentation in this particular case.
    – Nick Gammon
    Nov 7, 2023 at 20:59
4

I think you are confusing the stack and the heap. For something to appear in the heap you would expect to see the words new or malloc which I don't see.

What is happening here is that a DateTime object is being created (in the stack frame of the current function) by RTC_DS1307::now which is being (presumably) copied into a variable of that type in the caller's stack frame when the function returns.

No memory is being wasted here.


it appears to literally be instantiating a new throw-away DateTime object on the heap

What gives you that idea? The heap is not involved without you using new or malloc.


The heap is a block of memory used for dynamic memory allocation by using malloc or new, generally speaking. Typically it will be allocated past the place in RAM where variables are allocated (ie. after all the variables) and grow upwards, whereas the stack will be allocated at the end of available memory and grow downwards. Thus they don't clash until they collide, either by using too much stack (eg. by recursive function calls) or by allocating too much of the heap (by repeatedly calling malloc and without calling free).

When you use the heap, by using malloc you will get a pointer which you can then use, and pass around. That memory which you have reserved is yours until you free it, by literally calling free. In C++ something very similar is done by using new and delete.

If you don't call malloc or use new then you aren't using the heap, end of story.

4
  • Prior to about an hour ago, I didn't realize there was a mechanism in c++ to create an object on a subroutine/method's stack & atomically copy it to the caller's stack upon return. I thought the called method/subroutine's stack vanished the instant return executed, so an object created in & returned by a method/subroutine had to either go in the heap or use RVO. Up to now, I always tiptoed around it by doing the 'Android' thing & passing my return-object containers TO methods & reusing them.
    – Bitbang3r
    Nov 7, 2023 at 6:05
  • @Bitbang3r Well, yes, you could always pass one by reference.
    – Nick Gammon
    Nov 7, 2023 at 6:48
  • @Bitbang3r See amended answer for more explanations about the heap.
    – Nick Gammon
    Nov 7, 2023 at 11:05
  • In most cases, thanks to return value optimization the returned object is constructed in the stack frame of the caller, avoiding the copying step.
    – jpa
    Nov 7, 2023 at 11:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.