Sunday, December 23, 2012

C/C++ code - performance optimization tricks

This post simply borrows (read cut-pastes) from material in
http://stackoverflow.com/questions/2074099/coding-practices-which-enable-the-compiler-optimizer-to-make-a-faster-program

http://andr0meda.com/plunge/?p=95

http://assemblyrequired.crashworks.org/2008/07/08/load-hit-stores-and-the-__restrict-keyword

They are my notes all in one place.I have not yet tried any of the code snippets myself. I take them at face value, to be tried as and when I'm called upon to do so.

 Memory Pools

Algorithms used to manage the heap are notoriously slow in some platforms (e.g., vxworks). Even worse, the time that it takes to return from a call to malloc is highly dependent on the current state of the heap. Therefore, any function that calls malloc is going to take a performance hit that cannot be easily accounted for. That performance hit may be minimal if the heap is still clean but after that device runs for a while the heap can become fragmented. The calls are going to take longer and you cannot easily calculate how performance will degrade over time. You cannot really produce a worse case estimate. The optimizer cannot provide you with any help in this case either. To make matters even worse, if the heap becomes too heavily fragmented, the calls will start failing altogether. The solution is to use memory pools (e.g., glib slices ) instead of the heap. The allocation calls are going to be much faster and deterministic if you do it right.

 For C++, may be writing or using custom allocators that employ pooling will work well.

Avoid Aliasing related slowdowns

Write to local variables and not output arguments! This can be a huge help for getting around aliasing slowdowns. For example, if your code looks like

Bad 
     void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)       
     { 
       for (int i=0; i<numFoo, i++)  
        { barOut.munge(foo1, foo2[i]); 
        }
     } 

the compiler doesn't know that foo1 != barOut, and thus has to reload foo1 each time through the loop. It also can't read foo2[i] until the write to barOut is finished. You could start messing around with restrict'ed pointers, but it's just as effective (and much clearer) to do this:

Good

    void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
    {
       Foo barTemp = barOut;
       for (int i=0; i<numFoo, i++)
       {
         barTemp.munge(foo1, foo2[i]);
       }
       barOut = barTemp;
    } 
It sounds silly, but the compiler can be much smarter dealing with the local variable, since it can't possible overlap in memory with any of the arguments. This can help you avoid the dreaded load-hit-store (mentioned by Francis Boivin in this thread).

 

Load Hit Store issues

- read a from memory A into register X
- do something with a in X -> becomes b
- write b from register X to memory B
- read c from memory C into register Y 

There is nothing wrong with the code, nor with the reasoning behind it. But the CPU does have a problem when it has to execute it. The reason is that C and B can be equal in value, pointing effectively to the same address in memory. This means that c is in fact intended to be b (or a part of b – instructions sometimes chop binary values into lower and upper word fragments).
This dependency is recognized in the L1 cache, since all write instructions to memory temporarily block the CPU from reading from that cache until the value is written. So before the CPU can start reading from ‘C’, it has to wait until b is written to ‘B’, which could be at the very same memory address. [If the CPU would not wait, the value used as c is potentially not the one written by the previous instruction to memory at address B, and any further program flow / calculation might be off.] This waiting can take up quite some time, from 40 to 80 cycles, depending on your CPU. Despite advances in bus-logic and increased Front-Side-Bus performance, writing into memory is usually one of the slowest operations in the instruction set.
 
int CauseLHS(int *ptrA)
{
   int a, b;
   int *ptrB = ptrA; // B and A point to the same address
   *ptrA = 5; // write data to address ptrA
   b = *ptrB; // read that data back in again (won't be
                  // available for 80 cycles)
   a = b + 10; // stalls! the data isn't available yet.
} 

 

Use __restrict keyword for pointers

Use pointer parameters only if necessary, and mark them restrict if possible. This helps alias analysis a lot because the programmer guarantees there is no alias (the interprocedural alias analysis is usually very primitive). Very small struct objects should be passed by value, not by reference.
int slow(int *a, int *b)
{
   *a = 5;
   *b = 7;
   return *a + *b; // LHS stall: the compiler doesn't
                  // know whether a == b, so it has to
                  //  reload both before the add
}
 
