13
\$\begingroup\$

I have seen some of the libraries for microcontrollers and and their functions do one thing at a time. For example, something like this:

void setCLK()
{
    // Code to set the clock
}

void setConfig()
{
    // Code to set the config
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
}

Then come other functions on top of it that uses this 1 line code containing a function to serve other purposes. For example:

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

I am not sure, but I believe this way it would be creating more call to jumps and creating overhead of stacking the return addresses every time time a function is called or exited. And that would make the program work slow, right?

I have searched and everywhere they say that the thumb rule of programming is that a function should perform only one task.

So if I write directly an InitModule function module that sets the clock, adds some desired configuration and does something else without calling functions. Is it a bad approach when writing embedded software?


EDIT 2:

  1. It seems like lot of people have understood this question as if I am trying to optimize a program. No, I have no intention to do. I am letting the compiler do it, because it is going to be always (I hope not though!) better than me.

  2. All the blames on me for choosing an example that represents some initialization code. The question has no intention of regarding function calls made for the initialization purpose. My question is Does breaking a certain task into small functions of multi-line (so in-line is out of question) running inside a infinite loop has any advantage over writing long function without any nested function?

Please consider readability defined in the answer of @Jonk.

\$\endgroup\$
8
  • 28
    \$\begingroup\$ You are very naive (not meant as an insult) if you believe that any reasonable compiler will blindly turn code as written into binaries as written. Most modern compilers are quite good at identifying when a routine is better inlined and even when a register vs RAM location should be used to hold a variable. Follow the two rules of optimization: 1) don't optimize. 2) don't optimize yet. Make your code readable and maintainable, and THEN only after profiling a working system, look to optimize. \$\endgroup\$
    – akohlsmith
    Jul 16, 2018 at 20:36
  • 10
    \$\begingroup\$ @akohlsmith IIRC the Three rules of optimization are: 1) Don't! 2) No really Don't! 3) Profile first, then and only then optimize if you must -- Michael_A._Jackson \$\endgroup\$
    – esoterik
    Jul 17, 2018 at 1:06
  • 3
    \$\begingroup\$ Just remember that "premature optimization is the root of all evil (or at least most of it) in programming" - Knuth \$\endgroup\$ Jul 17, 2018 at 13:07
  • 1
    \$\begingroup\$ @Mawg: The operative word there is premature. (As the next paragraph in that paper explains. Literally the next sentence: "Yet we should not pass up our opportunities in that critical 3%.") Don't optimize til you need to -- you won't have found the slow bits til you have something to profile -- but also don't engage in pessimization, for example by using blatantly wrong tools for the job. \$\endgroup\$
    – cHao
    Jul 17, 2018 at 21:58
  • 1
    \$\begingroup\$ @Mawg I don't know why I got answers / feedback related to optimization, since I never mentioned the word and I intend to do it. The question is much more about how to write functions in Embedded Programming to achieve better performance. \$\endgroup\$
    – MaNyYaCk
    Jul 18, 2018 at 7:26

9 Answers 9

29
\$\begingroup\$

Arguably, in your example the performance would not matter, as the code is only run once at startup.

A rule of thumb I use: Write your code as readable as possible and only start optimizing if you notice that your compiler isn't properly doing its magic.

The cost of a function call in an ISR might be the same as that of a function call during startup in terms of storage and timing. However, the timing requirements during that ISR might be a lot more critical.

Furthermore, as already noticed by others, the cost( and meaning of the 'cost') of a function call differs by platform, compiler, compiler optimization setting, and the requirements of the application. There will be a huge difference between an 8051 and a cortex-m7, and a pacemaker and a light switch.

\$\endgroup\$
1
  • 6
    \$\begingroup\$ IMO the second paragraph should be in bold and at the top. There's nothing wrong with choosing the right algorithms and data structures right off the bat, but worrying about function call overhead unless you've discovered that it's an actual bottleneck is definitely premature optimization, and should be avoided. \$\endgroup\$
    – Nic
    Jul 16, 2018 at 23:24
11
\$\begingroup\$

There is no advantage I can think of (but see note to JasonS at the bottom), wrapping up one line of code as a function or subroutine. Except perhaps that you can name the function something "readable." But you can just as well comment the line. And since wrapping up a line of code in a function costs code memory, stack space, and execution time it seems to me that it is mostly counter-productive. In a teaching situation? It might make some sense. But that depends on the class of students, their preparation beforehand, the curriculum, and the teacher. Mostly, I think it's not a good idea. But that's my opinion.

