STM32: Macros vs functions in interrupts. - Page 1
Forum section: electronics / general

author: Pack34
author_rank: Frequent Contributor
timestamp: October 19, 2016, 03:06:40 pm
content: When working with the HAL libraries on an STM32 using the OpenSTM32 workbench I noticed a substantial performance hit when calling a function from an interrupt. It seemed on the magnitude of 800 clock cycles.Now, on another project I'm working with the CMSIS libraries on an STM32F4. In the current revision of the firmware functions are called to setup a DAC. I'm looking at changing this up so that a Macro is called instead in an attempt to improve the amount of clock cycles it takes to complete the interrupt.Is it correct to say that the compiler simply takes the code behind the macro and substitutes it where it's called in the interrupt. So no additional stack work is done, like what happens when you call a function?Timing in this situation is critical due to the interrupt is being driven at a maximum of 320-kHz. This is indeed required by the specific application.

author: rstofer
author_rank: Super Contributor
timestamp: October 19, 2016, 03:42:33 pm
content: Quote from: Pack34 on October 19, 2016, 03:06:40 pmYesYou can look at the assembly code to see the difference.

author: Pack34
author_rank: Frequent Contributor
timestamp: October 19, 2016, 03:58:57 pm
content: Thank you. I wanted to be sure to double check.

author: langwadt
author_rank: Super Contributor
timestamp: October 19, 2016, 04:16:33 pm
content: Quote from: Pack34 on October 19, 2016, 03:06:40 pmlookup inline insteadand the is no way a call is adding 800 cycles, entering an interrupt is something like 30 cycles with floating point registers

author: andersm
author_rank: Super Contributor
timestamp: October 19, 2016, 04:27:23 pm
content: Quote from: Pack34 on October 19, 2016, 03:06:40 pmCalling a function doesn't cost 800 cycles. The function you called may have cost 800 cycles. There is a big difference.The function call overhead on a Cortex-M should be on the order of ten cycles. Any decent compiler will inline static functions that are defined above the call site, and that are called only once.

author: Sal Ammoniac
author_rank: Super Contributor
timestamp: October 19, 2016, 05:48:50 pm
content: You shouldn't be doing anything in an interrupt handler that takes a long time. Use the interrupt handler to signal something that performs the required work at thread level after the interrupt has been dismissed.

author: John Coloccia
author_rank: Super Contributor
timestamp: October 19, 2016, 05:54:31 pm
content: If it's still giving you trouble, post the function you're calling.edit:BTW, my thought is that you're inadvertently invoking something that's sucking up tons of time, or doing something else that's bad. Even full on, C++ style virtual calls are lightweight though there is definitely an overall impact from less optimizations being possible.

author: lem_ix
author_rank: Regular Contributor
timestamp: October 19, 2016, 06:33:44 pm
content: Don't know about HAL but there is a small gotcha with macros, they paste text so watch out for formatting like brackets etc if you're using complex constructs. Maybe wrap in do{}while(0).

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: October 19, 2016, 06:43:01 pm
content: Quote from: Pack34 on October 19, 2016, 03:06:40 pm..in which case you probably shouldn't be using functions macros whose contents you don't necessarily know, but writing your own code to do exactly what you want with no wastage.

author: nctnico
author_rank: Super Contributor
timestamp: October 19, 2016, 06:50:04 pm
content: Compiling using speed optimisation (-O3 in gcc) is also a good idea. It will make the code larger but also a lot faster.

author: Kleinstein
author_rank: Super Contributor
timestamp: October 19, 2016, 08:09:02 pm
content: Usually using a function call should not give a 800 cycles penalty - more like 10-30 cycles. There might be extreme cases, if the function is using dynamic memory, that might get a different kind of optimization when used as direct code.

author: Pack34
author_rank: Frequent Contributor
timestamp: October 20, 2016, 12:15:49 am
content: Quote from: andersm on October 19, 2016, 04:27:23 pmThe called function is only ~12 lines of code setting up GPIO pins. Using HAL when this seemed to cause collisions in the interrupt. At the time it seemed like it would have taken a couple hundred clock cycles for that to happen. This time it was the same exercise using the CMSIS libraries. I know I won't be able to get upwards of the 260-kHz that I want on the F4 but I was able to squeeze out 140-kHz, which is enough for now until I get around to laying out the F7 board.Using the direct code on the HAL F7 substantially improved performance, I was hoping the same with the F4 but using Macros instead of copy and pasting the same 12 lines of code throughout the interrupt. The same code is executed at about 8 different locations depending on the situation.

