Duplicated Functions in the MPLAB® XC8 Compiler

Last modified by Microchip on 2023/11/09 09:13

Handling Non-reentrant Functions

The latest versions of the MPLAB® XC8 compiler can now issue message #1510, a warning that informs you:

non-reentrant function “_foo” appears in multiple call graphs and has been duplicated by the compiler

The duplication performed by the compiler that triggers this message is nothing new - the older compiler versions also performed this action - it’s just that the compiler now warns you that this duplication has taken place. Given that this new message makes function duplication more noticeable, it is worth taking a look at exactly what function duplication is, why it is done, and what you can do to avoid it.

A function is said to be duplicated when the compiler generates two distinct instances of output code for that function. Each instance is allocated separate program memory and each uses its own copy of the function's local variables. It is only the function's assembly encoding and variables that are duplicated; you will not see any change in your project source code.

To understand the reason why functions need to be duplicated, you need to know how variables are allocated memory in a data stack and the consequences of this allocation.

The MPLAB XC8 compiler uses a compiled data stack when the default stack model is selected. With this type of stack, local, stack-based variables (auto and parameter variables, as well as compiler-allocated temporary storage) are allocated a fixed address in data memory, much like the allocation of global objects. Since direct memory-access instructions can be used to read and write objects allocated in this way, this model can be used for all 8-bit PIC devices and yields very efficient code. The one problem this type of allocation presents is that functions that define local objects are not reentrant.

A reentrant function can have multiple instances of itself active at the same time. The common usage of reentrant functions is with recursion, where a function calls itself or calls a function that will ultimately lead to itself being called. So if the function foo() calls the function bar() and bar() calls foo(), then foo() (and bar() for that matter) need to be reentrant for this code to execute correctly.

For a function to be reentrant, each instance of itself must use its own copy of any local variables. However, since a compiled stack allocates local objects at a fixed address, the local variables of one instance of a function would be at the same address as the local variables used by another, so there would be a corruption of the function's local variables.

A function must also be reentrant if it is called from separate call graphs in a program. A call graph is a tree of function calls that constitute one sequential part of a program, much like a thread. Main-line code constitutes one call graph, and the code executed for an interrupt, another. If foo(), for example, has been written so that it does not use recursion, you can safely use this function anywhere in your program. But if foo() is called from main-line code and also from interrupt code, this could result in both instances being active at the same time, specifically, if foo() was executing in main-line code when an interrupt was triggered, then execution of that instance of foo() would be suspended and a second instance would be executed by the interrupt, corrupting the variables of the original instance.

If your project is set up to use the compiled stack, recursion is not supported. If you attempt to recursively call a function you will get the following error message:

recursive function call to "_foo"

However, calling the same function from main-line and interrupt code is something that you might not be able to avoid, especially considering that the operation of library functions are equally affected.

If a function is called from more than one call graph, the compiler will automatically duplicate the output of the function, adjusting the generated code so that one instance is called from main-line code, and the other is called by the interrupt. Each instance of the function will appear to be a unique function, having its own local variables allocated to unique addresses. The function instances are still not reentrant, but since the compiler will carefully control how each instance is called, this is no longer a problem. It is during this duplication process that the compiler issues the warning we saw at the beginning of this article.

Duplication of functions called from multiple call graphs ensures that the program will execute correctly, and it saves you from having to manually duplicate the source code associated with a function and maintain both copies of that code. However, this situation is not ideal - which is why the compiler issues the warning. Duplication of functions will increase the program and data memory of your project, and remember that any functions called by a duplicated function (i.e., all the functions in the duplicated function's call graph) must also be duplicated since they will also need to be reentrant.

If you are seeing a warning that indicates that you have duplicated functions in your project, here are some steps you can consider:

The first is to simply not call the function from an interrupt routine. Try to structure your program so that the function is only called from main-line code. Have your interrupt code set a flag that is checked by main-line code and that signifies that the function needs to be called. This technique will circumvent the need for duplication and suppress the warning. It might also make your interrupt code more efficient.

If your program cannot be restructured and a function must be called from multiple call graphs, you can swap to using a hybrid stack model if this is supported by your target device. (If you are using the MPLAB X IDE, make this selection from the XC8 Global options category, Stack options > Stack type; or using —STACK=hybrid on the command line.) When this model is selected, any function that is called from multiple call graphs will not be duplicated and will instead use a software stack rather than a compiled stack. A software stack is like a traditional data stack, using a stack pointer to access objects and push- and pop-like instructions to dynamically load and unload the stack, and functions that use such a stack are reentrant. Accessing objects in a software stack is not as efficient as accessing those in a compiled stack, and only some 8-bit PIC devices have the instruction set that can implement this stack.

If, however, you cannot restructure your program and you are using a device that does not support the software stack, don't worry, your code will still execute correctly. The duplication of the affected functions will ensure that there is no corruption of data.