Skip to content

Commit

Permalink
feat: translate cmake series
Browse files Browse the repository at this point in the history
  • Loading branch information
xizhibei committed May 10, 2024
1 parent 8b6bf2e commit 7c2b72a
Show file tree
Hide file tree
Showing 12 changed files with 1,514 additions and 2 deletions.
154 changes: 154 additions & 0 deletions source/_posts/en/cmake-1-introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
title: (CMake Series) Part 1 - Introduction
date: 2020-03-09 20:09:15
categories: [CMake]
tags: [C/C++,CMake,Build Systems,Tutorial]
author: xizhibei
issue_link: https://github.com/xizhibei/blog/issues/133
---
<!-- en_title: cmake-1-introduction -->

Since the last time I picked up C++ in [Why the Order of C++ Static Libraries Matters](https://github.com/xizhibei/blog/issues/100), I've embarked on a point of no return.

Today, let's talk about CMake, a powerful tool for modern C++ projects.

### Introduction

Why do we need CMake? For C++ developers, they are accustomed to using GNU Make to compile C++ projects, which is fine for simple projects. Furthermore, there are so many open-source tools available, especially the autotools series, which are quite convenient. Many C++ projects still require you to use `./configure` to preprocess before compiling, which is much more convenient than using Make alone, thus very friendly to beginners (not to mention advanced users, as not many can make Vim fly).

Personally, I find the autotools series less tidy (I really dislike it when I see a bunch of temporary files generated in the current directory; conversely, CMake's dedicated build directory is a godsend for my OCD). Since I haven’t used it much, I can't comment further or make detailed comparisons. Those interested can also check out the discussion on [What are the differences between Autotools, Cmake, and Scons?](https://stackoverflow.com/questions/4071880/what-are-the-differences-between-autotools-cmake-and-scons).

In terms of accessibility, CMake is a blessing for beginners, although the tricky part is that you need to learn a new language, which is still very simple compared to programming languages.

### Preparation

In most cases, you can just install it directly with one of the following commands depending on your machine:

```bash
brew install cmake
sudo apt install cmake
pip install cmake
```

Alternatively, you can directly download and install it from the [official website](https://cmake.org/download/).

### A Simple Example

Let's start with the simplest project.

```cpp
// main.cpp
#include <stdio.h>

int main(int argc, char *argv[])
{
printf("Hello world");
return 0;
}
```
You can then create a CMakeLists.txt in the current directory:
```cmake
# Minimum version of CMake required, you can also set a version range, such as 3.1...3.15
cmake_minimum_required (VERSION 3.0)
# Project information, version number, and description can be set
project (demo VERSION 0.1.0 DESCRIPTION "Demo project")
# Create an executable named demo
add_executable(demo main.cpp)
```

Next, execute the classic four commands to compile the executable:

```bash
mkdir build
cd build
cmake ..
make
```

Unlike Make, CMake greatly simplifies the configuration process. With just a few lines, you can write powerful cross-platform compilation scripts. You can see from the output that a series of steps have been automated:

```
-- The C compiler identification is GNU 7.4.0
-- The CXX compiler identification is GNU 7.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/xizhibei/demo/build
```

Simple, right? Let's try a more complex example.

### A Slightly More Complex Example

As you add more features to your project, you'll need to update your CMakeLists.txt.

Suppose your project structure looks like this:

```
--- root
|-- CMakeLists.txt
|-- main.cpp
|-- include/
|------ a.h
|------ b.h
|-- lib/
|------ a.cpp
|------ b.cpp
```

Corresponding changes are as follows:

```cmake
cmake_minimum_required (VERSION 3.0)
project (demo VERSION 0.1.0 DESCRIPTION "Demo project")
# Add a static library called demo_lib
add_library(demo_lib STATIC lib/a.cpp lib/b.cpp)
# Specify the location of header files
target_include_directories(demo_lib PUBLIC ${CMAKE_SOURCE_DIR}/include)
# As above
add_executable(demo main.cpp)
# No need to set target_include_directories again since we have already set includes for demo_lib, CMake will automatically add them
# Link the demo_lib library to the demo executable
target_link_libraries(demo demo_lib)
```

Following a more standard approach, we should create a new CMakeLists.txt under the lib directory and include it with `add_subdirectory(lib)`. However, this approach is used here to save space.

Thus, a simple C++ project is complete.

### Other

We've completed a simple example, but how do we add third-party dependencies? How do we add compilation options? How do we test? How do we add documentation?

Don't worry, I'll gradually explain these in a lengthy series.

### Ref

1. <http://www.cmake.org/>

***
First published on Github issues: https://github.com/xizhibei/blog/issues/133, welcome to Star and Watch

{% post_link footer_en %}
***
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
title: "(CMake Series) Part 10 - Enhancing Compilation Speed and Program Performance"
date: 2020-07-29 17:31:23
categories: [CMake]
tags: [C/C++, CMake, Performance Optimization]
author: xizhibei
issue_link: https://github.com/xizhibei/blog/issues/145
---
<!-- en_title: cmake-10-refine-compile-speed-and-program-performance -->

Every C/C++ developer knows that as projects grow, compilation speed can start to drag, a love-hate situation: it tests our patience and drives us crazy when we're eager to modify code, yet it provides a perfect excuse for a break to walk or sip some tea when we need a pause.

Additionally, the runtime speed of the program is another critical factor—slow compile times are bearable, but slow execution is not, not just for us but for our bosses and users too.

That said, for our own development efficiency, enhancing compilation speed is indisputably beneficial. Today, let’s discuss how to optimize both compilation and program performance within CMake.

### Compilation Speed Optimization

#### Ninja Generator

The default generator in CMake is Unix Makefiles, which uses the common `make` command. However, the Ninja generator is a better choice. If you haven't used it, I recommend giving it a try.

#### CCache

The simplest and most effective tool for speeding up compilation is enabling a compilation cache, and `ccache` is the tool we need.

Its principle is straightforward: it wraps the compiler, receives compilation parameters and files, and when it detects that there's no corresponding cache, it calls the compiler to store the output in a file. If the compilation parameters and files haven’t changed, it retrieves the output directly from the cache file, greatly reducing recompilation time.

In earlier versions of CMake (before 2.8 for Unix Makefiles and 3.4 for Ninja), there was no support for ccache, and we had to manually set `CMAKE_C_COMPILER` and `CMAKE_CXX_COMPILER` to prefix with ccache:

```cmake
set(CMAKE_C_COMPILER ccache gcc)
set(CMAKE_CXX_COMPILER ccache g++)
```

In newer versions, it's even easier:

```cmake
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
message(STATUS "Setting up ccache ...")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
endif()
```

Then, refer to [Using ccache with CMake][1] for the XCode generator.

You might wonder how effective it is. Below are the results I got from running `ccache -s` on one machine:

```
cache directory /home/me/.ccache
primary config /home/me/.ccache/ccache.conf
secondary config (readonly) /etc/ccache.conf
stats zero time Tue Apr 7 16:06:27 2020
cache hit (direct) 41056
cache hit (preprocessed) 7179
cache miss 25047
cache hit rate 65.82 %
called for link 10928
called for preprocessing 5929
compile failed 3055
preprocessor error 1325
can't use precompiled header 86
bad compiler arguments 56
autoconf compile/link 3428
no input file 616
cleanups performed 90
files in cache 25512
cache size 4.5 GB
max cache size 5.0 GB
```

In the past few months, it saved me approximately 65.82% of compilation time.

#### Precompiled headers (PCH) and Unity builds<sup>[2]</sup>

**Precompiled headers**: These significantly reduce the repetitive compilation time of C++ header files. You can add headers of some third-party libraries, like nlohmann/json, spdlog/spdlog.h, Boost, and rarely changed project headers into precompilation:

```cmake
target_precompile_headers(<my_target> PRIVATE my_pch.h)
```

However, the intermediate files generated will be quite large, taking up significant disk space.

**Unity builds**: As the name suggests, this involves merging multiple CPP files into a single compilation unit. This reduces the number of times the compiler needs to parse, optimizes the same templates, reduces the number of compiler invocations, and makes the linker more efficient.

It’s simple to use:

```cmake
set(CMAKE_UNITY_BUILD ON)
```

```cmake
set_target_properties(<target> PROPERTIES UNITY_BUILD ON)
```

However, these techniques are advanced and not suitable for all projects as they might **increase project maintenance costs**. If used incorrectly, they may lead to compilation failures.

#### Other Tips

- Switching from gcc to clang;
- Changing from static to dynamic linking;
- Upgrading to a high-performance machine with a better CPU and SSD, or even using a RAM disk (these are cases of trading money for performance but are very effective).

### Program Performance Optimization

For CMake, the simplest optimization is switching from Debug to Release mode.

Additionally, consider [Interprocedural optimization](https://en.wikipedia.org/wiki/Interprocedural_optimization), which can be understood as a program-level Release mode because the ordinary Release mode optimizes at the individual file level.

Not every compiler supports it, so you need to check first:

```cmake
include(CheckIPOSupported)
check_ipo_supported(RESULT _IsIPOSupported)
if(_IsIPOSupported)
message(STATUS "Turning on INTERPROCEDURAL_OPTIMIZATION")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
```

In terms of performance optimization, compilers have limited capabilities; more often, optimization should occur during the coding phase of the entire program.

Issues such as business logic, algorithms, and waiting times are not within the compiler's capabilities to resolve, yet they are crucial in impacting the final outcome.

### Ref

1. [Using ccache with CMake][1]
2. [CMake 3.16 added support for precompiled headers & unity builds - what you need to know][2]

[1]: https://crascit.com/2016/04/09/using-ccache-with-cmake/

[2]: https://onqtam.com/programming/2019-12-20-pch-unity-cmake-3-16/


***
First published on GitHub issues: https://github.com/xizhibei/blog/issues/145, welcome to Star and Watch

{% post_link footer_en %}
***
102 changes: 102 additions & 0 deletions source/_posts/en/cmake-2-third-party-dependances-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: "(CMake Series) Part 2 - Third-Party Dependency Management"
date: 2020-03-15 23:18:50
categories: [CMake]
tags: [C/C++, CMake, Build Systems, Dependency Management]
author: xizhibei
issue_link: https://github.com/xizhibei/blog/issues/134
---
<!-- en_title: cmake-2-third-party-dependency-management -->

Continuing from the last [(CMake Series) Part 1 - Introduction](/en/2020/03/09/cmake-1-introduction/).

This time, let's talk about dependency management in CMake.

### Dependency Management

When we talk about dependency management in CMake, we are often referring to the management of dependencies in C/C++ projects. However, this ancient language still lacks a unified official dependency management tool to date.

Looking at its successors, Ruby has `gem`, Node.js has `npm`, Golang has `go mod`, and Rust has `cargo`.

You might mention that C++ introduced Modules in C++20, but currently, [compiler support](https://en.cppreference.com/w/cpp/compiler_support) is still insufficient, not to mention support from famous C++ projects. If in the future it becomes as simple as modern languages to install all dependencies with one command and use them directly, it would be greatly beneficial.

So, what kind of support does CMake provide? Essentially, projects supported by CMake usually provide a file named `xxx-config.cmake` or `xxxConfig.cmake`. Their role is to provide the means to find and incorporate them into the current project for use.

### CMake `find_package`

Let's see what it looks like:

```cmake
find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
[REQUIRED] [[COMPONENTS] [components...]]
[OPTIONAL_COMPONENTS components...]
[NO_POLICY_SCOPE])
```

Thus, if you need to include the OpenCV dependency, you need to write the following line before compiling your project files:

```cmake
find_package(OpenCV 3.4.9 REQUIRED)
```

This line means that you need to find the OpenCV dependency with version `3.4.9` (version compatibility logic is controlled by the package itself if not `EXACT`), and it is mandatory, which is indicated by `REQUIRED`. Of course, if this dependency is not crucial, you can use the `QUIET` keyword.

Then at the point of use:

```cmake
target_link_libraries(lib PUBLIC ${OpenCV_LIBS})
```

Note the `PUBLIC` keyword here, which means that if other libraries or executable programs depend on lib, you don't need to declare the need for OpenCV related libraries again, CMake will automatically add the dependency. Also, if you are using CMake version 3.5 or below, you also need to do this:

```cmake
target_include_directories(lib PUBLIC ${OpenCV_INCLUDE_DIRS})
```

Moreover, if your found dependency requires sub-module dependencies, like Boost, you would need to use `COMPONENTS` or `OPTIONAL_COMPONENTS`:

```cmake
find_package(Boost 1.50 REQUIRED COMPONENTS filesystem)
target_link_libraries(lib PUBLIC Boost::filesystem)
```

### Third-Party Dependency Management

Now, the question arises, since I can use `find_package` to manage packages so conveniently, where do these packages come from?

Friend, that's the key. If you're developing on a local machine, some can be installed directly (and there are also some cross-compilation libraries, which can be searched to confirm), such as on Ubuntu, if you need OpenCV, simply `sudo apt install libopencv-dev`; and on MacOS, `brew install opencv`.

If you are cross-compiling, or if the current development library version in your system does not meet your requirements, then you will need to compile from source.

At this point, the question arises, how should the source code be placed in our development project? Just copy it in? For those who have read my previous article on [how to clone a large Git project](https://github.com/xizhibei/blog/issues/131), you would know not to make this mistake.

So, what should be done? It's simple, use `git submodule`.

You can place the respective code repository in a specific folder, such as `extern` or `third_party`, etc.

Then you can add it with a command like this: `git submodule add https://github.com/opencv/opencv -b 3.4 extern/opencv`.

For users, they need to use `git submodule update --init --recursive`, or add the `--recursive` parameter when cloning.

Then, you need to add extra configuration steps to your CMakeLists.txt. If the project's CMakeLists.txt supports being used as a module, you can directly add it as a subdirectory: `add_subdirectory(extern/opencv)`.

I have tried this method, but encountered insurmountable cross-compilation issues at the time, so I gave up, as some projects' CMakeLists.txt are not standard, do not support being used as submodules of other projects, and not all projects support CMake. Of course, you can also modify by writing patches for the project or by writing specialized bash scripts to solve these issues.

Here's a prerequisite, it requires the user to know the usage plan of `git submodule`. If you do not like this method, you have three other options:

1. ExternalProject: Runs during build, cannot use `add_subdirectory` at configuration time.
2. [DownloadProject](https://github.com/Crascit/DownloadProject): Runs at configuration time, can `add_subdirectory`;
3. FetchContent (CMake 3.11+): The official version of the second solution, simpler but only available in higher versions, considering most machines have lower versions of CMake, it may require users to upgrade to use;

Currently, in our team projects, we are using ExternalProject, because **as a separate step, it allows pre-compiling libraries dependent on various platforms, saving unnecessary time for other team members.**

I've made the third-party dependencies compilable with one line of command, so every time it can be quickly compiled on a server with high-performance CPU, then the build artifacts are directly saved in a public storage space, and later others can directly download and use them, of course, the artifacts are signed for verification.

Moreover, even the downloading step can be done by CMake during the configuration stage, greatly improving the team's development efficiency.


***
Originally published on Github issues: https://github.com/xizhibei/blog/issues/134, feel free to Star and Watch

{% post_link footer_en %}
***
Loading

0 comments on commit 7c2b72a

Please sign in to comment.