author: langwadt
author_rank: Super Contributor
timestamp: October 20, 2016, 12:28:35 am
content: Quote from: Pack34 on October 20, 2016, 12:15:49 amwhat are those 12 lines of code? setting up IO with any of the usual libraries could (will) do a crap ton of checking and possiblecall function to do so

author: Pack34
author_rank: Frequent Contributor
timestamp: October 20, 2016, 01:07:19 pm
content: Quote from: langwadt on October 20, 2016, 12:28:35 amDirect port manipulation. Setting up a uint16 variable based on an input value and control bits, then pushing it into a GPIO register, clearing, and repeating.The contents of the function isn't an issue. It's as streamlined as possible. The issue was the best way to write the code so it exists once and used in multiple places. With functions there will always be stack work. I wanted to be certain on how the compiler treats a macro in order to minimize the amount of clock cycles it takes to complete the interrupt. I am going to be triggering it as fast as possible before it starts to break down.

author: Kalvin
author_rank: Super Contributor
timestamp: October 20, 2016, 01:25:36 pm
content: If your compiler supports inline functions and/or templates, you can produce as efficient code as you can do with the macros. The bonus is better type checking and the source code is easier to maintain. With the templates you can do pretty sophisticated stuff:http://www.embedded.com/design/programming-languages-and-tools/4442876/C---template-metaprogramming-for-AVR-microcontrollersWith a poor template design, it is also extremely very easy to produce poor code. So, please check the compiler assembly output from time to time in order to see whether the template produces the code you are expecting.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: October 20, 2016, 02:43:06 pm
content: Quote from: Pack34 on October 20, 2016, 01:07:19 pmMacros will be expanded every time - this happens in the preprocessor before the compiler even sees it.

author: John Coloccia
author_rank: Super Contributor
timestamp: October 20, 2016, 05:26:34 pm
content: Sometime a function (in particular static functions) will be more efficient than a macro because it allows the compiler to make optimizations that it would otherwise not make.

author: nctnico
author_rank: Super Contributor
timestamp: October 20, 2016, 06:26:41 pm
content: Quote from: John Coloccia on October 20, 2016, 05:26:34 pmWhy is that? Macros are expanded by the preprocessor (cpp in the GNU tools) which is nothing more than a sophisticated text search & replace. After cpp is done the resulting text is fed into the C compiler. All in all an equally well written inline function or macro shouldn't make any difference.