Which brings us to the bottom line. Your broad question area has been, for decades, a matter of some debate and remains to this day a matter of some debate. So, at least as I read your question, it seems to me to be an opinion-based question (as you asked it.)

It could be moved away from being as opinion-based as it is, if you were to be more detailed about the situation and carefully described the objectives you held as primary. The better you define your measurement tools, the more objective the answers may be.


Broadly speaking, you want to do the following for any coding. (For below, I'll assume that we are comparing different approaches all of which achieve the goals. Obviously, any code that fails to perform the needed tasks is worse than code that succeeds, regardless of how it is written.)

  1. Be consistent about your approach, so that another reading your code can develop an understanding of how you approach your coding process. Being inconsistent is probably the worst possible crime. It not only makes it difficult for others, but it makes it difficult for yourself coming back to the code years later.
  2. To the degree possible, try and arrange things so that initialization of various functional sections can be performed without regard to ordering. Where ordering is required, if it is due to close coupling of two highly related subfunctions, then consider a single initialization for both so that it can be reordered without causing harm. If that isn't possible, then document the initialization ordering requirement.
  3. Encapsulate knowledge in exactly one place, if possible. Constants should not be duplicated all over the place in the code. Equations that solve for some variable should exist in one and only one place. And so on. If you find yourself copying and pasting some set of lines that perform some needed behavior in a variety of locations, consider a way to capture that knowledge in one place and use it where needed. For example, if you have a tree structure that must be walked in a specific way, do not replicate the tree-walking code at each and every place where you need to loop through the tree nodes. Instead, capture the tree-walking method in one place and use it. This way, if the tree changes and the walking method changes, you have only one place to worry about and all the rest of the code "just works right."
  4. If you spread out all of your routines onto a huge, flat sheet of paper, with arrows connecting them as they are called by other routines, you will see in any application there will be "clusters" of routines that have lots and lots of arrows between themselves but only a few arrows outside the group. So there will be natural boundaries of closely coupled routines and loosely coupled connections between other groups of closely coupled routines. Use this fact to organize your code into modules. This will limit the apparent complexity of your code, substantially.

The above is just generally true about all coding. I didn't discuss the use of parameters, local or static global variables, etc. The reason is that for embedded programming the application space often places extreme and very significant new constraints and it's impossible to discuss all of them without discussing every embedded application. And that's not happening here, anyway.

These constraints may be any (and more) of these:

  • Severe cost limitations requiring extremely primitive MCUs with miniscule RAM and almost no I/O pin-count. For these, whole new sets of rules apply. For example, you may have to write in assembly code because there isn't much code space. You may have to use ONLY static variables because the use of local variables is too costly and time consuming. You may have to avoid the excessive use of subroutines because (for example, some Microchip PIC parts) there are only 4 hardware registers in which to store subroutine return addresses. So you may have to dramatically "flatten" your code. Etc.
  • Severe power limitations requiring carefully crafted code to start up and shut down most of the MCU and placing severe limitations on the execution time of code when running at full speed. Again, this might require some assembly coding, at times.
  • Severe timing requirements. For example, there are times where I've had to make sure that the transmission of a open-drain 0 had to take EXACTLY the same number of cycles as the transmission of a 1. And that sampling this same line also had to be performed with an exact relative phase to this timing. This meant that C could NOT be used here. The ONLY possible way to make that guarantee is to carefully craft assembly code. (And even then, not always on all ALU designs.)

And so on. (Wiring code for life-critical medical instrumentation has a whole world of its own, as well.)

The upshot here is that embedded coding often isn't some free-for-all, where you can code like you might on a workstation. There are often severe, competitive reasons for a wide variety of very difficult constraints. And these may strongly argue against the more traditional and stock answers.


Regarding readability, I find that code is readable if it is written in a consistent fashion that I can learn as I read it. And where there isn't a deliberate attempt to obfuscate the code. There really isn't much more required.

Readable code can be quite efficient and it can meet all of the above requirements I've already mentioned. The main thing is that you fully understand what each line of code you write produces at the assembly or machine level, as you code it. C++ places a serious burden on the programmer here because there are many situations where identical snippets of C++ code actually generate different snippets of machine code that have vastly different performances. But C, generally, is mostly a "what you see is what you get" language. So it's safer in that regard.


EDIT per JasonS:

I've been using C since 1978 and C++ since about 1987 and I've had a lot of experience using both for both mainframes, minicomputers, and (mostly) embedded applications.

Jason brings up a comment about using 'inline' as a modifier. (In my perspective, this is a relatively "new" capability because it simply didn't exist for perhaps half of my life or more using C and C++.) The use of inline functions can actually make such calls (even for one line of code) quite practical. And it's far better, where possible, than using a macro because of the typing that the compiler can apply.

But there are limitations, as well. The first is that you cannot rely on the compiler to "take the hint." It may, or may not. And there are good reasons not to take the hint. (For an obvious example, if the address of the function is taken, this requires the instantiation of the function and the use of the address to make the call will ... require a call. The code cannot be inlined then.) There are other reasons, as well. Compilers may have a wide variety of criteria by which they judge how to handle the hint. And as a programmer, this means you must spend some time learning about that aspect of the compiler or else you are likely to make decisions based upon flawed ideas. So it adds a burden both to the writer of the code and also any reader and also anyone planning to port the code to some other compiler, as well.

Also, C and C++ compilers support separate compilation. This means that they can compile one piece of C or C++ code without compiling any other related code for the project. In order to inline code, assuming the compiler otherwise might choose to do so, it not only must have the declaration "in scope" but it must also have the definition, as well. Usually, programmers will work to ensure that this is the case if they are using 'inline'. But it is easy for mistakes to creep in.

In general, while I also use inline where I think it is appropriate, I tend to assume that I cannot rely on it. If performance is a significant requirement, and I think the OP has already clearly written that there has been a significant performance hit when they went to a more "functional" route, then I certainly would choose to avoid relying upon inline as a coding practice and would instead follow a slightly different, but entirely consistent pattern of writing code.

A final note about 'inline' and definitions being "in scope" for a separate compilation step. It is possible (not always reliable) for the work to be performed at the linking stage. This can occur if and only if a C/C++ compiler buries enough detail into the object files to allow a linker to act on 'inline' requests. I personally haven't experienced a linker system (outside of Microsoft's) that supports this capability. But it can occur. Again, whether or not it should be relied upon will depend on the circumstances. But I usually assume this hasn't been shoveled onto the linker, unless I know otherwise based on good evidence. And if I do rely on it, it will be documented in a prominent place.


C++

For those interested, here's an example of why I remain fairly cautious of C++ when coding embedded applications, despite its ready availability today. I'll toss out some terms that I think all embedded C++ programmers need to know cold:

  • partial template specialization
  • vtables
  • virtual base object
  • activation frame
  • activation frame unwind
  • use of smart pointers in constructors, and why
  • return value optimization

That's just a short list. If you don't already know everything about those terms and why I listed them (and many more I didn't list here) then I'd advise against the use of C++ for embedded work, unless it is not an option for the project.

Let's take a quick look at C++ exception semantics to get just a flavor.

A C++ compiler must generate correct code for compilation unit \$A\$ when it has absolutely no idea what kind of exception handling may be required in separate compilation unit \$B\$, compiled separately and at a different time.

Take this sequence of code, found as part of some function in some compilation unit \$A\$:

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

For discussion purposes, compilation unit \$A\$ doesn't use 'try..catch' anywhere in its source. Neither does it use 'throw'. In fact, let's say that it doesn't use any source that couldn't be compiled by a C compiler, except for the fact that it uses C++ library support and can handle objects like String. This code might even be a C source code file that was modified slightly to take advantage of a few C++ features, such as the String class.

Also, assume that foo() is an external procedure located in compilation unit \$B\$ and that the compiler has a declaration for it, but does not know its definition.

The C++ compiler sees the first call to foo() and can just allow a normal activation frame unwind to occur, if foo() throws an exception. In other words, the C++ compiler knows that no extra code is needed at this point to support the frame unwind process involved in exception handling.

But once String s has been created, the C++ compiler knows that it must be properly destroyed before a frame unwind can be allowed, if an exception occurs later on. So the second call to foo() is semantically different from the first. If the 2nd call to foo() throws an exception (which it may or may not do), the compiler must have placed code designed to handle the destruction of String s before letting the usual frame unwind occur. This is different than the code required for the first call to foo().

(It is possible to add additional decorations in C++ to help limit this problem. But the fact is, programmers using C++ simply must be far more aware of the implications of each line of code they write.)

Unlike C's malloc, C++'s new uses exceptions to signal when it cannot perform raw memory allocation. So will 'dynamic_cast'. (See Stroustrup's 3rd ed., The C++ Programming Language, pages 384 and 385 for the standard exceptions in C++.) Compilers may allow this behavior to be disabled. But in general you will incur some overhead due to properly formed exception handling prologues and epilogues in the generated code, even when the exceptions actually do not take place and even when the function being compiled doesn't actually have any exception handling blocks. (Stroustrup has publicly lamented this.)

Without partial template specialization (not all C++ compilers support it), the use of templates can spell disaster for embedded programming. Without it, code bloom is a serious risk which could kill a small-memory embedded project in a flash.

When a C++ function returns an object an unnamed compiler temporary is created and destroyed. Some C++ compilers can provide efficient code if an object constructor is used in the return statement, instead of a local object, reducing the construction and destruction needs by one object. But not every compiler does this and many C++ programmers aren't even aware of this "return value optimization."

Providing an object constructor with a single parameter type may permit the C++ compiler to find a conversion path between two types in completely unexpected ways to the programmer. This kind of "smart" behavior isn't part of C.

A catch clause specifying a base type will "slice" a thrown derived object, because the thrown object is copied using the catch clause's "static type" and not the object's "dynamic type." A not uncommon source of exception misery (when you feel you can even afford exceptions in your embedded code.)

C++ compilers can automatically generate constructors, destructors, copy constructors, and assignment operators for you, with unintended results. It takes time to gain facility with the details of this.

Passing arrays of derived objects to a function accepting arrays of base objects, rarely generate compiler warnings but almost always yields incorrect behavior.

Since C++ doesn't invoke the destructor of partially constructed objects when an exception occurs in the object constructor, handling exceptions in constructors usually mandates "smart pointers" in order to guarantee that constructed fragments in the constructor are properly destroyed if an exception does occur there. (See Stroustrup, page 367 and 368.) This is a common issue in writing good classes in C++, but of course avoided in C since C doesn't have the semantics of construction and destruction built in. Writing proper code to handle the construction of subobjects within an object means writing code that must cope with this unique semantic issue in C++; in other words "writing around" C++ semantic behaviors.

C++ may copy objects passed to object parameters. For example, in the following fragments, the call "rA(x);" may cause the C++ compiler to invoke a constructor for the parameter p, in order to then call the copy constructor to transfer object x to parameter p, then another constructor for the return object (an unnamed temporary) of function rA, which of course is copied from parameter p. Worse, if class A has its own objects which need construction, this can telescope disasterously. (A C programmer would avoid most of this garbage, hand optimizing since C programmers don't have such handy syntax and have to express all the details one at a time.)

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

Finally, a short note for C programmers. longjmp() doesn't have a portable behavior in C++. (Some C programmers use this as a kind of "exception" mechanism.) Some C++ compilers will actually attempt to set things up to clean up when the longjmp is taken, but that behavior isn't portable in C++. If the compiler does clean up constructed objects, it's non-portable. If the compiler doesn't clean them up, then the objects aren't destructed if the code leaves the scope of the constructed objects as a result of the longjmp and the behavior is invalid. (If use of longjmp in foo() doesn't leave a scope, then the behavior may be fine.) This isn't too often used by C embedded programmers but they should make themselves aware of these issues before using them.

\$\endgroup\$
19
  • 4
    \$\begingroup\$ This kind of functions used only once are never compiled as function call, the code is simply placed there without any call. \$\endgroup\$
    – Dorian
    Jul 16, 2018 at 7:32
  • 6
    \$\begingroup\$ @Dorian - your comment might be true under certain circumstances for certain compilers. If the function is static within the file then the compiler has the option to make the code inline. if it is externally visible then, even if it is never actually called, there has to be a way for the function to be callable. \$\endgroup\$
    – uɐɪ
    Jul 16, 2018 at 7:45
  • 1
    \$\begingroup\$ @jonk - One other trick that you have not mentioned in a good answer is to write simple macro functions that perform the initialisation or configuration as expanded inline code. This is especially useful on the very small processors where RAM/stack/function call depth is limited. \$\endgroup\$
    – uɐɪ
    Jul 16, 2018 at 7:49
  • \$\begingroup\$ @ʎəʞouɐɪ Yes, I missed discussing macros in C. Those are deprecated in C++, but a discussion on that point might be useful. I may address it, if I can figure something useful to write about it. \$\endgroup\$
    – jonk
    Jul 16, 2018 at 7:53
  • 1
    \$\begingroup\$ @jonk -- I completely disagree with your first sentence. An example like inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); } which is called in numerous places is a perfect candidate. \$\endgroup\$
    – Jason S
    Jul 16, 2018 at 19:54
8
\$\begingroup\$

1) Code for readability and maintainability first. The most important aspect of any codebase is that it is well-structured. Nicely written software tends to have less errors. You may need to make changes in a couple of weeks/months/years, and it helps immensely if your code is nice to read. Or maybe someone else has to make a change.

