Efficiency and Robustness - JawaharT/Best-Practices-On-Azure-Sphere GitHub Wiki
This section is devised to show how you can better optimise your code for the efficiency and robustness of hardware in mind. This will also enable you to take better advantage of C.
The stack is a specific area of the RAM where a program stores temporary data from the execution of code blocks. Data is statically allocated through the "Last in, First out" doctrine. The types of data stored include return addresses, function arguments, and local variables. An overflow situation will arise if the stack size is not large enough to support the code executed (overflow is discussed in a later section). The heap is for dynamically generated memory that is also part of the RAM. The data stored here is typically for information that is required by multiple parts of the program (transient data objects).
Heap:
Advantages | Disadvantages |
---|---|
Allows access to variable globally | Can complicate memory management |
No limit on memory size | In general, has a slower execution time than the stack |
More flexible than the stack | Manually release memory leading to memory leaks |
Stack:
Advantages | Disadvantages |
---|---|
Last in first out doctrine not possible for other data structures (arrays, linkedlists) | Too many objects can risk stack overflow |
Automatically destroyed local variable once returned | Random access not possible |
Unresizable arrays | Variable storage is overwritten, can lead to undefined behaviour inside a program |
Important to understand this concept as it forms the basics of C language. A pointer is a variable that stores a memory address. References on the other hand refer to existing variables by another name, sharing the same memory address as the existing variable but also using its original stack space. A reference is a valid pointer whereas a pointer does not have to be valid. Referencing is not possible in C, but it is useful to understand the concept. De-referencing refers to the access of the value for a memory address that the pointer stores using the * operator. The main purpose of using a pointer is to save memory space (a compact memory footprint), it is also the best way to access memory that has not been allocated yet, and in return, they can increase execution time (compared to the equivalent of not using pointers), when high performance is also a requirement, this is important to keep in mind especially for embedded systems.
But remember the more pointers that are used, the more complex the program becomes which will increase the difficulty of maintenance if the application becomes broken.
Example 1: Common Mistakes and how to use Pointers in arrays
These are the common mistakes beginners make when attempting to understand pointers and values:
// For demonstration purposes only
#include <stdio.h>
void main() {
int a, *ab;
// ab is an address but a is not
ab = a; // Error
// &a is an address but *ab is not
*ab = &a; // Error
}
The correct version:
// For demonstration purposes only
#include <stdio.h>
void main() {
int a, *ab;
// Both &a and ab are addresses
ab = &a;
// Both a and *ab values
*ab = a;
int b = 3;
int *ba = &b;
// Print 3 to console because: int *ba = &b; is equivalent to int* ba; ba = &b
// Essentially creates &b and assigns it to pointer ba
printf("%d", ba);
}
A beginner may write a program that loops through an array without the use of a pointer such as an example below:
#include <stdio.h>
void main(){
int general_array[5] = {0,1,2,3,4};
size_t size = sizeof(general_array)/sizeof(general_array[0]);
for(int element = 1; element < size; element++){
printf("%d", general_array[element]);
}
}
All embedded systems will be able to store this array in memory and execute the loop. But the amount of memory used for the array will depend on the type of application and the memory available. In addition, we can use pointers to access array elements because array variables during compilation are converted to pointers in most cases. There are two main cases whereby this may not occur:
-
Array is used as an argument to the sizeof operator
-
Array is used as an argument to the address of operator (&)
But it is important to remember that pointers and arrays are not the same.
The ideal way to access the contents of an array using a pointer:
#include <stdio.h>
void main() {
// Initalise an array
int general_array[5] = {0, 1, 2, 3, 4};
// general_pointer is assigned the address of the second element
int* general_pointer = &general_array[1];
printf("*general_pointer = %d \n", *general_pointer); // Prints 1
printf("*(general_pointer-1) = %d \n", *(general_pointer-1)); // Prints 0
printf("*(general_pointer+1) = %d", *(general_pointer+1)); // Prints 2
}
There may be a situation when you want to maintain an array, this is where the use of pointers is appropriate because as mentioned above they can help with compacting memory, specifically if general_array is used in multiple parts of the program it is best to refer to it as a pointer rather than creating a copy of the array each time of use.
Beginners are weary of common run-time errors such as segmentation violations like ‘segmentation fault’, these occur when the program attempts to access memory that is not allowed to use. If this occurs during the development of your program, you can use debuggers inside Visual Studio or Visual Studio Code to backtrack and find the memory issue. This can be through placing breakpoints in suspected areas in conjunction with the use of the diagnostic tool (explained below) to specifically track Azure Sphere memory and watch for any sudden memory use spikes to narrow down the location of the problem. This is because they can cause complete crashes if not fixed appropriately.
The heap is a specific part of the memory like the stack but used for dynamic storage that is managed by malloc and free functions in C. On the other hand, if variables or data structures are initialised automatically without malloc, it means they are saved inside the stack.
Example 2: De-referencing value of a pointer after it has been freed from heap
Non-ideal way:
#include <stdio.h>
#include <stdlib.h>
void main(void){
// Not meant to work
int *number = (int*)malloc(sizeof(int));
*number = 99;
free(number);
*number = 100; // Will cause undefined behaviour
}
Ideal Way:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int* number = (int*)malloc(sizeof(int)); // Can be performed for other data types too
// Check if malloc has succeeded
if (number != NULL) {
// Succeeded, assign value and continue program as normal
*number = 99;
}else {
// Failed, exit program and log the issue
return 0;
}
// Free allocated memory
if (number != NULL) {
free(number);
number = NULL;
}
return 0;
}
In general, it is always common to define all memory (malloc) and hardware used by the device upfront. This is to avoid leaks and reduce the time for memory allocation. But depending on your application you may also need dynamic memory. Malloc is required if you need to allocate objects that exist beyond the lifetime of the current execution block. As well as a free the number variable before the system shut down or if it should never be used again, and as good practice should be set to NULL if it is ever used again an access violation exception crash will occur and can be easily fixed during development. Both programs look and run very similarly but the unideal way reflects how easy it is to manage memory poorly.
The built-in malloc function is used to allocate variable space inside the heap, if the number returns a NULL pointer, sets errno to EN0MEM, and crashes the application if malloc fails. Then it is up to the developer to handle the next steps. If successful on the other hand, the value is assigned and the program can continue as normal. The freeing of allocated memory inside the heap is performed similarly to checking if the initialisation was successful through the use of an if statement. In addition, it also acts as a check if the heap becomes corrupted.
The advantage of malloc is that you can manually allocate as much available heap memory as possible but this is not possible in the stack as there is a fixed size limit. However, a disadvantage of malloc is the prospect of memory leaks, if you fail to free the memory from malloc at the end of its use, typically during application or system exit.
This is a quick demonstration of the example above that can be run on an Azure Sphere. First open the AzureSphereHeapManagementExample inside Visual Studio, any Azure Sphere board can be used for this example. Insert a breakpoint at line 5 that contains the malloc number declaration. Enter F5 or Click on 'Debug' menu and 'Start Debugging' to run the code inside Azure Sphere. It will take a few seconds to build and deploy the application. If you are unable to deploy, please use the commands below inside a Windows Powershell with a connected Azure Sphere board to remove existing applications present on the board before continuing:
azsphere device sideload delete
azsphere device restart
Once begun you should see the Diagnostics Tool sidebar appear, outlining the current memory usage of the Azure Sphere board, Device Output and Autos consoles are also shown below:
Once you click F10 or 'Step Over' is clicked, you can see the pointer number will hold the value 99 (seen inside the Autos console), this means the malloc has been successful, shown below:
Once you 'Step Over' again, the memory can be seen to be successfully freed with an arbitrary value, shown below:
This example is designed to show that Visual Studio IDE provides the diagnostics tool to help view locals/autos and the memory used inside an Azure Sphere. This debugging tool will help you step through your program to find any potential memory issues that occur as well as highlight the importance of memory as it becomes very difficult to manage for embedded applications.
Optimisation does not impact code quality because concise code can still produce errors. Code quality is considered to be both meeting requirements of a project as well as free from defects. The best way to start optimising involves writing a solution that works first then it can be re-written smaller with a more memory-efficient algorithm (optimisation). This involves performing more operations with the minimum amount of memory usage required. The compiler in Azure Sphere contains optimisations flags. Once turned on they make the compiler attempt to improve performance at the compile-time expense. The optimisation is completed by the compiler's knowledge of the program.
In most cases, it is important to minimise work inside loops by using as many constants as possible. The best function to optimise for speed is to factorise the loop counter and store it as a temporary constant. This can dramatically speed up calculations inside loops as well as making sure RAM usage is minimal. But since the example provided is small, the compiler will probably optimise the code anyway.
Example 3: Minimise loop calculations
Non-ideal way:
#include <stdio.h>
void main(void){
const int size = 250;
int arr[size];
// Only used once so not very elegant
int a = 1; b = 2; c = 99;
for(int counter = 0; counter < size; ++counter){
arr[counter] = ((c % b) + a) * counter;
printf("%d\n", arr[counter]);
}
}
Ideal Way:
#include <stdio.h>
void main(void){
const int size = 100;
const float multiple = (99 % 2) + 1;
int arr[size];
for(int counter = 0; counter < size; ++counter){
arr[counter] = (multiple) * counter;
printf("%d\n", arr[counter]);
}
}
Interrupts is a hardware mechanism to communicate to the CPU that attention for an external I/O peripheral is required through an interrupt signal. Polling is a software operation that reads the state of a connected system and acts on it, this could be software or hardware. For example, waiting for an external button to be pressed for a specific duration of time.
Desktop Application Interrupts are tasks handled by low-level parts of the operating system. They then are queued before being called by the OS before analysed by the application. Interrupts wait for any hardware response, unlike polling. However, the development of high-level Azure Sphere applications needs to poll for GPIO states but real-time applications can use GPIO interrupts.
Example 4: Example Timer for polling buttons on Azure Sphere AVNET MT3620 Board
// Demonstration purposes only
// Poll time set in milliseconds
struct timespec buttonPressCheckPeriod = {0, 1000000};
// Creating the poll
buttonPollTimer = CreateTimerFdAndAddToEpoll(epollFd, &buttonPressCheckPeriod, &buttonEventData, EPOLLIN);
// Exitcode to terminate program if there is a problem with poll timer
if (buttonPollTimer == NULL) {
return ExitCode_Init_ButtonPollTimer;
}
This example is not intended to run directly.
Here you have seen how to take advantage of pointers, manage the heap inside your SOC, handle speed versus size, and poll buttons through real examples which show how to handle small systems efficiently.
In the exercises coming up, printf is used to quickly display the outputs to the console but can be translated to embedded systems depending on the hardware used. The logic in the programs and the idea behind the best practices are more important to understand as part of these exercises.