int fast(int * __restrict a, int * __restrict b)
{
   *a = 5;
   *b = 7; // RESTRICT promises that a != b
   return *a + *b; // no stall; the compiler hangs onto
                         // 5 and 7 in the registers.
}
  
It bears repeating that __restrict is a promise you make to your compiler. If you break your promise, you can get incorrect results. If pointers pA and pB are __restrict, and pA == pB, that will cause mysterious bugs.
For example, in the fast() function above, if a does equal b, then the correct return value would be 14. However, the compiler won’t know that *b = 7 has changed the value of *a, and so you might actually get a return value of 12.

 

Pay attention to order in which memory is accessed

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}
  

 

Do IO in blocks

For example :- 
static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);
 

 

Don't use printf family for constant data

Constant data can be output using a block write. Formatted write will waste time scanning the text for formatting characters or processing formatting commands. See above code example.

 

Declare small functions as inline or macros


 

 Loop Unrolling

Each loop has an overhead of incrementing and termination checking. To get an estimate of the performance factor, count the number of instructions in the overhead (minimum 3: increment, check, goto start of loop) and divide by the number of statements inside the loop. The lower the number the better.

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}
After unrolling:
unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}
In this advantage, a secondary benefit is gained: more statements are executed before the processor has to reload the instruction cache.

 

Use boolean arithmetic instead of if statements. 

Basically reduce if blocks.
For example:-

bool status = true;
status = status && /* first test */;
status = status && /* second test */;
 

 

Factor out variable allocation outside of loops


 

Declare static text (const literals) as static const

When variables are declared without the static, some compilers may allocate space on the stack and copy the data from ROM. These are two unnecessary operations. This can be fixed by using the static prefix.

 

Avoid too many floating point to int conversions and vice versa. 


They use different registers and converting from one type to another means calling the actual conversion instruction, writing the value to memory and reading it back in the proper register set.


 

Use const correctness as much as possible in your code. 

It allows the compiler to optimize much better.
In this document are loads of other optimization tips: CPP optimizations (a bit old document though)

Highlights:
  • use constructor initialization lists
  • use prefix operators (++i instead of i++)
  • use explicit constructors
  • inline functions
  • avoid temporary objects
  • be aware of the cost of virtual functions
  • return objects via reference parameters
  • consider per class allocation
  • consider stl container allocators
  • the 'empty member' optimization
  • etc

 

Put functions that are used together in close physical proximity in source.

 This increases the likelihood that they will be near each other in object files and near each other in your executable. This improved locality of instructions can help avoid instruction cache misses while running.

 

Make most functions static.

 

If global variables are used, mark them static and constant if possible.

If they are initialized once (read-only), it's better to use an initializer list like static const int VAL[] = {1,2,3,4}, otherwise the compiler might not discover that the variables are actually initialized constants and will fail to replace loads from the variable with the constants.

 

When writing tests with multiple conditions place the most likely one first.

 if(a || b || c) should be if(b || a || c) if b is more likely to be true than the others. Compilers usually don't know anything about the possible values of the conditions and which branches are taken more (they could be known by using profile information, but few programmers use it).

 

Using a switch is faster than doing a test like if(a || b || ... || z). 

Check first if your compiler does this automatically, some do and it's more readable to have the if though.

 

Data structure alignment

It  is the way data is arranged and accessed in computer memory. It consists of two separate but related issues: data alignment and data structure padding. When a modern computer reads from or writes to a memory address, it will do this in word sized chunks (e.g. 4 byte chunks on a 32-bit system). Data alignment means putting the data at a memory offset equal to some multiple of the word size, which increases the system's performance due to the way the CPU handles memory. To align the data, it may be necessary to insert some meaningless bytes between the end of the last data structure and the start of the next, which is data structure padding.

 

Profile, Profile, Profile

Then, and only then, you might want to start investigating the effects of telling the compiler how to use memory. Make 1 change at a time and measure its impact. Focus only on the hotspots.
Yes, compiler writers are not from a race of coding giants and compilers contain mistakes and what should, according to the manual and according to compiler theory, make things faster sometimes makes things slower. That's why you have to take one step at a time and measure before- and after-tweak performance.