2) Performance of code that runs once does not matter very much. Care for style, not for performance

3) Even code in tight loops needs to be correct first and foremost. If you face performance issues, then optimize once the code is correct.

4) If you need to optimize, you have to measure! It does not matter if you think or someone tells you that static inline is just a recommendation to the compiler. You have to take a look at what the compiler does. You also have to measure if inlining did improve performance. In embedded systems, you also have to measure code size, since code memory is usually pretty limited. This is THE most important rule that distinguishes engineering from guesswork. If you didn't measure it, it didn't help. Engineering is measuring. Science is writing it down ;)

\$\endgroup\$
2
  • 2
    \$\begingroup\$ The only criticism I have of your otherwise excellent post is point 2). It is true that performance of initialization code is irrelevant - but in an embedded environment, the size can matter. (But that doesn't override point 1; start optimizing for size when you need to - and not before) \$\endgroup\$ Jul 17, 2018 at 18:26
  • 2
    \$\begingroup\$ Performance of initialization code might at first be irrelevant. When you add low-power mode, and want to recover quickly to handle the wakeup event, then it becomes relevant. \$\endgroup\$ Jul 18, 2018 at 4:52
5
\$\begingroup\$

When a function is called only in one place (even inside other function) the compiler allways puts the code in that place instead of really calling the function. If the function is called in many places than it makes sense to use a function at least from the code size point of view.

After compiling the code will not have the multiple calls instead the readability will be greatly improved.

Also you will want to have for example the ADC init code in the same library with other ADC functions not in the main c file.

Many compilers allow you to specify different levels of optimization for speed or code size, so if you have a small function called in many places then the function will be "inlined" , copied there instead of calling.

The optimization for speed will inline functions in as many places it can, the optimization for code size will call the function, however , when a function is called only in one place as in your case it will always be "inlined".

Code like this:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

will compile to:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

without using any call.

And the answer to your question,in your example or similar, the readability of the code does not affect the performance, nothing is lots in speed or code size. It's common to use multiple calls just to make the code readable , at the end they're be complied as an inline code.

Update to specify that above statements are not valid for on purpose crippled free version compilers like Microchip XCxx free version. This kind of function calls is a gold mine for Microchip to show how much better is the paid version and if you compile this you will find in the ASM exactly as much calls as you have in the C code.

Also it's not for dumb programmers that expect to use pointer to an inlined function.

This is the electronics section, not general C C++ or programming section, the question is about microcontroller programming where any decent compiler will do the above optimisation by default.

So please stop downvoting only because in rare, unusual cases this might be not true.

\$\endgroup\$
15
  • 15
    \$\begingroup\$ Whether code becomes inline or not is a compiler vendor implementation specific issue; even using the inline keyword does not guarantee inline code. It is a hint to the compiler. Certainly good compilers will inline functions used just once if they know about them. It will not usually do so if there are any"volatile" objects in scope, though. \$\endgroup\$ Jul 16, 2018 at 8:08
  • 9
    \$\begingroup\$ This answer is just not true. As @PeterSmith says, and according to the C language specification, the compiler has the option to inline the code but may not, and in many cases will not do so. There are so many different compilers in the world for so many different target processors that making the sort of blanket statement in this answer and assuming that all compilers will place code inline when they only have the option to is not tenable. \$\endgroup\$
    – uɐɪ
    Jul 16, 2018 at 8:19
  • 2
    \$\begingroup\$ @ʎəʞouɐɪ You are pointing rare cases where is not possible and it would be a bad idea to not call a function in the first place. I've never seen a compiler so dumb to really use call in the simple example given by the OP. \$\endgroup\$
    – Dorian
    Jul 16, 2018 at 8:32
  • 6
    \$\begingroup\$ In cases where these functions are called only once, optimising the function call out is pretty much a non-issue. Does the system really need to claw back every single clock cycle during the setup? As is the case with optimisation anywhere - write readable code, and optimise only if profiling shows that it is needed. \$\endgroup\$
    – Baldrickk
    Jul 16, 2018 at 9:39
  • 5
    \$\begingroup\$ @MSalters I'm not concerned with what the compiler ends up doing here - more in how the programmer approaches it. There is either no, or negligible performance hit from breaking up the initialisation as seen in the question. \$\endgroup\$
    – Baldrickk
    Jul 16, 2018 at 11:01
2
\$\begingroup\$

First off, there is no best or worst; it's all a matter of opinion. You are very correct that this is inefficient. It can be optimized out or not; it depends. Usually you will see these types of functions, clock, GPIO, timer, etc. in separate files/directories. Compilers generally have not been able to optimize across these gaps. There is one that can that I know of but, not widely used for stuff like this.

Single file:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Picking a target and compiler for demonstration purposes.

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

This is what most of the answers here are telling you, that you are naive and that this all gets optimized and the functions are removed. Well, they are not removed as they are globally defined by default. We can remove them if not needed outside this one file.

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

removes them now as they are inlined.

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

But the reality is when you take on chip vendor or BSP libraries,

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

You are most definitely going to start adding overhead, which has a noticeable cost to performance and space. A few to fives of percent of each depending on how small each function is.

Why is this done anyway? Some of it is the set of rules professors would or still teach to make grading code easier. Functions must fit on a page (back when you printed your work out on paper), don't do this, don't do that, etc. A lot of it is to make libraries with common names for different targets. If you have tens of families of microcontrollers, some of which share peripherals and some don't, maybe three or four different UART flavors mixed across the families, different GPIOs, SPI controllers, etc. You can have a generic gpio_init() function, get_timer_count(), etc. And re-use those abstractions for the different peripherals.

It becomes a case of mostly maintainability and software design, with some possible readability. Maintainability, readability, and performance you can't have all; you can only pick one or two at a time, not all three.

This is very much an opinion-based question, and the above shows the three major ways this can go. As to what path is BEST that is strictly opinion. Is doing all of the work in one function? An opinion-based question, some folks lean for performance, some define modularity and their version of readability as BEST. The interesting issue with what a lot of folks call readability is extremely painful; to "see" the code you have to have 50-10,000 files open at once and somehow try to linearly see the functions in execution order to see what is going on. I find that the opposite of readability, but others find it readable as each item fits on the screen/editor window and can be consumed in whole after they have memorized the functions being called and/or have an editor that can pop into and out of each function within a project.

That is another big factor when you see various solutions. Text editors, IDEs, etc. are very personal, and it goes beyond vi vs Emacs. Programming efficiency, lines per day/month go up if you are comfortable and efficient with the tool you are using. The features of the tool can/will intentionally or not lean toward how the fans of that tool write code. And as a result if one individual is writing these libraries the project to some extent reflects these habits. Even if it is a team, the lead developer or boss's habits/preferences may be forced onto the rest of the team.

Coding standards which have a lot of personal preferences buried in them, very religious vi vs. Emacs again, tabs vs. spaces, how brackets are lined up, etc. And these play into how the libraries are designed to some extent.

How should YOU write yours? However you want, there really isn't a wrong answer if it functions. There is bad or risky code sure, but if written such that you can maintain it as needed, it meets your design goals, give up on readability and some maintainability if performance is important, or vice versa. Do you like short variable names so that a single line of code will fit the width of the editor window? Or long overly descriptive names to avoid confusion, but readability goes down because you can't get one line on a page; now it is visually broken up, messing with the flow.

You are not going to hit a home run the first time at bat. It may/should take decades to truly define your style. At the same time, over that time, your style may change, leaning one way for a while, then leaning another.

You are going to hear a lot of don't optimize, never optimize, and premature-optimization. But as shown, designs like this from the beginning create performance issues, then you start to see hacks to solve that problem rather than re-design from the beginning to perform. I agree there are situations, a single function a few lines of code that you can try to hard to manipulate the compiler based on a fear of what the compiler is going to do otherwise (note with experience this kind of coding becomes easy and natural, optimizing as you write knowing how the compiler is going to compile the code), then you want to confirm where the cycle stealer really is, before attacking it.

You also need to design your code for the user to some extent. If this is your project you are the sole developer; it's whatever you want. If you are trying to make a library to give away or sell, you probably want to make your code look like all the other libraries, hundreds to thousands of files with tiny functions, long function names, and long variable names. Despite the readability issues and performance issues, IMO you will find more folks will be able to use that code.

\$\endgroup\$
7
  • 4
    \$\begingroup\$ Really? What "some target" and "some compiler" do you use may I ask? \$\endgroup\$
    – Dorian
    Jul 17, 2018 at 13:14
  • \$\begingroup\$ It looks to me more like a 32/64 bit ARM8, maybe from a raspbery PI then an usual microcontroller . Have you read the first sentence in the question? \$\endgroup\$
    – Dorian
    Jul 17, 2018 at 13:27
  • \$\begingroup\$ Well, the compiler does not remove unused global functions, but the linker does. If it's configured and used properly, they won't show up in the executable. \$\endgroup\$ Jul 17, 2018 at 13:28
  • \$\begingroup\$ If someone is wondering which compiler can optimize across file gaps: the IAR compilers support multi file compilation (that is how they call it), which allows for cross file optimization. If you throw all the c/cpp files at it in one go you end up with an executable which contains a single function: main. The performance benefits can be quite profound. \$\endgroup\$
    – Arsenal
    Jul 17, 2018 at 14:24
  • 3
    \$\begingroup\$ @Arsenal Of course gcc supports inlining, even across compilation units if called properly. See gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html and look for the -flto option. \$\endgroup\$ Jul 17, 2018 at 15:55
1
\$\begingroup\$

Very general rule - the compiler can optimize better than you. Of course, there are exceptions if you are doing very loop intensive things, but overall if you want good optimization for either speed or code size choose your compiler wisely.

\$\endgroup\$
1
  • \$\begingroup\$ Sadly it's true for most programmers today. \$\endgroup\$
    – Dorian
    Jul 17, 2018 at 15:40
0
\$\begingroup\$

It for sure depends on your own style of coding. One general rule that is out there is, that variable names as well as function names should be as clear and as self-explaining as possible. The more sub-calls or code lines you put in a function, the harder it gets to define a clear task for that one function. In your example you have a function initModule() that initializes stuff and calls sub-routines which then set the clock or set the configuration. You can tell that by just reading the function's name. If you put all the code from the subroutines in your initModule() directly it gets less obvious what the function actually does. But as often, it's just a guideline.

\$\endgroup\$
4
  • \$\begingroup\$ Thank you for your reply. I might change style if needed for performance but the question here is does readability of the code affect the performance? \$\endgroup\$
    – MaNyYaCk
    Jul 16, 2018 at 6:19
  • \$\begingroup\$ A function call will result in a call or a jmp command but that's a negligible sacrifice of resources in my opinion. If you use design patterns you sometimes end up with a dozen layers of function calls before you reach the actual code snipped. \$\endgroup\$
    – po.pe
    Jul 16, 2018 at 6:31
  • \$\begingroup\$ @Humpawumpa - If you are writing for a microcontroller with only 256 or 64 bytes of RAM then a dozen layers of function calls is not a negligible sacrifice, it's just not possible \$\endgroup\$
    – uɐɪ
    Jul 16, 2018 at 8:23
  • \$\begingroup\$ Yes, but these are two extremes... usually you have more than 256 bytes and are using less than a dozen layers - hopefully. \$\endgroup\$
    – po.pe
    Jul 16, 2018 at 9:18
0
\$\begingroup\$

If a function really does only one very small thing, consider making it static inline.

Add it to a header file instead of the C file, and use the words static inline to define it:

static inline void setCLK()
{
    //code to set the clock
}

Now, if the function is even slightly longer, such as being over 3 lines, it might be a good idea to avoid static inline and add it to the .c file. After all, embedded systems have limited memory, and you don't want to increase the code size too much.

Also, if you define the function in file1.c and use it from file2.c, the compiler will not automatically inline it. However, if you define it in file1.h as a static inline function, chances are your compiler inlines it.

These static inline functions are extremely useful in high-performance programming. I have found them to increase code performance often by a factor of over three.

\$\endgroup\$
2
  • \$\begingroup\$ "such as being over 3 lines" -- line count has nothing to do with it; inlining cost has everything to do with it. I could write a 20-line function that is perfect for inlining, and a 3-line function that is horrible for inlining (e.g. functionA() that calls functionB() 3 times, functionB() that calls functionC() 3 times, and a couple of other levels). \$\endgroup\$
    – Jason S
    Jul 16, 2018 at 19:49
  • \$\begingroup\$ Also, if you define the function in file1.c and use it from file2.c, the compiler will not automatically inline it. False. See e.g. -flto in gcc or clang. \$\endgroup\$ Jul 18, 2018 at 4:39
0
\$\begingroup\$

One difficulty with trying to write efficient and reliable code for microcontrollers is that some compilers cannot handle certain semantics reliably unless code uses compiler-specific directives or disables many optimizations.

For example, if has an a single-core system with an interrupt service routine [run by a timer tick or whatever]:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

it should be possible to write functions to start a background write operation or wait for it to complete:

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

and then invoke such code using:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

Unfortunately, with full optimizations enabled, a "clever" compiler like gcc or clang will decide that there's no way the first set of writes can have any effect on the observable of the program and they can thus be optimized out. Quality compilers like icc are less prone to do this if the act of setting an interrupt and awaiting completion involves both volatile writes and volatile reads (as is the case here), but the platform targeted by icc isn't so popular for embedded systems.

The Standard deliberately ignores quality-of-implementation issues, figuring that there are several reasonable ways the above construct could be handled:

  1. A quality implementations intended exclusively for fields like high-end number crunching could reasonable expect that code written for such fields wont' contain constructs like the above.

  2. A quality implementation may treat all accesses to volatile objects as though they might trigger actions that would access any object that is visible to the outside world.

  3. A simple but decent-quality implementation intended for embedded systems use might treat all calls to functions not marked "inline" as though they might access any object that has been exposed to the outside world, even if it doesn't treat volatile as described in #2.

The Standard makes no attempt to suggest which of the above approaches would be most appropriate for a quality implementation, nor to to require that "conforming" implementations be of sufficiently-good quality as to be usable for any particular purpose. Consequently, some compilers like gcc or clang effectively require that any code wanting to use this pattern must be compiled with many optimizations disabled.

In some cases, making certain that the I/O functions are in a separate compilation unit and a compiler will have no choice but to assume they might access any arbitrary subset of objects that have been exposed to the outside world may be a reasonable least-of-evils way of writing code that will work reliably with gcc and clang. In such cases, however, the goal is not to avoid the extra cost of an unnecessarily function call, but rather to accept the should-be-unnecessary cost in exchange for getting the required semantics.

\$\endgroup\$
11
  • \$\begingroup\$ "making certain that the I/O functions are in a separate compilation unit" ... isn't a sure-fire way of preventing optimisation issues like these. At least LLVM and I believe GCC will perform whole-program optimisation in many cases, so could decide to inline your IO functions even if they are in a separate compilation unit. \$\endgroup\$
    – Jules
    Jul 17, 2018 at 1:55
  • \$\begingroup\$ @Jules: Not all implementations are suitable for writing embedded software. Disabling whole-program optimization may be the least expensive way of forcing gcc or clang to behave as a quality implementation suitable for that purpose. \$\endgroup\$
    – supercat
    Jul 17, 2018 at 2:50
  • \$\begingroup\$ @Jules: A higher-quality implementation intended for embedded or systems programming should be configurable to have semantics that are suitable for that purpose without having to completely disable whole-program optimization (e.g. by having an option to treat volatile accesses as though they might potentially trigger arbitrary accesses to other objects), but for whatever reason gcc and clang would rather treat quality-of-implementation issues as an invitation to behave in useless fashion. \$\endgroup\$
    – supercat
    Jul 17, 2018 at 3:18
  • 1
    \$\begingroup\$ Even the "highest quality" implementations won't correct buggy code. If buff is not declared volatile, it won't be treated as a volatile variable, accesses to it may be reordered or optimized entirely out if apparently not used later. The rule is simple: mark all variables that might be accessed outside the normal program flow (as seen by the compiler) as volatile. Are the contents of buff accessed in an interrupt handler? Yes. Then it should be volatile. \$\endgroup\$ Jul 17, 2018 at 13:04
  • \$\begingroup\$ @berendi: Compilers can offer guarantees beyond what the Standard requires and quality compilers will do so. A quality freestanding implementation for embedded systems use will allow programmers to synthesize mutex constructs, which is essentially what the code does. When magic_write_count is zero, the storage is owned by the main-line. When it's non-zero, it's owned by the interrupt handler. Making buff volatile would require that every function anywhere that operates upon it use volatile-qualified pointers, which would impair optimization far more than having a compiler... \$\endgroup\$
    – supercat
    Jul 17, 2018 at 15:40

Not the answer you're looking for? Browse other questions tagged or ask your own question.