Performance
and Convenience Trade-Offs in Source Code and Build Structures
The structure of the source code for an application can cause
differences to its perform-ance. Source code is often distributed across source
files for the convenience of the developers. It is appropriate that the
developers’ convenience is one of the main criteria for structuring the
sources, but care needs to be taken that it does not cause inconven-ience to
the user of an application.
Performance opportunities are lost when the compiler sees only a single
file at a time. The single file may not present the compiler with all the
opportunities for optimizations that it might have had if it were to see more
of the source code. This kind of limitation is visible when a program uses an
accessor function—a short function that returns the value of some variable. A
trivial optimization is for the compiler to replace this function call with a
direct load of the value of the variable. Consider the code sequence shown in
Listing 2.5 for an example of accessor functions.
Listing 2.5 Accessor
Functions
#include <stdio.h>
int a;
void setvalue( int v ) { a = v; }
int getvalue() { return a; }
void main()
{
setvalue( 3 );
printf( "The value of a is %i\n", getvalue() );
}
The code in Listing 2.5 can be replaced with the equivalent but faster
code shown in Listing 2.6. This is an example of inlining within a source file.
The calls to the routines getvalue() and
setvalue() are replaced by the actual code
from the functions.
Listing 2.6 Pseudosource
Code After Inlining Optimization
#include <stdio.h>
int a;
void main()
{
a = 3;
printf( "The value of a is %i\n", a );
}
At some optimization level, most compilers support inlining within the
same source file. Hence, the transformation in Listing 2.6 is relatively
straightforward for the compiler to perform. The problem is when the functions
are distributed across multiple source files. Fortunately, most compilers
support cross-file optimization where they take all the source code and examine
whether it is possible to further improve performance by inlining a routine
from one source file into the place where it’s called in another source file.
This can negate much of any performance loss incurred by the structure used to
store the source code. Cross-file optimization will be discussed in more detail
in the sec-tion “How Cross-File Optimization Can Be Used to Improve
Performance.”
Some
build methodologies reduce the ability of the compiler to perform optimizations
across source files. One common approach to building is to use either static or
archive libraries as part of the build process. These libraries combine a
number of object files into a single library, and at link time, the linker
extracts the relevant code from the library.
Listing
2.7 shows the steps in this process. In this case, two source files are
combined into a single archive, and that archive is used to produce the
application.
Listing 2.7 Creating
an Archive Library
$
cc -c a.c
$
cc -c b.c
$
ar -r lib.a a.o b.o
ar:
creating lib.a
$
cc main.c lib.a
There are three common reasons for using static libraries as part of the
build process:
n For
“aesthetic” purposes, in that the final linking of the application requires
fewer objects. The build process appears to be cleaner because many individual
object files are combined into static libraries, and the smaller set appears on
the link line. The libraries might also represent bundles of functionality
provided to the executable.
n To
produce a similar build process whether the application is built to use static
or dynamic libraries. Each library can be provided as either a static or a
dynamic ver-sion, and it is up to the developer to decide which they will use.
This is common when the library is distributed as a product for developers to
use.
n To hide
build issues, but this is the least satisfactory reason. For example, an
archive library can contain multiple versions of the same routine. At link
time, the linker will extract the first version of this routine that it
encounters, but it will not warn that there are multiple versions present. If
the same code was linked using individ-ual object files without having first
combined the object files into an archive, then the linker would fail to link
the executable.
Listing 2.8 demonstrates how using static libraries can hide problems
with multiply defined functions. The source files a.c and b.c both contain a function status(). When they are combined into an archive and linked into an executable,
the linker will extract one of the two definitions of the function. In the
example, the linker selects the definition from a.c. However, the build fails with a multiply defined symbol error if an
attempt is made to directly link the object files into an executable.
Listing 2.8 Example of a Static Library Hiding Build Issues
$ more a.c
#include <stdio.h> void status()
{
printf("In status of A\n");
}
$ more b.c
#include <stdio.h> void status()
{
printf("In status of B\n");
}
$ cc -c a.c
$ cc -c b.c
$ ar -r
lib.a a.o b.o ar: creating lib.a
$ more
main.c void status(); void main()
{
status();
}
$ cc main.c lib.a $ a.out
In status of A
$ cc main.c a.o b.o
ld: fatal: symbol 'status' is
multiply-defined: (file a.o type=FUNC; file b.o type=FUNC);
ld: fatal: File processing errors. No output
written to a.out
An unfortunate side effect of using static libraries is that some
compilers are unable to perform cross-file optimization of code contained in
the static libraries. This may mean that functions are not inlined from the static
library into the executable or that the code held in the static library does
not take any part in cross-file optimization.
Related Topics
Privacy Policy, Terms and Conditions, DMCA Policy and Compliant
Copyright © 2018-2023 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.