EEVblog® Electronics Community Forum
Electronics => Microcontrollers => Topic started by: Kittu20 on November 06, 2023, 03:03:46 pm
-
Hello everyone,
I am trying to understand when can inline function may be useful Take example of my code for reference I wrote previously. I am calling all function in main function
#include <xc.h>
#include <stdio.h>
#include"config.h"
void initializeOscillator(void);
void initializeEUSART1(void);
/*****
* NOTE: This function configures the oscillator settings for the microcontroller.
* It sets the IRCF bits to 110 for HF-INTOSC/2 (8 MHz, default).
*****/
void initializeOscillator(void)
{
OSCCON = 0x60; // Set IRCF bits to 110 for HF-INTOSC/2 (8 MHz, default)
OSCCON2 = 0x00;
OSCTUNE = 0x00;
REFOCON = 0x00;
}
void initializePort() {
// Set LATx registers to initial states (all low)
LATA = 0x00;
LATB = 0x00;
LATC = 0x00;
LATD = 0x00;
LATE = 0x00;
// Set TRISx registers to configure pins as outputs (all output)
TRISA = 0x00;
TRISB = 0x00; // RB0 is RED, RB1 is YELLOW and RB2 is GREEN LED
TRISC = 0x80; // RC7 is TX and RC6 TX
TRISD = 0x00;
TRISE = 0x00;
// Additional configuration for unused peripherals
ANCON0 = 0x00; // Set all analog pins to digital mode
ANCON1 = 0x00; // Set all analog pins to digital mode
CM1CON = 0x00; // Turn off Comparator 1
CM2CON = 0x00; // Turn off Comparator 2
ADCON0 = 0x00; // Disable A/D conversion
ADCON1 = 0x00; // Disable A/D conversion
ADCON2 = 0x00; // Disable A/D conversion
}
/*****
*
* NOTE: This function configures the EUSART1 settings for communication.
*
*****/
void initializeUART1() {
// Configure BAUDCON1 register
// Bit 7: ABDOVF = 0 (Auto-Baud Acquisition Rollover Status bit - Cleared in software)
// Bit 6: RCIDL = 0 (Receive Operation Idle Status bit - Receive operation is active)
// Bit 5: RXDTP = 0 (Received Data Polarity Select bit - Receive data is not inverted)
// Bit 4: TXCKP = 0 (Clock and Data Polarity Select bit - Idle state for transmit is high level)
// Bit 3: BRG16 = 1 (16-Bit Baud Rate Register Enable bit - 16-bit Baud Rate Generator)
// Bit 2: Unimplemented: Read as ?0? (Reserved, read as '0')
// Bit 1: WUE = 0 (Wake-up Enable bit - RX pin is not monitored or rising edge is detected)
// Bit 0: ABDEN = 0 (Auto-Baud Detect Enable bit - Baud rate measurement is disabled or completed)
BAUDCON1 = 0b00001000; // Set BRG16 bit to 1 (16-Bit Baud Rate Register Enable)
// Calculate and set SPBRG1 for a baud rate of approximately 9600 at 8 MHz
SPBRG1 = 207;
SPBRGH1 = 0;
// TXSTA1 register configuration
// Bit 7: CSRC = 0 (Clock Source Select bit - Don't care in Asynchronous mode)
// Bit 6: TX9 = 0 (8-bit transmission)
// Bit 5: TXEN = 1 (Transmit is enabled)
// Bit 4: SYNC = 0 (Asynchronous mode)
// Bit 3: SENDB = 0
// Bit 2: BRGH = 1 (High-speed baud rate)
// Bit 1: TRMT = 1 (TSR is empty initially)
TXSTA1 = 0b00100100; // Configure TXSTA1 register
// RCSTA1 register configuration
// Bit 7: SPEN = 1 (Serial Port Enable bit - Serial port is enabled)
// Bit 6: RX9 = 0 (8-bit reception)
// Bit 5: SREN = 0 (Single Receive Enable bit - Don't care in Asynchronous mode)
// Bit 4: CREN = 1 (Continuous Receive Enable bit - Enables receiver)
// Bit 3: ADDEN = 0 (Address Detect Enable bit - Don't care in Asynchronous mode)
// Bit 2: FERR = 0 (Framing Error bit - No framing error)
// Bit 1: OERR = 0 (Overrun Error bit - No overrun error)
RCSTA1 = 0b10010000; // Configure RCSTA1 register
}
void hardwareInitialize() {
// Initialize the oscillator
initializeOscillator();
// Initialize Port Pins
initializePort();
// Initialize Uart1
initializeUART1();
}
/*****
*
* NOTE: This function waits until the transmitter buffer is empty (TXIF is set)
* and then sends the character by placing it in the TXREG1 register.
*
*****/
void UARTWrite(char Character)
{
// Wait until the transmitter buffer is empty (TXIF is set)
while (TXIF == 0);
// Send the character by placing it in the TXREG1 register
TXREG1 = Character;
}
/*****
*
* NOTE: This function waits until the receiver buffer is full (RCIF is set)
* and then reads the received character from the RCREG1 register.
*
*****/
char UARTRead(void)
{
// Wait until the receiver buffer is full (RCIF is set)
while (RCIF == 0);
// Read the received character from the RCREG1 register
return RCREG1;
}
/*****
*
* main - Main program function
*
*****/
void main(void)
{
// Initialize hardware
hardwareInitialize();
// Delay for 5 seconds
__delay_ms(5000);
while (1)
{
// Define a character to send
char Character = 'U';
char ReceivedChar;
ReceivedChar = UARTRead(); // Read a command from UART
UARTWrite(ReceivedChar); // Send the command back
// Delay for 5 seconds before the next transmission
__delay_ms(5000);
}
}
-
Hello,
Normally function inlining will give you an advantage when you have a short function that you call often - it can increase code efficiency and the speed of code of execution if you do it correctly. The potential problem that I see with inlining any of the functions in the code example that you provided is that you dont call them often enough to see the benefit of improved execution speed and efficiency and also you are working with a UART - you wont see any benefit of inlining UART fuctions either. In fact in this case at least you could (potentially at least) get more code bloat rather than more efficient code :-//
Ilya
-
I am not an embedded guru but here is what I think. Inline functions are like macros. It is worth using them in applications when critical parts of the code have to be executed quickly without additional calls or e.g. accesses to external memory.
One big benefit of inline functions is that the compiler checks for types. If you pass incorrect arguments to macros, you get whatever. Imagine you have a macro that returns smaller of two variables MIN(a,b). You won't get any error if a is signed and b is unsigned. If you create an inline function for the same, you get warning during compilation (in case input types don't match the function declaration).
Another way to think about inline functions is that they allow for tradeoffs between execution time and program memory footprint.
-
Inlining is an optimization, so it makes your code execute faster. In order for the compiler to inline you generally need the function to be defined in the same translation unit as the call site (unless you enable link time optimization). The compiler decides based on optimization level and various heuristics.
The inline keyword should be used for functions defined in header files to avoid violating the one definition rule. It is neither required to get inlining nor does it mandate that the compiler inline. It's purpose is that by putting the definition in a header you can have it visible for inlining in multiple translation units.
-
and also you are working with a UART - you wont see any benefit of inlining UART fuctions either. In fact in this case at least you could (potentially at least) get more code bloat rather than more efficient code :-//
For very simple functions such as those UART calls, the code to save and restore registers for a function call may well be more code than the function body, so inlining can actually reduce the code footprint. Most compilers would always inline such functions of they had the chance.
-
As usual, many wrong answers. C has no standard way of inlining functions, and inline keyword specifically, and unintuitively, does not do that. There are compiler-specific attributes like always_inline which may help in cases where you want to micromanage compiler's optimization choices.
But in reality this is secondary. More important, by far, is to make it a habit to always declare local functions static. C is a weird beast because by default, all functions are exposed to linker. A more sane language design would use a keyword like public or interface etc. to expose only interface functions to a module, usually a small subset of functions. In C, you need to do opposite, and remember to use static qualifier for all functions except those that are the interface to your module (compilation unit, .c file).
And it is colossally important for compiler to know that this function cannot be accessed from elsewhere, therefore enabling optimizations, including "inlining" the function: inserting the generated machine code at where the function would be called. Compiler does this automagically for a static function when it thinks it's a good idea; adding inline qualifier usually does not affect this decision.
-
For example:
WriteLED(value) {
LATA1= value;
}If this was a normal function, the call overhead would make it a lot slower.
Inlining inserts the instructions right there, so it's executed instantly, for example inserting ”LATA1=1" when calling WriteLED(1);
Inline functions are not really functions in the tradicional way (Call, return), the code will be inserted everywhere it's called, increasing program size, thus normally used only for very short functions.
-
and also you are working with a UART - you wont see any benefit of inlining UART fuctions either. In fact in this case at least you could (potentially at least) get more code bloat rather than more efficient code :-//
For very simple functions such as those UART calls, the code to save and restore registers for a function call may well be more code than the function body, so inlining can actually reduce the code footprint. Most compilers would always inline such functions of they had the chance.
That may well be true - I will admit that I personally didnt delve into it deep enough even for the 8, 16, and 32 MHz 8-bitters I normally work with since I normally considered that it wasnt a guarantee that you would get a 100% guarantee (in a context of applicability) that youd get a performance benefit for such functions (though I have a sneaking suspicion that my reasoning comes from the fact that I was using the gcc 5.x until relatively recently so didnt really bother to investigate that claim of mine - perhaps time to do a quick test on gcc-12.x and see whats what)
Ilya
-
I'm a bit confused about why it executes faster compared to the normal function. I've heard about the term 'function call overhead,' but I'm not sure what it exactly means. I've read somewhere that the compiler replace the function call, so I'm wondering, if the function call is replaced, how do we call the function?
-
When you call a function, you have to save the context, place the data somewhere for the function, jump to the function address, then the function must load the data, do its thing, restore the context and return.
Inline function avoid all this.
This overhead is negligible for a large function, but can make a very small function run 5-10x slower.
-
I'm a bit confused about why it executes faster compared to the normal function. I've heard about the term 'function call overhead,' but I'm not sure what it exactly means. I've read somewhere that the compiler replace the function call, so I'm wondering, if the function call is replaced, how do we call the function?
Imagine you renovate a house with many rooms. Let's say you do new flooring. Additionally, you have a workshop in the basement with a tool that you can't move (e.g. a tablesaw). In every single room, there are plenty of flooring tiles you need to cut. However, your tablesaw is in the basement. This means that with every piece you need to go to the basement to make the cut. This extra effort is the function call overhead. It is not efficient but you need just one saw.
A second alternative is having a table saw in every room. You need more tablesaws but you will be more efficient.
PS: Yes, I've done some renovations recently & could not come up with a better example. :palm:
-
Your compiler (XC8) may have different ideas about what the keyword 'inline' means. Its meaning for a compiler like gcc has little to do with actually inlining code (there is an attribute you can specify when you do want inlined code). It has specific purposes, and is probably best to leave it alone until you have a need to use it for a good purpose. Whether XC8 plays by these same rules, who knows.
You are at a level where you should concentrate on using 'static' when appropriate. If a function is only going to be used in a single source file, make the function static (same with variables). With that info, the compiler will optimize as appropriate since it knows no other source file will need to access the function/var. Let the compiler deal with optimization, but give it enough basic info like 'static' when appropriate so it can optimize better (it will choose- maybe it inlines the code or maybe it results in a call if used often, either way it will be optimal).
Your example code has functions that will not be needed in any other source file, so make them static. The compiler will figure out the best way to optimize. With the XC8 compiler and a pic8 instruction set, it is going to be difficult to start analyzing what is the 'best' way to get the most optimized code (and little need to do so) so just use standard C language best practices and let the compiler do its work.
-
When you call a function, you have to save the context, place the data somewhere for the function, jump to the function address, then the function must load the data, do it's thing, restore the context and return.
Inline function avoid all this.
In this scenario, I have a function that takes two integers as input, performs addition on them, and then returns the result. This function is typically defined separately. , I would call this function from main , passing the two integers i want to add as arguments. The function will execute the addition and return the result, which we can use or display as needed.
#include <stdio.h>
// Function that takes two integers, adds them, and returns the result
int add(int a, int b) {
return a + b;
}
int main() {
int num1 = 5;
int num2 = 7;
int result;
// Calling the 'add' function and storing the result
result = add(num1, num2);
printf("The sum of %d and %d is: %d\n", num1, num2, result);
return 0;
}
When we call a function in C, the values of the function's parameters (in this case, a and b) are typically pushed onto the function's call stack, and memory is allocated for these values in the stack frame of the function then , performing the addition of a and b, and storing the result in a temporary location within the stack frame. Once the function completes its execution, the result is typically stored in a another memory location. The memory allocated for the parameters in the stack is released, and the program continues its execution. This is process happens with normal function. What happens if I make this function as inline function
-
Inlining that will make "result = n1+n2", no calls no anything.
When enabling optimizations, the compiler will detect this doesn't need to be done, and simply do "result=12".
-
I'm a bit confused about why it executes faster compared to the normal function. I've heard about the term 'function call overhead,' but I'm not sure what it exactly means. I've read somewhere that the compiler replace the function call, so I'm wondering, if the function call is replaced, how do we call the function?
Two aspects to this. First is just the overhead of saving context, jumping to a new location, and restoring context when done. Inlining can avoid most or all of that.
The second is that it allows all of the conventional compiler optimizations: constant propagation, constant subexpression elimination, loop unrolling, and so on to work across procedural boundaries. A free function has to be implemented to work for any possible caller. An inlined function can take advantage of context specific constraints for optimization.
-
That may well be true - I will admit that I personally didnt delve into it deep enough even for the 8, 16, and 32 MHz 8-bitters I normally work with since I normally considered that it wasnt a guarantee that you would get a 100% guarantee
Any remotely recent gcc (like; less than 20 years old) will inline whatever it feels like as long as that compiler optimization is enabled regardless of whether you label it with inline. The inline keyword is basically to be used for functions defined in header files. It doesn't force inlining.
So it's not guaranteed either way, well formed programs aren't supposed to care beyond performance and memory use.
-
When calling any function, an internal convention on the position of passed parameters is triggered. Function parameters can be passed through the stack, through registers, through external memory - there are many variants. However, the general rules are equivalent for all functions.
The agreement on passing function parameters - in some cases may generate additional code. Since data always has its own address, moving data to the position of a function parameter is a prerequisite.
So, the built-in function does not require moving the data to the position of the function parameter. This is the main advantage of the built-in function. The simple code of the built-in function has a secondary meaning.
-
The inline keyword is basically to be used for functions defined in header files. It doesn't force inlining.
Indeed. A good rule of thumb: many reserved words in C like static, extern or inline have nothing to do with their names whatsoever, which is highly confusing (especially the re-use of static in totally different meanings). It would be actually better if they were called apple, banana and orange instead because you wouldn't have to then unlearn the obvious meanings just to re-learn what they mean in C. But this is what we pay for using an ancient language, and one can get used to it.
-
I appreciate your insights. I was quite wrong but learnt something new.
-
If you dig deeper, modern compilers are quite clever. They build the code in the best way by themselves. It is very difficult to make the compiler produce assembly code the way you want, and it is almost impossible.
The "inline" directive does not have absolute power either, it is closer to a hint for the compiler, which it can easily ignore.
"__attribute__((noinline))" - absolute noinline.
"__attribute__((always_inline)))" - is stronger than just "inline", but is powerless in some cases.
Using "inline" reduces the inadequate number of "mov" instructions. However, if the function has no parameters, it makes no sense.
-
However, if the function has no parameters, it makes no sense.
Even parameterless function has overhead - you have to jump and return. Granted, the overhead is even smaller. But in cases where the function does 1 instruction, it is more efficient both performance and size-wise to just copy-paste ("inline") that instruction where it's needed, instead of jumping to a function.
-
Using "inline" reduces the inadequate number of "mov" instructions. However, if the function has no parameters, it makes no sense.
That's not actually true. Even if a function has no parameters the caller must save any registers that the called function is allowed to clobber, whether it does so or not. It probably has to update the stack pointer. Plus, there is always a jump and return.
Of course all of these are small overheads: calling a funciton is fast. But they are not zero. Inlining is especially important in C++ where it's more common to have many layers of wrapper functions and lots of "trivial" accessor functions. Those are considered to be effectively zero runtime overhead because it is assumed that (when compiled with optimization) the compiler will inline the whole stack of them and then optimize down to exactly the few instructions you would have had without the function calls.
-
inline-
instead of either a declaration or definition, you are giving the toolhain a definition (and declaration) that will not trigger a multiple definition error, and allows the options of either 'calling' the function (this function must be callable, and exist somewhere else if ends up being called) or producing a 'copy' inline (it has all the information it needs to do either, as opposed to a definition where the function body is unknown)
inline int somefunc(int a){ return a+1; }
so this usage is somewhat rare as it requires the function to actually exist somewhere else in case the toolchain decides to 'call' the function, which will depend on how complex the function is and the optimization settings (most usage/misuse like this will work inadvertently, only because the toolchain will most likely produce an inline copy and rarely make a 'call')
a more common use is in combination with 'static' (typically in a header, but can be useful in a source file also)
static inline int somefunc(int a){ return a+1; }
where you are using the 'inline' keyword only to eliminate the toolchain complaining about unused function(s) if the function is unused
a function that lives in a header would typically be 'static inline', where the 'static' makes it local to the translation unit and also allows for it being unused without the toolchain complaining ('tricking' the toolchain into thinking it may be used in another translation unit)- the 'static' is all the toolchain needs to fully optimize its use(s)
summary-
inline
there is an external function by this name, either call it or produce the code inline (toolchain's choice) since the definition is known
(since the option to call the external function exists, no guarantee you get the inlined version)
static inline
useful for headers, where 'static' does the work of getting the compiler to fully optimize its use(s) and the 'inline' just prevents unused function complaints
The 'static' keyword is the more useful and more appropriate keyword in most cases. The 'inline' keyword only gives the option for 'static-like', but no guarantee.
If using C++, there are other things to say- like implied inline when a class definition lives in a header, inline variables (c++17), etc.
As I said previously, use C best practices such as 'static' and 'const' and leave the 'inline' keyword alone until actually required. Outside these best practices, I would not spend a lot of time worrying about whether functions are called/inlined as there are plenty of other more important things to spend time on.
-
a more common use is in combination with 'static' (typically in a header, but can be useful in a source file also)
static inline int somefunc(int a){ return a+1; }
where you are using the 'inline' keyword only to eliminate the toolchain complaining about unused function(s) if the function is unused
Yes, that's pretty much the only use of 'inline' that serves a purpose, and if you frequently use functions rather than macros for "small tasks", this is something that will come handy.
Back in pretty old days, some compilers would actually use it as a hint to inline (and would never inline functions otherwise), just like the 'register' keyword - that was of course not a guarantee, but a hint and compilers would follow the hint if the context allowed. We're talking like compilers from 30 years ago and older.
-
That may well be true - I will admit that I personally didnt delve into it deep enough even for the 8, 16, and 32 MHz 8-bitters I normally work with since I normally considered that it wasnt a guarantee that you would get a 100% guarantee
Any remotely recent gcc (like; less than 20 years old) will inline whatever it feels like as long as that compiler optimization is enabled regardless of whether you label it with inline. The inline keyword is basically to be used for functions defined in header files. It doesn't force inlining.
So it's not guaranteed either way, well formed programs aren't supposed to care beyond performance and memory use.
Actually you may have reminded me that I may have already done a quick experiment on that and found that as you quite rightly point out it actually made no actual difference if I inlined functions or not and inlining was pretty much in the hands of the compiler so to speak. So for what I was doing at the time, quite often there wasnt really much of a guarantee of benefit to have my code inlined :-DD
-
As a side note, as was discussed recently in another thread, while GCC optimizations (especially starting with -O2, and very aggressively with -O3) will inline a lot of functions automatically, -Os tends to do the exact opposite, which can lead to pretty odd stuff: it tries to factor pieces of code that are repeated into functions and call them.
In both cases, the end result may not be favorable in terms of actual performance - the only way to tell is to test, and have a look at the generated assembly code. If it matters, of course. Otherwise just let the compiler do its stuff and move on.
-
Actually you may have reminded me that I may have already done a quick experiment on that and found that as you quite rightly point out it actually made no actual difference if I inlined functions or not and inlining was pretty much in the hands of the compiler so to speak. So for what I was doing at the time, quite often there wasnt really much of a guarantee of benefit to have my code inlined :-DD
For micromanaging inlining (which is sometimes useful in embedded, if you don't want to go as far as writing assembly, but instead use the "write-C-look-at-assembly-output" or "write-C-measure-with-oscillosscope" strategies), in GCC, just use:
#define ALWAYS_INLINE static inline __attribute__((always_inline))
ALWAYS_INLINE void set_pin() { ... }
I also use this pattern when I refactor something out of function and possibly pass large objects by value (for the code to be most readable), then I feel confident it always works without significant performance penalty. I guess it's still not 100% guaranteed but it has never failed me so far. Note though that if you forcefully inline large functions that are called more than once it will waste significant amount of code memory.
-
I have a practical example on function inlining in a program, in an effort to help Kittu20 and others to understand what inlining actually is and is not.
Let's say you are doing some work that calculates the values of univariate polynomials of form \$f(x) = C_0 + C_1 x + C_2 x^2 + \dots + C_{N-2} x^{N-2} + C_{N-1} x^{N-1}\$, and you don't want to hardcode the values of \$C_k\$ into your C program, and instead read them from a file. Then, you can use a function like
#include <stdlib.h>
double poly(const double x,
const size_t n,
const double c[n])
{
if (n < 1)
return 0.0; // Or NaN
double result = c[0];
double factor = 1.0;
for (size_t i = 1; i < n; i++) {
result += factor * c[i];
factor *= x;
}
return result;
}
to calculate the value for any given polynomial, given their coefficients c[] and the value of x to evaluate it at. For example, to calculate \$f(x) = x^3 + 25 x - 4\$, you'd use n = 3 and double c[3] = { -4.0, 25.0, 1.0 } (always \$C_0\$ first, \$C_{N-1}\$ last).
(In C, you can provide the length of an array, if you just do it before the array itself. You can even use multi-dimensional arrays: (const size_t n, const size_t k, const double m[n][k]) is perfectly valid. For some reason, it seems rarely used. I like to promote it every now and then, because if more programmers were to use it, then the C compilers would more likely grow facilities to report array overruns, giving us programmers a very powerful tool to avoid buffer overrun bugs, the bane of bad C code. The array notation is completely compatible with the pointer notation, and you can supply a pointer to double as the third parameter to poly() without any problems, because in C, arrays always decay to a pointer to their initial element. The size_t type, and not int, is also the proper type for sizes.)
(Also, while modern C compilers can infer the const-ness automagically from the code itself, and removing/adding them does not actually cause any change in the machine code generated by current C compilers, they are important –– for us humans. They indicate the author intent, as a promise that "no attempt to modify the values of these variables will be made in this code" (which the compiler will check, and warn or error if violated). Simply put, because x, n, and the contents of the c array are marked const, we humans know their value should stay the same within each invocation of the function, and therefore don't need to spend any mental effort to track them. I use them, because it makes it much easier to maintain code long term for me: instead of looking at the code for a minute, I recognize the function parameter const-ness, and can parse what the function body does within a few seconds. Other experienced C programmers may be thrown off a bit by the variably-modified array argument, but as I wrote above, I think they too should use it more, instead of the more common pointer notation.)
Let's say, however, that your actual task is to calculate the sum of the values \$f(x)\$ for a set or array of \$x\$ instead. You come up with a function using a very similar interface:
double poly_sum(const size_t nx,
const double x[nx],
const size_t nc,
const double c[nc])
{
double result = 0.0;
if (nx < 1 || nc < 1)
return 0.0; // Or NaN
for (size_t i = 0; i < nx; i++)
result += poly(x[i], nc, c);
return result;
}Let's assume we write a few tests to verify the code works, and that it indeed does.
Now, in the latter function implementation as machine code, does the C compiler generate a call to the previously defined poly() function, or does it inline the body of that function?
The only correct answer is that the compiler gets to choose. It may generate a call, or copy the machine code implementing the poly() function. Even when using the exact same compiler and same optimization settings, it may generate different cases on different hardware architectures. And chaning the compiler settings can of course change that too.
Let's say we realize we don't actually need the poly() function outside the current compilation unit (this C file at hand), and we mark it static or static inline –– the two are exactly equivalent here ––, i.e. "static double poly(const double x,...". What changes?
Without static, the compiler must still provide object code for the poly() function even in the case it chooses to inline its implementation within the poly_sum() function. The static keyword tells the compiler we only use it locally, no external linkage is needed, so it can drop the externally-visible implementation. The compiler is still free to choose whether to generate separate code for the poly() function, or to inline it within the still externally-visible poly_sum() function.
All architectures you can compile C code to, have their own function call application binary interface, ABI, or "calling conventions". For example, on 32-bit Windows, when you mark your functions __fastcall, it means they will get their first parameter in the ECX register, and the second in the EDX register. What we call 'function call overhead' therefore usually includes quite a bit of shuffling data from/to registers and stack, since the way functions take parameters and return value(s) is fixed. When the compiler inlines a function implementation, it not only gets rid of the calling instruction, but also all that shuffling of values to/from registers and stack. If the core functionality of the called function is just a few machine instructions, or a short loop like here (on hardware with built-in double precision floating-point math support), the version of the outer poly_sum() function can end up shorter with the other function inlined, because many data-shuffling instructions can be avoided. Or not; depending on the exact hardware and ABI, which is why it is a good thing we can leave it up to the compiler to decide on a case-by-case basis.
If we were to look at the generated machine code when the C compiler decides to inline the code (at say Compiler Explorer (https://godbolt.org/z/oWrWdrzGf)), we'd also find that when inlined, the compiler also omitted the superfluous check of whether n is zero at the beginning of poly(), because the call site, poly_sum() already checked that earlier, and the compiler knows it will never trigger and can therefore omit the code for checking it altogether. Yes, many expert programmers will tell you that that check in poly() is "unneeded"; I am telling you that because of the auto-inlining facilities of modern C compilers, it does not hurt either. You might hide it behind a NDEBUG check or similar, but having that check there is useful, because again, it tells us humans what the author intended when they wrote the function: it is not supposed to return anything useful when the array has zero elements, because then the functions \$f(x)\$ is undefined: it has zero terms!
In fact, when the function is marked local-linkage only, static, even when not inlined, the C compiler can note that all call sites have already verified that n will be greater than zero, and even though choosing not to inline it, can generate code for poly() that excludes the explicit check. Nice!
However, if you forget the static, and think "hey, it's just an extra symbol in the linker which costs nothing, I'll worry about that later, for now it might come handy elsewhere", the compiler will NOT be allowed to drop that check, and therefore you'll get machine code that does unnecessary checking (and possibly other unnecessary work), just because you-the-programmer did not want to think about it just now.
One key in programming is being as descriptive as you can to the compiler. You do NOT leave things as broad as possible "for now" –– say, because you think you might want to use it from another file later on, and believe it might "save you time" by leaving it exported "for now" ––, and then think you'll restrict them later on as an optimization step, because later never comes. Instead, be as strict and precise and limited as you can be by default, and only relax those when necessary. This ensures the compiler has maximum amount of information available from the get go, and can do those optimization choices the best way possible. Even its warnings about stuff it thinks is contradictory are more useful then. This also means that marking function parameters const just by habit, while deemed "unnecessary" by "1337 c0d3rZ", is actually a Very Good Thing.
That leaves just the difference between static and static inline functions. For practical purposes, they are identical. Some compilers do warn about an unused static function but not about an unused static inline function, which is useful, but not required/suggested/based on the C standard. (Any tricks one might do with external/internal function visibility are better done using compiler extensions, especially via ELF "weak" symbol support.)
Personally, I use this to my advantage, by habitually using static for normal local functions, and static inline for accessor and helper functions (especially those defined in header files). The compiler does not care, but for us humans, it is another indicator of author intent. If you find you need to expand a static inline function, you should instead create a new function that calls the static inline function to provide the added functionality, because adding extra stuff to an accessor/helper function likely called elsewhere will add unnecessary overhead for those other calls. On the other hand, a normal local static function is not supposed to do meaningful work, so the consideration shifts to refactoring "is this way of splitting the functionality, into these local functions, the most sensible one?", and only if the answer is "yeah, this still makes sense", you go ahead and add the functionality to the existing function; otherwise you refactor the code, changing how you split the overall task into smaller sub-tasks, and which functions handle which ones.
All this means that whether the compiler does or does not inline a particular function is something you do not worry about! What you should worry about is whether you, the human writing the code, have given enough information for the compiler to make good optimization decisions like inlining.
Actually, I personally prefer to worry about whether I have given enough information for the compiler to avoid making horribly bad optimization decisions instead.
It is really not at all important for the compiler to always make the optimal choices, because they are already so good that the difference between "good" choice and "optimal" choice is minuscule. However, horribly bad optimization choices still exist, and those do bite us in the butt: you can lose orders of magnitude of performance by writing "stupid C" which does not allow the compiler to generate efficient machine code, because of the rules.
As programmers and developers, we should always concern ourselves most at the algorithm level –– whether our own approach is an efficient one, or whether some other one would work better ––, making sure we implement the code with sufficient specifity and precision, that the compiler can generate good code, or at least avoid generating horrible code.