-1

Consider the following:

while(running) { 
    if(logMode == true) 
        log(); 

    bar();
}

VS

if (logMode == true)
    func_ptr = log;
else
    func_ptr = noop; //noop is empty function that does nothing

while(running) {
    func_ptr();

    bar();
}

Is the 2nd code snippet (which uses function pointers instead of a conditional) faster?

Note: I don't want to use conditional macros

12
  • 6
    The correct answer is that premature optimisation is almost never worth it. However, I'd say on most common architectures the time taken for testing a variable and jumping (especially with branch prediction) is going to be a lot less than a call, so the first if you expect logMode to be usually false, the second if usually true. Commented Apr 11 at 4:38
  • 6
    Of if(logmode) while(running) { log(); run(); } else while (running) run();... Commented Apr 11 at 4:40
  • 6
    Micro-optimizations like that are almost never worth the extra complexity in maintenance. If you have performance problems, then profile to find the top one single bottleneck, and solve that with plenty of comments about what you're doing and why. Commented Apr 11 at 5:05
  • The result depends heavily on the concrete implementation and optimization level, and is therefore not generally predictable. As this seems to be an XY problem, please edit your question to clarify what you want to solve or know actually. Commented Apr 11 at 5:24
  • 2
    @Alex Mungan, Aside: for boolean objects, if(logMode == true) is a C antipattern. Use if (logMode). Commented Apr 11 at 10:10

5 Answers 5

10

This isn't very meaningful to discuss without a specific system in mind. if creates branches, and on high end systems with instruction cache, that's bad for performance. On the other hand function pointers tend to block function inlining optimizations, so they can be bad for performance too. And if the function is worth inlining in turn depends on how the function was written.

Furthermore, source code isn't very meaningful to discuss without the most important part of any code missing, namely the variable declarations. Whether if(logMode == true) evaluates at each lap of the loop or not depends on how and where logMode was declared and if the compiler can make assumptions that the variable isn't updated inside the loop.

In general, do not attempt micro-optimizations unless:

  • You are actually compiling with optimizations enabled.
  • You have encountered an actual performance bottleneck and tracked it down to this particular code.
  • You have somewhat indepth knowledge of the specific system and ISA.

The general rule of thumb which tends to hold well on all systems: readable code leads to fast executing code. The more the programmer complicates things for the heck of it, the larger the chances of lower performance (as well as bugs).

Sign up to request clarification or add additional context in comments.

Comments

1

If you had one global pointer to your log function that you set up in advance, this may make sense as a design idea.

But as written, the compiler optimiser will most likely take the invariant condition outside the loop in the first case.

Calling a function directly is generally faster than calling through a pointer, but not by very much, especially if that function pointer is being held in a processor register throughout the loop.

So in this exact example I suspect the pointer call may be slower when fully optimised for speed.

As gnasher says, it is hard to see how the code as written in either case would scale to a production code with lots of logging going on. But the single initialised global pointer may work for you to reduce complexity.

The other side of this is that your running state clearly needs to be declared as volatile, or the whole code will be optimised away to loop forever. If running is volatile, how do I know if logMode or func_ptr are volatile? That will change the performance significantly. This is C rather than C++ so I don't care whether they are atomic, but you do.

Comments

1

I agree with answers above: you generally don't need micro-optimizations, especially with high-level languages with optimizing compilers.

However, I want to add one more, slightly lower point of view.

Let's pretend (almost)all optimizations are OFF and find out what machine code we end up with:

In case 1:

  • When logMode is false, we end up just with one jump instruction (branch on if) and proceed right to useful work

  • When logMode is true, we end up with at least three jumps (branch + call + return) and executing whatever inside log() function