And yes, ultimately, you might be faced with a combinatorial explosion of compiler flags so you need to have a script or two to run make with various compiler flags, queue the jobs on the large cluster and gather the run time statistics. If it's just you and Visual Studio on a PC you will run out of interest long before you have tried enough combinations of enough compiler flags.






 
 

Sunday, December 2, 2012

Testing the optimality of algorithms using Alloy - Part 3

Continuing from part 2...
http://feels-good-to-ramble.blogspot.in/2012/11/testing-optimality-of-algorithms-using_30.html
 , we are not done yet concluding that a BFS-based coloring algorithm is suboptimal. We now need to modify the algorithm to run BFS for every vertex as starting vertex in turn, and then check whether the least number of colors used in all those sub-runs is still suboptimal. This complicates things quite a lot, because we now need to store results for every BFS sub-run. It is not now enough to continue to store with Node, the fields that we stored with it in part 2. This is because, the field values are liable to change for each sub-run of the BFS. We solve this problem in a simple way by introducing another signature called Run, to denote a sub-run. We store fields, 'colormap', 'ordermap', 'levelmap' and 'snapshot', to denote maps between a Node and respectively its BFS 'color', its order of discovery during this sub-run of the BFS, its level in this sub-run of the BFS and finally, the  set of Nodes that include itself and the ones discovered before it in this sub-run. We need to introduce one more signature called Order which is the order of discovery of a Node for a sub-run. Only, the fields 'adjacency' and 'optcolor' remain with the Node, since they do not change from sub-run to sub-run. Finally, the predicate 'bfs_suboptimal' changes to assert the following:
all r:Run |  bfs[r] and #Node.optcolor < #r.colormap[Node]

which means that for every sub-run, the coloring found by Alloy (in 'optcolor') uses fewer colors than the coloring obtained by BFS in that sub-run.
  With these changes to our Alloy source,  it was found that no suboptimal coloring is obtained for vertices (Nodes)  up to 5 in number. The smallest graphs where BFS-based coloring is suboptimal has 6 or more vertices. All the test results in the figures below show only the 'optcolor' arrows and not the colors obtained through BFS. Also, the double-arrowed, red edges between any two Nodes (blue boxes) are the adjacency edges between the nodes.


Fig 1
Fig 2
Fig 3

 Some more results found after tweaking the source to constrain the number of 'optcolor's required to more than 3, then we get no 'suboptimal' coloring for 6 nodes, but we do get some for 7 nodes, one of which is shown in fig 4.

Fig 4

 This means that for 6 nodes the BFS based coloring does not use more than 4 colors in any case.




Fig 5

 It was also found similarly, that for 7 nodes the BFS based coloring does not exceed 5 colors, but for 8 nodes it does, as shown in Fig 5.
 The Alloy source listing is given below :


open util/graph[Node] as GR
open util/ordering[Order] as OR
open util/ordering[Color] as CO
open util/ordering[Level] as LV


sig Color {}

sig Run {
   start_node: one Node,
  private colormap: Node->one Color,
  private ordermap: Node one ->one Order,
  private levelmap: Node->one Level,
  private snapshot: Node  -> set Node
}

sig Order{}

sig Node{
    adjacency: set Node,
    optcolor: one Color
}

sig Level {}

pred  undirected_graph() {
    GR/undirected[adjacency]
    GR/noSelfLoops[adjacency]
       GR/stronglyConnected[adjacency]
}

pred init (r:Run) {
    let i=OR/first | let n= r.start_node | r.ordermap[n]=i and
    r.levelmap[n]=LV/first and r.snapshot[n] = n and r.colormap[n]=CO/first
}

fun done_nodes(r:Run, n:Node): set Node {
   r.snapshot[n]
}

fun next_level (r:Run, n:Node) : Level {
   LV/next[r.levelmap[n]]
}

fun prev_level (r:Run, n:Node) : Level {
   LV/prev[r.levelmap[n]]
}

fun level(r:Run, n:Node):Level {
   r.levelmap[n]
}

