Code/data locality (compactness, % of each cache line that gets used)
Predictable access patterns: pre-fetch (instructions and data) friendly. This explains branching costs, why linear transversal might be faster than trees at smaller scales because of pointer chasing, why bubble sort is the fastest if the chunks fit in the cache.
Avoid false sharing: shared cache line unnecessarily with other threads/cores (due to how the data is packed) might have cache invalidating each other when anyone writes.
Here’s a concise paper describing common C programming pitfalls by Andrew Koening (www.literateprogramming.com/ctraps.pdf) corresponding to be book with the same title.
As a reminder to myself, I’ll spend this page summarizing common mistakes and my history with it.
Here are the mistakes that I don’t make because of certain programming habits:
Operator precedence: I use enough parenthesis to not rely on operator precedence
Pointer dereferencing: I always do *(p++) instead of *p++ unless it’s idiomatic.
for() or if() statements executing only first line: I always surround the block with {} even if it’s just one line. Too often we need to inject an extra line and without {} it becomes a trap.
Undefined side effect order: I never do something like y[i++]=x[i]
char* p, q: very tempting since C++ style emphasize on pointer as a type over whether the variable is a pointer. I almost never declare multiple variables in one line.
Macro repeating side effects: use inline functions instead whenever possible. Use templates in C++.
Unexpected macro associations: guard expressions with (). Use typedef.
Did these once before, adjusted my programming habits to avoid it:
counting down in for() loop with unsigned running variable: I stick with signed running variables in general. If I’m forced to use unsigned, I’ll remind myself that I can only stop AFTER hitting 1, but not 0 (i.e. i=0 never got executed).
Haven’t got a chance to run into these, but I’ll program defensively:
Integer overflow: do a<b instead of (a-b)<0. Calculate mean by adding halfway length to the smaller number (i.e. (a+b)/2 == a + (b-a)/2 given a<b). Shows up in binary search.
Number of bits to shift is always unsigned (i.e. -1 is a big number!)
What I learned from the paper:
stdio buffer on stack (registered with setbuf()) freed before I/O flushed: use static buffer (or just make sure the buffer lives outside the function call).
char type might be signed (128 to 255 are -128 to -1) so it sign extends during upcast. Use unsigned char go guarantee zero extend for upcasting.
toupper()/tolower() might be implemented as a simple macro (no checks, incorrect /w side effects)
Can index into a string literal: "abcdefg"[3] gives 'd'
Mistakes that I usually make when I switch back from full-time MATLAB programming:
Logical negation using ~ operator instead of ! operator.
Common mistakes I rarely make because of certain understanding:
Forgetting to break at every case in switch block. It’s hard to forget once you’re aware of the Duff’s device.
sizeof(a[])/sizeof(a[0]) after passing into a function does not give array length: hard to get it wrong once you understand that array (declared on stack) has meta-info that cannot be accessed beyond the stack level it’s initialized.