Monday Apr 02, 2012

Efficient inline templates and C++

I've talked before about calling inline templates from C++, I've also talked about calling inline templates efficiently. This time I want to talk about efficiently calling inline templates from C++.

The obvious starting point is that I need to declare the inline templates as being extern "C":

  extern "C"
  {
    int mytemplate(int);
  }

This enables us to call it, but the call may not be very efficient because the compiler will treat it as a function call, and may produce suboptimal code based on that premise. So we need to add the no_side_effect pragma:

  extern "C"
  {
    int mytemplate(int); 
    #pragma no_side_effect(mytemplate)
  }

However, this may still not produce optimal code. We've discussed how the no_side_effect pragma cannot be combined with exceptions, well we know that the code cannot produce exceptions, but the compiler doesn't know that. If we tell the compiler that information it may be able to produce even better code. We can do this by adding the "throw()" keyword to the template declaration:

  extern "C"
  {
    int mytemplate(int) throw(); 
    #pragma no_side_effect(mytemplate)
  }

The following is an example of how these changes might improve performance. We can take our previous example code and migrate it to C++, adding the use of a try...catch construct:

#include <iostream>

extern "C"
{
  int lzd(int);
  #pragma no_side_effect(lzd)
}

int a;
int c=0;

class myclass
{
  int routine();
};

int myclass::routine()
{
  try
  {
    for(a=0; a<1000; a++)
    {
      c=lzd(c);
    }
  }
  catch(...)
  {
    std::cout << "Something happened" << std::endl;
  }
 return 0;
}

Compiling this produces a slightly suboptimal code sequence in the hot loop:

$ CC -O -xtarget=T4 -S t.cpp t.il
...
/* 0x0014         23 */         lzd     %o0,%o0
/* 0x0018         21 */         add     %l6,1,%l6
/* 0x001c            */         cmp     %l6,1000
/* 0x0020            */         bl,pt   %icc,.L77000033
/* 0x0024         23 */         st      %o0,[%l7]

There's a store in the delay slot of the branch, so we're repeatedly storing data back to memory. If we change the function declaration to include "throw()", we get better code:

$ CC -O -xtarget=T4 -S t.cpp t.il
...
/* 0x0014         21 */         add     %i1,1,%i1
/* 0x0018         23 */         lzd     %o0,%o0
/* 0x001c         21 */         cmp     %i1,999
/* 0x0020            */         ble,pt  %icc,.L77000019
/* 0x0024            */         nop

The store has gone, but the code is still suboptimal - there's a nop in the delay slot rather than useful work. However, it's good enough for this example. The point I'm making is that the compiler produces the better code with both the "throw()" and the no side effect pragma.

Friday Jan 13, 2012

C++ and inline templates

A while back I wrote an article on using inline templates. It's a bit of a niche article as I would generally advise people to write in C/C++, and tune the compiler flags and source code until the compiler generates the code that they want to see.

However, one thing that I didn't mention in the article, it's implied but not stated, is that inline templates are defined as C functions. When used from C++ they need to be declared as extern "C", otherwise you get linker errors. Here's an example template:

.inline nothing
  nop
.end

And here's some code that calls it:

void nothing();

int main()
{
  nothing();
}

The code works when compiled as C, but not as C++:

$ cc i.c i.il
$ ./a.out
$ CC i.c i.il
Undefined                       first referenced
 symbol                             in file
void nothing()                   i.o
ld: fatal: Symbol referencing errors. No output written to a.out

To fix this, and make the code compilable with both C and C++ we use the __cplusplus feature test macro and conditionally include extern "C". Here's the modified source:

#ifdef __cplusplus
  extern "C"
  {
#endif
    void nothing();
#ifdef __cplusplus
  }
#endif

int main()
{
  nothing();
}

Tuesday Dec 23, 2008

Debugging inline templates with dbx

Been working on inline templates to improve the performance on a couple of hot routines in a customer code. I've a couple of articles on this kind of work if you want to find out more details. There's an introductory article which covers the rules, and there's an article specifically talking about using VIS instructions.

Anyway, one of the most important things to do is to write a test harness, it's very easy to make a mistake and have the template not work for some particular situation. For these routines, one of my colleagues had already written a test harness. I ended up extending it to try a different corner case, and at that point discovered that my code no longer validated. The problem turned out to be a branch that should have been branch >= 2 and I'd coded branch != 2. The original test cases terminated with the value 2 at this point, but the new test I added ended up with the value 1, which still should have terminated, but the inline template as written didn't handle it correctly.