fun next_order(r:Run, n:Node): Order {
  OR/next[r.ordermap[n]]
}

fun prev_order(r:Run, n:Node): Order {
  OR/prev[r.ordermap[n]]
}

fun order(r:Run, n:Node) :Order {
  r.ordermap[n]
}

fun nodes_at_same_level(r:Run, n:Node) : set Node {
  (r.levelmap). (r::level[n])
}

fun node_at_order (r:Run, o:Order) : one Node {
  (r.ordermap). o
}

fun nodes_at_level (r:Run, l:Level) : set Node {
  (r.levelmap). l
}

fun next_siblings(r:Run, n:Node): set Node
{
  let parent_nodes= r::nodes_at_level [r::prev_level[n] ]   |
     some parent_nodes implies
      {
       let siblings = parent_nodes.adjacency -  r::done_nodes[n] |
         some siblings  implies
            siblings
         else
            none
     }
      else
            none
}


fun  children_at_next_level(r:Run, last_upper_node:Node) : set Node
{
    let n=last_upper_node |
           let peers= (r::nodes_at_same_level[n].adjacency - r::done_nodes[n]) |
           some peers implies
             peers
           else
             none
}

pred bfs(r:Run) {
    init[r]  
    all ord:Order - OR/last | step[r, ord ]
   correctness [r]
}

pred correctness [r:Run] {
   all n:Node | r.colormap[n] not in r.colormap[n.adjacency]
}

fact {
  some Node
  undirected_graph 
  no disj r1,r2:Run | r1.start_node=r2.start_node
  no disj n1,n2:Node | start_node.n1 = start_node.n2
  Run.start_node = Node
  coloring_prob
}

pred coloring_prob  { (no n,m:Node | (m in n.adjacency and n.optcolor= m.optcolor) ) }

pred set_color(r:Run, n:Node)
{
 let prevn = r::node_at_order[r::prev_order[n]] | let cols=Color - r.colormap[(n.adjacency & r::done_nodes[prevn])] |
   r.colormap[n] = CO/min[cols]
}

--This creates the next level from the current level. It is based on traversing the adjacencies
pred step (r:Run, i : Order) {
     let n= r::node_at_order[i] | 
     {
       let sibs=r::next_siblings[n] |
        some sibs implies
        {
         one m:sibs | r::order[m] = r:: next_order[n]  and
              r::level[m] = r::level[n] and
              r::done_nodes[m] = r::done_nodes[n] + m
       }
      else
      {
         let  children=r::children_at_next_level[n] |
         some children implies
         {
           one m: children |
               r::order[m] = r::next_order[ n ] and
              r::level[m] = r::next_level[n] and
              r::done_nodes[m] = r::done_nodes[n] + m
        }
      }
     let m= r::node_at_order[ r::next_order[n] ] |  r::set_color[m]
   }
}

pred bfs_suboptimal()
{
   all r:Run |  bfs[r] and #Node.optcolor < #r.colormap[Node]
}

run bfs_suboptimal for 6 but  4 Color

To conclude, in this set of 3 posts we saw how Alloy can be employed to test our understanding of problems, especially when we are learning about or exploring a subject. Specifically, we demonstrated how a simple (class-P) BFS-based algorithm is unable give optimal results for the vertex coloring problem, for many graph instances, shattering any intuition that this problem may have this algorithm as a solution. Of course, it does not demonstrate that there is no exact solution for the vertex-coloring problem in P, simply that our BFS-based algorithm is not the one.
 Of course, the world knows the vertex-coloring problem is NP-complete, and that there is perhaps (note the 'perhaps') little sense in pursuing an exact solution in P for this problem. These posts simply illustrate a larger point about how we can use approaches and tools to test the efficacy of any approximate algorithms that we may rig up for such problems. For example in future it may be interesting to find out the bounds on the suboptimality of the BFS-based algorithm, that is, how far from the optimal solution can it get, and whether there are bounds on this distance from optimality. This requires theoretical proof, but tools like Alloy can suggest directions and check hunches.

Is this stock advise worth taking seriously?

 Introduction Business related TV channels and newspapers are replete with stock advisory from investment firms and certified individua...