author: newbrain
author_rank: Super Contributor
timestamp: October 20, 2016, 06:41:27 pm
content: Quote from: lem_ix on October 19, 2016, 06:33:44 pmThat's the first trap for young players, the second one, still due to the "blind" text substitution, is this:Code:#define do_something( a ) do { do_this( a ); do_that( a ); } while (0)uint16_t mustIncrementOnce = 0;do_something( mustIncrementOnce++ ); // Oops!So, one of the best advice so far is to use static inline functions.BUT! To me it's still unclear what's gobbling all that cycles.Before optimizing, using inlines etc. it would be better to understand what's happening.Even using the HAL provided IRQ handlers and callbacks, it should not be so terrible.To the OP: would it be possible to post your code (maybe altered enough not to give away any possible IP/trade secret)?Which interrupt handler is being used?A timer one would be a fair guess, as an exact rate seems to be the aim.If you are writing to the DAC (but that's not clear), have you consideredthe use of DMA?

author: John Coloccia
author_rank: Super Contributor
timestamp: October 20, 2016, 06:45:35 pm
content: Quote from: nctnico on October 20, 2016, 06:26:41 pmIf the compiler knows it's a function, and it's called multiple times, it's much easier to take advantage of things such as instruction/data caches, optimizing memory access/pipelines and things like that. May or may not make a difference depending on the specific compiler and platform, but it's not always obvious what will actually speed up vs slow down your code.This is why I recommend that people write clean, simple code and not worry too much about optimizing other than avoiding patterns that make optimization impossible if it's important that something runs quickly. Often times the compiler will do a better job than you can, especially if you have a good feel for how to write the code so that it works in harmony with what the compiler is good at doing.Of course, the opposite is also true. Sometimes the most difficult to find bugs are ones where the compiler has optimized code and broken something. If things absolutely have to be done in a certain order or a certain way, sometimes you have to turn optimizations off completely, or take steps to force the issue. Proper use of the volatile keyword comes to mind in C as an example.

author: IanB
author_rank: Super Contributor
timestamp: October 20, 2016, 06:54:16 pm
content: Why would anyone ever write:Code:do { ... } while (0)What's the point?(In other words, why not just write:Code:{ ... }without the redundant do and while bits?)

author: John Coloccia
author_rank: Super Contributor
timestamp: October 20, 2016, 07:05:02 pm
content: Quote from: IanB on October 20, 2016, 06:54:16 pmBecause the first way, this works:if (x)MACRO(a);elsesomethingElse();and the second way it doesn't, because if you just do {}, the macro expands toif(x){};elsesomethingElse();Basically, it makes the macro somewhat behave like a function so it doesn't look funny when you don't have a semicolon at the end. Personally, I don't really like it because it obfuscates things, but it's pretty common.I tend to always use braces anyway, unless something takes the form of:if (x) func();No else...all fits neatly on one line...makes perfect sense and looks pretty, but I tend to avoid that too unless it somehow makes the code MORE readable, like:if (func1()) thing1();if (func2()) thing2();if (func3()) thing3();Comes up sometimes, but I'd just as soon just use multiple lines and braces and avoid the issue altogether. Defensive programming.

author: Bruce Abbott
author_rank: Frequent Contributor
timestamp: October 20, 2016, 07:15:38 pm
content: Quote from: nctnico on October 20, 2016, 06:26:41 pmBut what if they aren't?Whether one method or another is more efficient depends on how they are implemented and what the compiler does with them. Seemingly subtle differences between the code in an expanded macro and an inline function could make large differences in code size and execution speed. So unless you can guarantee that they both produceidenticalsource code after pre-processing, the 'equally well written' proviso - while technically true - is not useful.Quote from: newbrainYep. View the machine code to see what the compileractuallyproduced. Everything else is speculation.Quote from: Pack34And you know this how?

author: hans
author_rank: Super Contributor
timestamp: October 20, 2016, 08:11:39 pm
content: Quote from: Pack34 on October 19, 2016, 03:06:40 pmIf you want to know it more accurately, you could set up a internal timer ready-to-go in your main code. Then clear&start it when you enter the ISR and stop it when you leave it. The timer value then contains the cycles (or multiple of) taken which can be read while running your main code.Alternatively you could set/clear a GPIO upon entering/leaving the ISR and measure it's timing with a scope or LA.You could obviously also narrow this down around particular parts of the code.QuoteMy initial thought is that the assertions of ST's CMSIS library are very slow. ST's libraries assert everything, even on the most simple GPIO_SetBits functions, in order to check if the port specified is a GPIO port, etc.In general I consider assertions to be a good thing, but they can be a bit annoying when performance gets in the way. You could try disabling assertions (which is often useful in e.g. release mode) and see if that speed things up.If so I would rewrite the functions in your ISR so you can keep assertions turned on in the rest of your project.If not then I would probably try to profile (manually) what code takes so many cycles to execute and reconsider the approach taken.In terms of the choice between macro's and functions: always prefer functions when you can. Macro's are horrible to debug and very error prone way of writing code. As demonstrated a single semicolon, brackets inside macro arguments or forgetting braces can introduce 'invisible bugs' because the actual compiled code is not transparant anymore. Debugging that via the C preprocessor output is not my definition of fun. This makes existing macro's almost "work of magic" that you never want to change again, because you need to be careful with using and writing them.If you're using GCC you could use -O3 and write it with a few short explicit functions instead. If you want to keep debugability in the rest of your project, you could consider usinga few pragma's(or better: int foo(int i) __attribute__((optimize("-O3"))) ) around the ISR functions. This should considerably 'streamline' the assembly for speed.

author: Pack34
author_rank: Frequent Contributor
timestamp: October 20, 2016, 09:52:26 pm
content: Quote from: hans on October 20, 2016, 08:11:39 pmI did. It was the function call. Copy and pasted the contents of the function into the interrupt and the time-sink went away.

author: newbrain
author_rank: Super Contributor
timestamp: October 20, 2016, 11:20:40 pm
content: Quote from: Pack34 on October 20, 2016, 09:52:26 pmStrange.Have you investigated the reason?Are you passing large amount of data by value?Using floating point in both the ISR and the function (but 800 cycles...)?Still, code diagnostics without seeing any code: it's always a shot in the dark.

author: cv007
author_rank: Frequent Contributor
timestamp: October 21, 2016, 12:47:43 am
content: QuoteWe can't see what your code is, what the optimization settings are being used etc., but its not that hard to believe that when 'calling' the function its doing a lot more- the function has no idea 'who' may be calling it and has to do 'everything' because it does not know what parameters are passed (if any). Taking the same function and 'inlining' it (manually, or better just use the inline attribute for the function, whatever that may be for your compiler) , now the compiler can optimize it because it knows what the parameters are and can eliminate everything that's not relevant. 12 lines sounds like very little, but who knows (we don't) what is hiding (defines/macros/other function calls) in those 12 lines. Its also not hard to believe 12 lines can expand into a truckload of code behind the scenes.The function call may take long, but its not the call that takes the time, its the function not knowing ahead of time who is calling it and what is has passed to it.Not sure what compiler is being used, but there will be a way to keep 'intermediate' files (like .i , .s), generate a combined c/asm listing (.lst), etc. - then you can see exactly whats going on by reading these files (.lst file is handy). I'm not great at this stuff, but after a while of checking what the compiler is up to, you get a pretty good idea whats going on under the hood.

author: westfw
author_rank: Super Contributor
timestamp: October 21, 2016, 07:57:14 am
content: QuoteThat's really hard to believe, unless you did something like pass a 200word structure by value...

author: Pack34
author_rank: Frequent Contributor
timestamp: October 21, 2016, 02:35:05 pm
content: Quote from: westfw on October 21, 2016, 07:57:14 amNope. Just an unsigned int and a char. Then the function was called a second time.The purpose of this thread was a sanity check on how the compiler handles macros. The observed and measurable behavior with the HAL libraries was a significant performance hit when calling a function from an interrupt and was alleviated when the code was directly used inside the interrupt instead of a function call. Both the cause and solution for this has been found.

author: Kalvin
author_rank: Super Contributor
timestamp: October 21, 2016, 02:46:44 pm
content: Quote from: Pack34 on October 21, 2016, 02:35:05 pmFor me it is still quite unclear what kind of code the compiler generated in both cases (inlined in the interrupt routine vs. calling a function from the interrupt routine). For example, what HAL function did you actually call? Can you enable the C compiler switches so that the compiler will also generate the assembly code so you can see where the 800 cycles of the overhead are coming from?

author: newbrain
author_rank: Super Contributor
timestamp: October 21, 2016, 02:48:50 pm
content: Quote from: Pack34 on October 21, 2016, 02:35:05 pmTBH, I don't agree.Maybe the solution, but we have no evidence of what was happening.The HAL, albeit not a light weight beast, does not add overhead to thefunction callsthemselves.We haven't seen any code, not even a hint of what IRQ was being used, and if the HAL was called by the function or the HAL provided ISR used.

author: John Coloccia
author_rank: Super Contributor
timestamp: October 21, 2016, 03:12:04 pm
content: Quote from: newbrain on October 21, 2016, 02:48:50 pmIt's hard to help some people.

author: donotdespisethesnake
author_rank: Super Contributor
timestamp: October 21, 2016, 03:39:26 pm
content: There is no way a function callby itselftakes 800 cycles, so you definitely didn't get to the root cause.But, there may be some other strange effect simply from locating code in different places. When you get to the large Cortex MCUs, there are accelerators and caches etc that have an impact on performance, as well as other things.Btw, macros are evil

author: Bruce Abbott
author_rank: Frequent Contributor
timestamp: October 22, 2016, 03:26:16 pm
content: Quote from: Pack34 on October 21, 2016, 02:35:05 pmInthisone particular case. Doesn't mean it will be the same for other code.Show us the code that proves your theory, or admit that you havenotdetermined how the compiler handles macros vs function calls.

author: rob42
author_rank: Newbie
timestamp: November 02, 2016, 10:32:35 am
content: why not simply generate ASM listings and compare it for both cases ?