yaska
yaska

Reputation: 225

When a C library is statically linked does the whole library get added to the executable?

Background info: I am trying to compare the memory requirements for two pieces of code which perform some numerical computations. For this, I am comparing the size of compiled C codes with the math library statically linked.

However, I am finding some strange results which seem to indicate that the whole library is being added. I'm describing an MWE below

// Program ex1.c
# include<math.h>
void main (void)
{
    float a = exp(2);

}

And

// Program ex2.c
# include <math.h>
void main(void)
{
    float a = exp(2);
    float b = pow(3,4);
    float c = sin(3.14159);
}

I compile the files as follows:

gcc -static -o ex1static.out ex1.c -lm
gcc -static -o ex2static.out ex2.c -lm

If the compiled object for program 1 contained the code only for exp() and that for compiled object for program 2 contained the code for exp(), pow() and sin(), then the second one would be larger than the first. But both objects have the same size of 912.6 kB.

Why is this happening and is there any way to make sure that only the required parts of code get added to the objects?

Upvotes: 4

Views: 1323

Answers (1)

Petr Skocik
Petr Skocik

Reputation: 60185

Static libs are archives of object files, and linking in a static lib adds only those object file members of the archive that resolve at least one undefined reference.

To make sure only the required code gets added, the static lib needs to be made up of small object files, preferably with one exported global in each.

Other than that, you can achieve a similar effect if the library is compiled with -ffunction-sections/-fdata-sections and you then pass --gc-sections to the linker.

The -ffunction-sections -fdata-sections approach is basically equivalent to one-global-per-source approach, but using source files to establish the boundaries is more flexible as sometimes grouping things together may be desirable (larger translation units may lead to more compact and more optimized code).

Anyway, in your case (the lib isn't under your control), all you can try is -Wl,--gc-sections (the -Wl option to gcc prefixes what gcc should pass to the linker) With your example and glibc, I was able to shed about about 41KiB from an original 849KiB.

Not very impressive, but glibc isn't build with static linking in mind anyway. You can get much better results with a libc library that is, such as musl-libc.

for ex in ex{1,2}.c; do for flg in '' -Wl,--gc-sections; do echo "$ex $flg"; musl-gcc -O0 $ex -static -lm $flg call.c && \ls -l a.out ; done ; done
ex1.c 
-rwxrwx--- 1 pjmp pjmp 8064 Jun 29 19:11 a.out
ex1.c -Wl,--gc-sections
-rwxrwx--- 1 pjmp pjmp 7744 Jun 29 19:11 a.out
ex2.c 
-rwxrwx--- 1 pjmp pjmp 8064 Jun 29 19:11 a.out
ex2.c -Wl,--gc-sections
-rwxrwx--- 1 pjmp pjmp 7744 Jun 29 19:11 a.out

Now this is better, but you may be wondering why same sizes for example 1 and 2.

If you add -Wl,--print-map, you'll find that the relevant object files from musl-libc aren't being included at all in either case. The reason is, gcc knows about these standard functions and it cheats by inserting opcodes instead of generated function calls. You can somewhat defeat gcc's cheating by adding a layer of indirection facilitated by another translation unit.

call.c:

double call1(double(*X)(double A), double A) { return X(A); }
double call2(double(*X)(double A,double B), double A, double B){ return X(A,B); }

Ex1.c

# include<math.h>
double call1(double(*X)(double A), double A);
double call2(double(*X)(double A,double B), double A, double B);
int main (void)
{
    float a = call1(exp,2);
}

Ex2.c

# include <math.h>
double call1(double(*X)(double A), double A);
double call2(double(*X)(double A,double B), double A, double B);
int main(void)
{
    float a = call1(exp,(2));
    float b = call2(pow,3,4);
    float c = call1(sin,(3.14159));
}

Now this gives me:

Ex1.c 
-rwxrwx--- 1 pjmp pjmp 8216 Jun 29 19:15 a.out
Ex1.c -Wl,--gc-sections
-rwxrwx--- 1 pjmp pjmp 7984 Jun 29 19:15 a.out
Ex2.c 
-rwxrwx--- 1 pjmp pjmp 17088 Jun 29 19:15 a.out
Ex2.c -Wl,--gc-sections
-rwxrwx--- 1 pjmp pjmp 16856 Jun 29 19:15 a.out

—a noticable difference between the two examples, which is possible thanks to how musl is made up of many small source/object files so that no more (or not much more) than the relevant referenced code gets added when linking statically.

Upvotes: 3

Related Questions