Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically allocate the SD sector cache on use, save ~512 bytes when SD not used #4204

Closed

Conversation

earlephilhower
Copy link
Collaborator

@earlephilhower earlephilhower commented Jan 20, 2018

Related to issue #3740

The SD library includes a 512-byte cache array as a static class member
to hold a copy of a sector on the SD card. Because it is statically
allocated, the mere act of including <SD.h> in either your own application
or any library you use--even if you didn't ever use any of the SD
objects--would cause the linker to allocate a 512 byte array in BSS out
of heap.

Replace this static allocation with a static pointer, and only allocate
that pointer on the instantiation of a SdVolume object. Free it when
the SdVolume is destroyed.

The simple test example below shows the issue:
cat > sdcardtest.ino <<EOF
#include <SD.h>

void setup() {
Serial.begin(115200);
Serial.println("Hello, world!");
}
void loop() {
}
EOF

Examining the memory map shows the large buffer that's allocated w/o anyone
ever actually allocating a SD object.
earle@/tmp$ readelf -a /tmp/arduino_build_705221/*.elf | grep cacheBuff
614: 3ffee9b8 512 OBJECT GLOBAL DEFAULT 3 _ZN8SdVolume12cacheBuffer

The SD library includes a 512-byte cache array as a static class member
to hold a copy of a sector on the SD card. Because it was statically
allocated, the mere act of including <SD.h> in either your own application
or *any library* you use--even if you didn't ever use any of the SD
objects--would cause the linker to allocate a 512 byte array in BSS out
of heap.

Replace this static allocation with a static pointer, and only allocate
that pointer on the instantiation of a SdVolume object.  Free it when
the SdVolume is destroyed.
@@ -431,14 +431,16 @@ union cache_t {
class SdVolume {
public:
/** Create an instance of SdVolume */
SdVolume(void) :allocSearchStart_(2), fatType_(0) {}
SdVolume(void) :allocSearchStart_(2), fatType_(0) { if (!cacheBuffer_) cacheBuffer_ = (cache_t *)malloc(sizeof(cache_t)); }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null is not checked. I don't really know myself what to do in constructors when this happen.
Maybe you can also help with this in #4134 :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe errors in constructors can only throw an exception. Unfortunately, that's not the Arduino way, so I'm as at a loss as yourself.

The alternative here, and elsewhere, is to use lazy allocation and/or defensive. Before use of the cache it can be checked and the method can error out if it can't be allocated, assuming the API allows it. Perusing the SD headers, it looks like SEGV'ing is really the only thing that's possible using the current API.

Something like link time optimization (-flto) might eliminate the need for this kind of change as it should be able to detect no call graph into this static class variable, but I've not been able to make it work (and I believe we're upgrading GCC soon, and at that point it should be "automatic" anyway).

platform.txt Outdated
@@ -86,7 +86,7 @@ recipe.S.o.pattern="{compiler.path}{compiler.c.cmd}" {compiler.cpreprocessor.fla
recipe.ar.pattern="{compiler.path}{compiler.ar.cmd}" {compiler.ar.flags} {compiler.ar.extra_flags} "{build.path}/arduino.ar" "{object_file}"

## Combine gc-sections, archives, and objects
recipe.c.combine.pattern="{compiler.path}{compiler.c.elf.cmd}" {compiler.c.elf.flags} {compiler.c.elf.extra_flags} -o "{build.path}/{build.project_name}.elf" -Wl,--start-group {object_files} "{build.path}/arduino.ar" {compiler.c.elf.libs} -Wl,--end-group "-L{build.path}"
recipe.c.combine.pattern="{compiler.path}{compiler.c.elf.cmd}" -Wl,-M -Wl,-Map "-Wl,{build.path}/{build.project_name}.map" {compiler.c.elf.flags} {compiler.c.elf.extra_flags} -o "{build.path}/{build.project_name}.elf" -Wl,--start-group {object_files} "{build.path}/arduino.ar" {compiler.c.elf.libs} -Wl,--end-group "-L{build.path}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#4186 :)

@earlephilhower
Copy link
Collaborator Author

@d-a-v That's what I get for doing two things at once. Just removed that change, sorry!

SdVolume(void) :allocSearchStart_(2), fatType_(0) {}
SdVolume(void) :allocSearchStart_(2), fatType_(0) { if (!cacheBuffer_) cacheBuffer_ = (cache_t *)malloc(sizeof(cache_t)); }
/** Delete an instance of SdVolume */
~SdVolume() { free(cacheBuffer_); cacheBuffer_ = NULL; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original cache was a static array, accessible from any instance. This change makes it a static pointer, and the constructor checks if it has been allocated before, in which case nothing is done, or not, in which case it is allocated.
This destructor just blindly frees the mem. If there is more than one instance, the others will then try to access after free.
I think it's unlikely that there will be more than one instance of this class, but I think this is something that should be fixed to cover e.g.: temps (pass by value) and copy.
I suggest use of a shared_ptr.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had only considered the single global SD object. Since then, with the BSSL stuff I've been introduced to the wonders of the std::shared_ptr and this would be a good use of one like you suggest. Will see if I can fix it up this weekend.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, but using a separate refcnt instead of std::ptr as there needs to be a special-case for cnt==1 (like the BearSSL issue).

The cache now is only freed when there are no references to it (i.e. if
for some reason multile SD objects were used).

Files in SD/src/utility converted to UNIX format (matching other libs)
@earlephilhower
Copy link
Collaborator Author

There's been some large changes to SD since this was submitted, so drop it for the near term.

@earlephilhower earlephilhower deleted the dynamic_sd_cache branch November 18, 2020 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants