5 ms·
Does anyone have any advice on articles about C compiler optimizations in general (especially gcc)? I'm doing my first serious C work in ten years, and I keep w
by pjungwir 12y ago
Does anyone have any advice on articles about C compiler optimizations in general (especially gcc)? I'm doing my first serious C work in ten years, and I keep wondering if I should fuss with things like this or let the compiler handle it all:
foo->bar->baz[i].oof = foo->bar->baz[i].durb + meep;
vs
what *tmp = foo->bar->baz[i];
tmp->oof = tmp->durb + meep;
EDIT: I'm not asking for a link to this:
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
I'm asking if there is advice about it. Any overviews with common pitfalls, advice on when to use -O1 vs -O2, specific optimizations to turn on/off, etc.
- exDM69 12y agofoo->bar->baz[i].oof = foo->bar->baz[i].durb + meep; This is fine, no need to "optimize" anything. This kind of common subexpression elimination should be done by any modern compiler (for any language!) and the algorithm behind it is taught in university classes too. Most of the time it's safe to use -O3. If you're doing numerical code with floating points -ffast-math is also pretty safe if your code is correct (ie. no NaN/Inf bugs). Almost the only reason to turn off optimization (-O0) is when higher optimizations make using a debugger harder. Here's a pretty nice article with some specific optimizations that GCC can (or can't) do. It's pretty old, though, the examples were done with GCC 4.2.1, current version is around 4.9. http://ridiculousfish.com/blog/posts/will-it-optimize.html http://ridiculousfish.com/blog/posts/will-it-optimize.html These days Clang can be as good or better than GCC most of the time. The exceptions are in more exotic code like kernel space stuff or micro controller programming. There's no room for guesswork if you actually want to optimize code, so spend some time reading the assembler output from your compiler as well as benchmarking the results. I usually use objdump -d objfile.o to look at assembly output.
- sjolsen 12y ago>I usually use objdump -d objfile.o to look at assembly output You can also compile to assembly with -S. I think it's clearer that way.
- exDM69 12y agoYes, I know about the -S flag. But CFLAGS, etc comes from Makefiles so inspecting the object files (which I already have) is easier than re-invoking the compiler with -S added to the command line.
- pjungwir 12y agoReally appreciate everyone's replies! A related question about my example: what if I want to assign the pointer dereference to `tmp` to improve readability (rather than avoid multiple traversals). Is there any reason not to use a tmp variable (presumably with a better name)?
- sjolsen 12y ago>what if I want to assign the pointer dereference to `tmp` to improve readability I personally find code like that harder to follow. The first version is clearer than the second (and you forgot to take the address of foo->bar->baz[i]).
- pjungwir 12y ago> and you forgot to take the address of foo->bar->baz[i] Ha, I was afraid of that. :-) Still re-learning when I need that with arrays and when not.
- haberman 12y ago> Is there any reason not to use a tmp variable (presumably with a better name)? Nope -- shouldn't hurt at all. It's interesting to me that LuaJIT recommends not using temp variables like this because they can hurt optimization for LuaJIT. That's obviously very different than C in almost every way, I just mention it because it was so surprising to me that there is a situation (in any optimized language) where a temp variable could hurt optimization.
- exDM69 12y agoNo, it doesn't hurt to have an extra local variable if you have compiler optimization enabled. If it would be a global or member of struct/class, that's a different deal. It's useful to add some variables to be inspected in the debugger.
- TheLoneWolfling 12y agoThis shouldn't need to be said, but don't use -ffast-math if you want reproducibility. Unfortunately, the times when you most want reproducibility tend to be the same sorts of number-crunching where -ffast-math would be most useful.
- exDM69 12y ago> Unfortunately, the times when you most want reproducibility tend to be the same sorts of number-crunching where -ffast-math would be most useful. Thanks for the clarification. The obvious caveats of -ffast-math are well documented and most applications shouldn't use that flag. There are exceptions to this however, I tend to work on such problems. For example, game physics, 3d graphics and some scientific algorithms that have built-in numerical inaccuracy (so -ffast-math doesn't help but doesn't hurt either) but high perf requirements. I also tend to have extensive testing for the most crucial parts of my programs that should catch any problems with this (but many people don't do this with game physics, etc code). Thankfully, -ffast-math is easy to disable if you start suspecting problems that are caused by that flag.
- TheLoneWolfling 12y agoI'd diagree, Game physics and scientific algorithms are often precisely where you want reproducible results. Game physics because of lockstep networking and replays, scientific algorithms because... well, you want your results to be reproducible. Scientific method and all that. Often times you don't mind if it isn't accurate, but that isn't the same thing as precision. You want it to be precise, i.e. reproducible. Yes, there are cases
- sjolsen 12y agoThat's basic dataflow analysis. Unless one of those pointers is to a volatile object, I'd be very surprised if any halfway serious compiler produced more than one access to foo->bar->baz[i] unless "meep" has side effects. To address the question, these some of the guidelines I try to follow: - Compile with "-Wall -Wextra" (and "-pedantic" if feasible) - Modularize your code. You can always mark functions "static inline." - Don't try to be clever; "Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?" In fact, Kernighan has a lot of good advice: https://en.wikipedia.org/wiki/The_Elements_of_Programming_Style https://en.wikipedia.org/wiki/The_Elements_of_Programming_St.... - Be careful with signed integers. Overflow can do weird things to your program. You can make signed integers act like unsigned integers on overflow using -fwrapv, but if that behaviour is correct, you probably should have used an unsigned integer outright. - Be careful with pointers; specifically, the requirements of any pointer passed to a function should be explicitly documented: whether it is allowed to be null, whether it's an "in" parameter or an "out" parameter, whether it points to one object or an array, etc. If a pointer points to an array, carry a length parameter with it; null-termination is really easy to foul up. - Don't optimize until the program needs to be faster. When it does, profile and target the low-hanging fruit. Personally, I usually use either -O0 or -Ofast, depending on whether or not I'm debugging something (-Og is a good one if you need speed while debugging). - Speaking of optimization, don't underestimate the power of inlining. It's easy to go overboard with it, but it can make a big difference in the right situations. - Your compiler probably has a peephole optimizer. Replacing "i / 16" with "i >> 4" is probably not an improvement to the quality of either the source code or the object code. - If you find yourself reimplementing something that C++ knows how to do, consider using C++ to do that. It's not always politically feasible, but remember that you can link C and C++ code.
- haberman 12y agoHere's my advice. Generally speaking, the compiler is really smart. I would characterize the optimization you put as a third-grade optimization: GCC is in college (Clang too, for that matter). It's many steps ahead of that level. However, if you're ever in doubt, I recommend compiling very short functions and viewing their output. typedef struct { int oof; int durb; } baz_t; typedef struct { baz_t *baz; } bar_t; typedef struct { bar_t *bar; } foo_t; void f(foo_t *foo, int i, int meep) { foo->bar->baz[i].oof = foo->bar->baz[i].durb + meep; } $ gcc -O2 -c -o test.o test.c $ objdump -d -r -M intel test.o test.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <f>: 0: 48 8b 07 mov rax,QWORD PTR [rdi] 3: 48 63 f6 movsxd rsi,esi 6: 48 8b 08 mov rcx,QWORD PTR [rax] 9: 48 8d 34 f1 lea rsi,[rcx+rsi*8] d: 03 56 04 add edx,DWORD PTR [rsi+0x4] 10: 89 16 mov DWORD PTR [rsi],edx 12: c3 You can see here that it followed the chain of pointers only once. The one thing to watch out for though is things that gcc isn't allowed to optimize because of C. For example, if a pointer escapes the function (to another function that the optimizer can't see), gcc cannot assume that the pointed-to memory remains unchanged, even if the called function takes a const pointer! Because the function could always cast away const. For example, this variant will have to follow the chain twice: int g(const foo_t *foo, int i); void f(foo_t *foo, int i) { int x = g(foo, foo->bar->baz[i].durb); foo->bar->baz[i].oof = x; } Generally people always use at least -O2. The main difference between -O2 and -O3 is that -O3 is more aggressive with unrolling and other optimizations that increase code size, so sometimes -O2 is faster because of icache pressure. I generally use -O3 on my tightest loops and -O2 (or even -Os) on everything else.