In case 2:

  • Regardless of logMode state, we have at least two jumps (call + return) and whatever inside function we calling (that our noop function is empty doesn't means it produces no code). (and also pointer adds indirection)

Real examples (built with `gcc -c -O0 testX.c -o testX`):

test1.c:

    #include <stdio.h>
    void log(void) { printf("Hello\n"); }
    int main(int argc, char **argv)
    {
        int logMode = 0;
        int result;
        while (1) {
            if (logMode == 1) {
                log();
            }
            result = 1; /* simulate useful work */
        }
        return result;
    }

test1 disassembly fragment:

    ...
    0000000000000016 <main>:
      16:   55                      push   %rbp
      17:   48 89 e5                mov    %rsp,%rbp
      1a:   48 83 ec 20             sub    $0x20,%rsp
      1e:   89 7d ec                mov    %edi,-0x14(%rbp)
      21:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
      25:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
    /* start of loop */
      2c:   83 7d fc 01             cmpl   $0x1,-0x4(%rbp) /* compare `logMode` to `1` */
      30:   75 05                   jne    37 <main+0x21>  /* if `false`, jump directly to "useful work" (37) */
      32:   e8 00 00 00 00          call   37 <main+0x21>  /* call log */
      37:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp) /* "useful work" */
      3e:   eb ec                   jmp    2c <main+0x16>  /* back to start of the loop */
    ...

test2.c:

    #include <stdio.h>
    void log(void) { printf("Hello\n"); }
    void noop(void) { /* nothing here */ }
    void (*func_ptr)(void);
    int main(int argc, char **argv)
    {
        int logMode = 0;
        int result;
        if(logMode == 1){
            func_ptr = log;
        } else {
            func_ptr = noop;
        }
        while (1) {
            func_ptr();
            result = 1; /* simulate useful work */
        }
        return result;
    }

test2 disassembly fragment:

    ...
    0000000000000016 <noop>:        /* here's five lines of our "empty" function */
      16:   55                      push   %rbp
      17:   48 89 e5                mov    %rsp,%rbp
      1a:   90                      nop
      1b:   5d                      pop    %rbp
      1c:   c3                      ret
    
    000000000000001d <main>:
      1d:   55                      push   %rbp
      1e:   48 89 e5                mov    %rsp,%rbp
      21:   48 83 ec 20             sub    $0x20,%rsp
      25:   89 7d ec                mov    %edi,-0x14(%rbp)
      28:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
      2c:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
      33:   83 7d fc 01             cmpl   $0x1,-0x4(%rbp)
      37:   75 10                   jne    49 <main+0x2c>
      39:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax
      40:   48 89 05 00 00 00 00    mov    %rax,0x0(%rip)
      47:   eb 0e                   jmp    57 <main+0x3a>
      49:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax
      50:   48 89 05 00 00 00 00    mov    %rax,0x0(%rip)
    /* start of loop */
      57:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax   /* loading function pointer from memory into register */
      5e:   ff d0                   call   *%rax            /* calling function regardless we want logs */ 
      60:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)  /* useful work */
      67:   eb ee                   jmp    57 <main+0x3a>   /* back to start of the loop */
    ...

So in case 1 you have:

  • Less code (doesn't means 'faster')
  • Less jumps when logs not needed
  • Clear sources which easy to understand (may be most important of these)
  • One more branch when you need logs

In case 2 you have:

  • Unnecessary extra call when you don't need logs
  • Indirect call (possible extra memory read to obtain function pointer)

Of course, all of these usually not a deal, and became even more or completely negligible when you turn optimizations ON. (maybe, except, function pointer, which may "tie compiler's hands" in some optimizations)

And at the end: most reliable way to find which is faster is to measure it.

Comments

-1

I don’t have one log() call in my code. I have 1000 different ones logging 1000 different things. How does what you are trying scale?

So you won’t need one function pointer, you need at least 2,000. Enjoy. Especially when the next developer runs to your boss and says “I can’t believe…”

There is a perfectly fine solution that works very well and doesnt produce any clutter in your source code. Why exactly don’t you want to use it?

2 Comments

Or in case of well-designed programs: status information is returned to the caller who in turn returns something to their caller until you gather all such things at the very top level of the program. One error handler at one place, one log at one place. That's the only way to go when code bases get big and cumbersome. Alternatively have some manner of logging thread which anyone can outsource the logging to - still only one place in the code.
I never said I didn't want to use the basic conditional. I merely asked a question out of curiosity.
-1

Alternatively:

if (logMode) {
    while(running) { 
        log(); 
        bar();
    }
} else while (running) {
    bar();
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.