When building a program in C, C++, or other compiled languages, one typically invokes a compiler or compiler driver (like g++ or clang) with a list of command-line options. These options (often referred to as flags) tell the compiler how to compile (and sometimes link) your code.
I stumbled upon many of these compiling/linking commands through my software engineering career, and this blogpost kind of reflects my current understanding of these concepts, from both a theoretical and practical viewpoints.
In this post, we’ll cover:
nvccfor instance).Compiler flags control different aspects of the compilation process.
Typical flags include:
I: Adds a directory to the compiler’s search path for header files.L: Adds a directory to the linker’s search path for libraries.l<name>: Links against a library named lib<name>.so (on Linux), lib<name>.dylib (on macOS), or lib<name>.a (static library).O3, O2, O0: Controls compilation optimization levels (O3 for advanced optimization, O0 for none).g: Generates debugging symbols for use with debuggers like gdb.fPIC: Generates Position-Independent Code (required for shared libraries, e.g. .so files).shared: Builds a shared library (e.g. .so on Linux).std=c++11 (or later standards like c++14, c++17): Specifies the C++ standard to use.fopenmp: Enables OpenMP support for multithreading.Xcompiler or Xlinker (in nvcc): Passes flags directly to the host compiler or linker.These flags provide precise control over how your code is parsed, optimized, and linked.
Compilation:
.c, .cpp, etc.) into object files (.o, .obj).Linking:
a.out) or library (e.g. .so, .dll).Examples of Flag Usage:
I, std=c++11, fPIC, O3) instruct the compiler on parsing, header search paths, optimizations, etc.L, l) tell the linker where to find and which libraries to link against.fopenmp affect both the compilation and linking stages, adding necessary libraries during linking.Note on Linker Flag Order:
The order of libraries in the linker command can sometimes matter, particularly with static libraries. If unresolved symbols appear, adjusting the order might resolve them.
g++ CommandCompilation:
g++ -I/usr/local/include -O3 -std=c++11 -c myfile.cpp -o myfile.o
I/usr/local/include: Look for header files in /usr/local/include.O3 and std=c++11: Enable advanced optimization and specify the C++ standard.c: Compile only (do not link). The output is myfile.o.Linking:
g++ myfile.o -L/usr/local/lib -lm -o myapp
L/usr/local/lib: Look for libraries in /usr/local/lib.lm: Link against the math library (libm.so on Linux).o myapp: Output an executable named myapp.You can also combine these steps:
g++ -I/usr/local/include -O3 -std=c++11 myfile.cpp -L/usr/local/lib -lm -o myapp
Under the hood, the compiler first compiles, then links, all in one invocation.
A linker is a program that resolves references between object files and external libraries. For example, if your code calls sqrt() from the math library, the linker locates where sqrt is defined (in libm.so on Linux, .so for shared objects) and links it into the final executable.
When you pass flags like -L/usr/local/lib -lm, you’re telling the linker to:
L/usr/local/lib: Search /usr/local/lib for libraries.lm: Link against the math library (locating libm.so, libm.a, etc ….).L and l?L<dir>: Adds <dir> to the linker’s search directories.l<name>: Tells the linker to look for a file named lib<name>.so, lib<name>.dylib, or lib<name>.a.Example:
g++ main.o -L/home/me/mylibs -lfoo -o main
The linker checks /home/me/mylibs for libfoo.so (shared) or libfoo.a (static) and includes it in the final executable.
Static vs. Shared Libraries:
While the discussion above focuses on shared libraries (using flags like
-sharedand-fPIC), static libraries are linked differently. For static libraries, the linker includes the library code directly into the executable, and the flags used (-l<name>) point to.afiles instead of.soor.dylib.
Here’s a very simplified overview of the compilation process:
#include and #define (for code in C/C++ for instance), expanding macros and including header files.O2 or O3..o or .obj) containing machine code and unresolved symbols.Compile:
g++ -fPIC -Iinclude -O3 -std=c++17 -c mylibrary.cpp -o mylibrary.o
fPIC: Required for generating position-independent code for shared libraries.Link:
g++ -shared -o libmylibrary.so mylibrary.o
shared: Produces a .so shared object.Using the Shared Library:
g++ main.cpp -Iinclude -L. -lmylibrary -o main
Iinclude: Look for header files.L.: Look in the current directory for libraries.lmylibrary: Link against libmylibrary.so.g++ -fopenmp -O3 main.cpp -o main
fopenmp: Enables both compiler and linker support for OpenMP, automatically linking with the OpenMP runtime (e.g., libgomp).clang++ -std=c++14 -stdlib=libc++ -I/usr/local/include -L/usr/local/lib main.cpp -o myapp -lc++
libc++ vs. libstdc++), so specifying stdlib=libc++ and linking with lc++ is useful..lib files).When compiling CUDA code, you typically use nvcc, NVIDIA’s compiler driver. Under the hood, nvcc calls the host compiler (like gcc or clang) for CPU code.
Basic CUDA Compilation:
nvcc -I/path/to/cuda/include -c mykernel.cu -o mykernel.o
More Complex Example:
nvcc -arch=sm_75 \\
-I/usr/local/cuda/include \\
-I/usr/local/include \\
-L/usr/local/cuda/lib64 \\
-lcudart \\
-Xcompiler -fPIC \\
-O3 -o mycudaapp main.cu
arch=sm_75: Specifies the target GPU architecture (e.g., NVIDIA Turing).I/usr/local/cuda/include: Specifies the directory for CUDA headers (e.g cuda_runtime.h).L/usr/local/cuda/lib64 & lcudart: Directs the linker to the CUDA runtime library (libcudart.so).Xcompiler -fPIC: Passes fPIC to the host compiler.O3: Applies optimization.Headers (.h, .hpp)
Found via -I paths.
Libraries (libsomething.so, libsomething.a)
Found via -L paths and included with -lsomething.
Optimization and Standards
Controlled via flags like -O2, -O3, and -std=c++17.
Position-Independent Code (PIC)
Required for shared libraries via -fPIC.
Shared Library Output
Created with -shared.
OpenMP
Enabled with -fopenmp, with consideration for dual-phase (compilation and linking) behavior.
CUDA
Managed by nvcc alongside host compiler flags, targeting specific GPU architectures with -arch=sm_xx.
Understanding compiler and linker flags is crucial for building functional and optimized software. Here’s what you need to remember:
L and l) determine how external libraries are located and included.nvcc orchestrating the process.By carefully controlling these flags, you can ensure your project is both robust and efficient.