Description
Description
- Type: Enhancement
- Priority: Minor
Enhancement
Reason to enhance or problem with existing solution
scanf/printf can be heavyweight (nanolib takes >2.5kb in flash) and hard to optimize even when only used once due to source->static lib->source traversal.
When you use a Stream subclass (like Serial), here's what happens:
Stream::Stream() calls fdopen->mbed_fdopen
In mbed_retarget.cpp, mbed_fdopen() calls sprintf just to do ":%p" % this, then calls std::fopen, which eventually leads to _open, which calls sscanf to rip the pointer out of ":%p".
Suggested enhancement
Option A: optimize these specific uses of printf/scanf out.
printf: A simpler (computationally) way to calculate a buf that will be checked by _open is:
// Around line 828
std::FILE *mbed_fdopen(FileHandle *fh, const char *mode) {
// char buf[12]; /* :0x12345678 + null byte */
// std::sprintf(buf, ":%p", fh);
char buf[2 + sizeof(fh) + 1];
static_assert(sizeof(buf) == 7, "Pointers aren't 4 bytes?");
buf[0] = ':';
memcpy(buf + 1, &fh, sizeof(fh));
buf[1 + sizeof(fh)] = '\0';
// Around line 178
extern "C" FILEHANDLE PREFIX(_open)(const char* name, int openmode) {
...
// Around line 232
if (name[0] == ':') {
void *p;
// std::sscanf(name, ":%p", &p);
memcpy(&p, name + 1, sizeof(p));
res = (FileHandle*)p;
This saves ~3.3kB from my program, though I still have these symbols in my Map and I can't trace them any further: svfprintf, vfprintf, fseek, setvbuf.
Option B: Allow Serial to not support file-like operations (via define?, or a SerialLite?)
To compare apples to apples, I first wrote the following code to ensure I was linking everything in:
mbed::Callback<void(int)> null_cb(nullptr);
char buf[80];
int written = std::sprintf(buf, "Some stuff: %d", 9);
serial.write((uint8_t*)buf, written, null_cb));
This uses the async write()
code since the synchronous write()
is implemented by Stream
. Then I removed Stream
as a base class of Serial
in both Serial.h and Serial.cpp. Compiling before and after that base class removal, I got a 5kB shrink in text (and 300 in bss because mbed_retarget's filehandles aren't linked in anymore).
Pros
Both options can be done, though B is more intrusive and more work as it seems the purpose of Stream was to capitalize on the stdlib's implementations of fprintf, fflush, etc. However, for more production-worthy systems, being able to use Serial ports without linking in string-manipulation libraries and/or file-manipulation libraries means being able to put more code in the same microcontroller or to cost-down and use one with less flash (and less ram, etc).
Option A:
- Save ~3kB in firmware not using scanf elsewhere
Option B:
- Save ~5kB in firmware not using file-like abilities on Serial.
- Since
Stream::printf
callsfprintf
synchronously, butSerialBase::write
is an async write, adding/movingStream::write
toSerialBase
or a newStreamLite
would giveSerial
a synchronous write that doesn't link in file handling.
To be clear, I haven't thought through Option B, partly because I don't know mbed's long-term vision for Serial
, Stream
, etc. I just know that ripping out file handling code brings my blinky+serial code down to ~15kB from ~23kB. While that's still really large for the functionality, it's mostly UART and RCC related plus 4 printf-related functions that I'll figure out some other way.
Cons
Option A:
- The filename string is less human-readable (not really a problem)
- Slightly more code to use memcpy over sscanf and sprintf.
Option B:
- This requires more thinking/decisions and a lot more work than what I've done here. If you're okay with more defines, I can make one to just rip out Stream as a subclass, but that seems hacky.
Conclusion
I'll send a PR for Option A to discuss the exact implementation since that seems reasonable. For Option B, I'd like to hear your thoughts and see what path you would prefer.