-
Notifications
You must be signed in to change notification settings - Fork 7
Internals of memory use on S4A
(note: some of this is intended for core team and core contributors and references areas of our source repositories that might not be visible to you... feel free to ask for sample code on Slack and someone will help)
https://github.com/swiftforarduino/S4A/blob/main/Unit%20Tests/xUnitTests-ARC.swift these are the unit tests for ARC in the S4A IDE (note they're marked as disabled because the main branch is beta 6, which uses the embedded mode compiler and breaks a bunch of unit tests... but that's nothing to worry about). The important point is the use of __brkval, like... print(">>> heap start is... (__malloc_heap_start)") print(">>> brkval: (__brkval)") which allows you to print out where the top of the heap is. Swift, like C++, uses the platform's malloc() and free() functions from the platform's libc. They are pretty universal in the way they work, from AVR, to Darwin on iOS, to Windows machines, great big Unix iron boxes, IBM mainframes, etc. You request a block of n bytes and you either get it or you get back a null pointer if the heap is full. And in most platforms, heap grows upwards in memory, stack grows downwards from the "top" of RAM. Details differ of course, but most unixes have __brkval which indicates the top of the heap, so you can print that out and see how much heap you have used. And running out of memory means "if you request a buffer of n bytes, that would take __brkval inside the stack". There are of course lots of strategies internally on each platform, in most cases, there's at the very least an extra forced gap added, to allow the stack room to grow even after your heap is "full". On AVR that's only 16 bytes by default, but you can make it larger if you wish.
A couple of quick notes on __brkval on AVR if you start using it to understand your heap size. It has slightly unintuitive behaviour at first. Before the first time you allocate memory (before you create the first class instance, array instance, allocate an unsafe mutable buffer pointer, etc.) __brkval equals zero. That just indicates it hasn't been configured, it's lazy loaded by malloc for efficiency (some programs never allocate heap memory and never need it). After your first allocation it will "jump up" to the true value. If you allocate n bytes, this will be + n, because the heap starts after the end of the global variables section in RAM. You'll know how big that is because S4A reports it after every build like "Size of global memory is 43 bytes." ... so if your first allocation is an unsafe buffer of 12 bytes and your globals are 43 bytes then after that allocation __brkval should be 55 bytes. But also, it's reported as a memory address in the AVR memory space. On AVR, RAM actually starts at 0x0100 (you can look this up on the atmega328p data sheet). So in the case we are talking about, __brkval would jump from 0x0000 to 0x0137 (the hexadecimal value). That does not mean "I've suddenly lost 256 bytes of RAM somewhere", it is just how addresses work on AVR. Your RAM on an atmega328p, for example, runs from address 0x0100 to 0x08FF. It's confusing at first when you see these addresses, until you "get your eye in". 8:13
Final note on __brkval: to be able to read __brkval, you'll need to add some stuff to your C bridging header on the project you're using, a bit like we do in the unit tests: https://github.com/swiftforarduino/S4A/blob/main/Unit%20Tests/utShims/utShims.h. As a bare minimum, just add extern uint16_t __brkval; to your clang import header should be enough.
Some more thoughts... It's worth a bit understanding how we handle out of (heap) memory and how it differs from "regular" Swift. And in particular the problem with Arrays. Because of its "safe by design" ethos and the platforms it was written to run on, Swift handles out of memory (OOM) extremely aggressively. The minute malloc returns "I can't give you that much memory" the Swift runtime instantly crashes your program on the spot, no mercy!! That means that the standard library and runtime designers were able to make a lot of simplifying assumptions. Swift essentially is written on the assumption of "infinite virtual memory". When you create an Array or add values to one in regular Swift, the language assumes it works, because otherwise your program would have crashed. So there's never any possibility of "throwing an error" or anything like that when you run out of RAM... it's just not a concept in normal Swift. Because that's not sensible for us, around version 5.1 of our IDE, I modified UnsafeMutableBufferPointer to return nil if you run out of memory, which allows graceful handling. However, I was not able to do this with Arrays. The concept of "assumed memory creation" was just so deeply built into Array that I couldn't remove it. The best I could do, the compromises I made were: 1) Arrays are fixed size and cannot be extended, you must create them at the correct size, 2) when you use an initialiser like [UInt8](repeating: X, count: &size) the initialiser has been altered slightly to take the size as an inout parameter... if our runtime detects there is not enough memory to create the array you ask for, then it will change your size variable to 0. This means it's sensible to check that value after you create an array to see if you have a problem. I know it would be nice to throw an error here but I'm not sure it's possible... it might be worth investigating though?... 3) finally, there are some places where Array creation just gets "stuck in a corner" on our platform. In particular, when you create arrays from an array of constants like you're doing in your code sample (and similar situations). In those cases, our runtime does the best it can, and just returns an empty array! So, again, it's worth checking if this has happened to see if you have run out of RAM. There may be ways we can improve some of these interfaces.