Post

c++-third-party-library-compilation-optimization

spdlognlohmann/json这样的仅标头(header-only)库因其易用性和便捷性而广受欢迎,但它们在大型项目中可能会带来编译时间和生成文件体积膨胀的问题。以下是针对这些库的优化策略:

1. 分离编译(Explicit Template Instantiation)

对于像nlohmann/json这样的模板库,编译时间和二进制体积膨胀的问题可以通过显式实例化模板来缓解。

nlohmann/json分离编译示例:

  1. 声明模板类(头文件):
    1
    2
    3
    4
    5
    6
    7
    8
    
    // json_wrapper.h
    #include <nlohmann/json.hpp>
       
    class JsonWrapper {
    public:
        using json = nlohmann::json;
        static json parse(const std::string& data);
    };
    
  2. 定义模板类(源文件):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // json_wrapper.cpp
    #include "json_wrapper.h"
       
    nlohmann::json JsonWrapper::parse(const std::string& data) {
        return nlohmann::json::parse(data);
    }
       
    // Explicit template instantiation for common types
    template nlohmann::json JsonWrapper::parse<int>(const std::string&);
    template nlohmann::json JsonWrapper::parse<double>(const std::string&);
    

    通过将常用的模板实例化放在一个实现文件中,你可以减少头文件的编译开销,并减少重复代码的实例化。

2. 减少包含头文件的范围

使用前置声明: 尽量减少头文件的包含范围。在不需要完整定义的情况下,使用前置声明。

spdlog使用前置声明示例:

1
2
3
4
5
6
// 只需要指针或引用的地方使用前置声明
namespace spdlog {
    class logger;
}

void log_message(spdlog::logger* logger, const std::string& message);

在实现文件中再包含spdlog的头文件:

1
2
3
4
5
#include <spdlog/spdlog.h>

void log_message(spdlog::logger* logger, const std::string& message) {
    logger->info(message);
}

3. 使用预编译头文件(Precompiled Headers, PCH)

spdlognlohmann/json等库的头文件放入预编译头文件中,可以大大减少它们在多个文件中重复编译的时间。

1
2
3
// pch.h
#include <spdlog/spdlog.h>
#include <nlohmann/json.hpp>

然后在编译器中设置使用预编译头文件:

1
2
3
4
5
# GCC/Clang
g++ -o myapp myapp.cpp -include pch.h

# MSVC
cl /Yu"pch.h" myapp.cpp

4. 优化头文件内容

减少不必要的头文件包含: 在头文件中,只包含必须的内容,将所有非必要的包含放到实现文件中。

1
2
3
4
// Avoid including full headers
class JsonWrapper;  // Forward declaration

void process_json(const JsonWrapper& json);

5. 模块化编译(C++20 Modules)

C++20的模块可以显著减少编译时间和二进制体积。虽然spdlognlohmann/json目前可能没有完全支持C++20模块,但你可以自己尝试将它们封装成模块。

1
2
3
4
5
6
// json_module.cpp
module;  // global module fragment
#include <nlohmann/json.hpp>

export module json_module;
export using json = nlohmann::json;

然后你可以在其他地方直接导入这个模块。

1
2
3
import json_module;

json j = json::parse(R"({"key": "value"})");

6. 条件编译和宏优化

在项目中使用条件编译来包含不同的功能模块,避免引入不必要的代码。

1
2
3
// config.h
#define USE_SPDLOG
#define USE_JSON
1
2
3
4
5
6
7
8
9
10
// logger.cpp
#ifdef USE_SPDLOG
#include <spdlog/spdlog.h>
#endif

void log_message(const std::string& message) {
    #ifdef USE_SPDLOG
    spdlog::info(message);
    #endif
}

7. 使用工具进行分析和优化

  • 编译时间分析: 使用GCC的-ftime-report或Clang的-ftime-trace来分析编译时间,找出编译时间长的部分,进行有针对性的优化。
  • 二进制体积分析: 使用工具如Bloaty McBloatface来分析生成的二进制文件,找出导致体积膨胀的部分。

8. 链接时优化

使用编译器链接时优化(Link Time Optimization, LTO)来减少二进制文件大小和运行时性能开销。

1
2
# GCC/Clang
g++ -flto -o myapp myapp.cpp

总结

通过分离编译、前置声明、预编译头文件、模块化编译、条件编译和使用工具进行分析和优化,你可以有效地解决仅标头库带来的编译时间和二进制体积膨胀问题。这些技术可以帮助你在保持开发效率的同时,优化项目的性能和资源利用率。

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.