So I fired up dbx to take a look at what was going on:

$ cc -g test.c test.il
$ dbx a.out
Reading a.out
Reading ld.so.1
Reading libc.so.1
(dbx) stop at 150
(dbx) run
stopped in main at line 150 in file "test.c"
  150           res1=campare(&buff1[j],buff2,i);

The stop at <line> command tells the debugger to stop at the problem line number (more details). However, the problem actually occurred when j was equal to 1. So I really should specify the break point better (more details).

(dbx) status
\*(2) stop at "mcmp-test-all.c":150
(dbx) delete 2
(dbx) stop at 150 -if j==1
(3) stop at "mcmp-test-all.c":150 -if j == 1
(dbx) run
Running: a.out
(process id 14983)

That got me to the point where the problem occurred. My initial thought was to step through the execution of the inline template using the nexti command. However, this is pretty inefficient:

(dbx) nexti
stopped in main at 0x00011cfc
0x00011cfc: main+0x1394:        sll      %l0, 1, %l1
(dbx) nexti
stopped in main at 0x00011d00
0x00011d00: main+0x1398:        add      %l3, %l1, %l0
(dbx) nexti
stopped in main at 0x00011d04
0x00011d04: main+0x139c:        ld       [%fp - 1044], %l1

It could take quite a large number of instructions before I actually encountered the problem code. Plus each step takes three lines on screen. However, there's a tracei command which traces the execution at the assembly code level (more details).

(dbx) tracei next
(dbx) cont
0x00011d08: main+0x13a0:        mov      %l0, %o0
0x00011d0c: main+0x13a4:        mov      %l2, %o1
0x00011d10: main+0x13a8:        mov      %l1, %o2
0x00011d14: main+0x13ac:        nop

The output took me through the code, and knowing the code path I had expected, I could pretty easily see the branch that caused the code to diverge.

Thursday May 15, 2008

Crossfile inlining and inline templates

Found an interesting 'feature' of using crossfile (-xipo) optimisation together with inline templates. Suppose you have a 'library' routine which is defined in one file and uses an inline template. This library routine is used all over the code. Here's an example of such a routine:

int T(int);
int W(int i)
{
 return T(i);
}

The routine W relies on an inline template (T) to do the work. The inline template contains some code like:

.inline T,0
  add  %o0,%o0,%o0
.end

The main routine resides in another file, and uses the routine W:

#include 
int W(int);
void main()
{
  printf("%i\\n",W(9));
}

To use inline templates you compile the file that contains the call to the inline template together with the inline template that it calls - like this:

$ cc -c -xO4 m.c
$ cc -c -xO4 w.c t.il
$ cc -xO4 m.o w.o

However, when crossfile optimisation (-xipo) is used, the routine W is inlined into main, and now main has a dependence on the inline template. But when m.o is recompiled after W has been inlined into main, the compiler cannot see the inline template for T because it was not present on the initial compile line for m.c. The result of this is an error like:

$ cc -c -xO4 -xipo m.c
$ cc -c -xO4 -xipo w.c t.il
$ cc -xO4 -xipo m.o w.o
Undefined                     first referenced
 symbol                             in file
T                                   m.o
ld: fatal: Symbol referencing errors. No output written to a.out

As you might guess from the above description, the workaround is not intuitive. You need to add the inline template to the initial compile of the file m.c:

$ cc -c -xO4 -xipo m.c t.il
$ cc -c -xO4 -xipo w.c t.il
$ cc -xO4 -xipo m.o w.o

It is not sufficient to add the inline template to the final compile line.

Looking beyond the simple test case shown above, the problem really is that when crossfile optimisation is used, the developer is no longer aware of the places in the code where inlining has happened (which is as it should be). So the developer can't know which initial compile lines to add the inline template to.

Hence, the conclusion is that whenever you are compiling code that relies on inline templates with crossfile optimisation, it is necessary to include the inline template on the compile line of every file.

About

Darryl Gove is a senior engineer in the Solaris Studio team, working on optimising applications and benchmarks for current and future processors. He is also the author of the books:
Multicore Application Programming
Solaris Application Programming
The Developer's Edge

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
5
6
8
9
10
12
13
14
15
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today
Bookmarks
The Developer's Edge
Solaris Application Programming
Publications
Webcasts
Presentations
OpenSPARC Book
Multicore Application Programming
Docs