前言
-
我们曾经在这篇文章中提出了一个实时无需编译反复读取文件就可以修改cpp参数的参数服务器设计: 基于yaml-cpp的C++参数服务器设计
- 不需要重新编译程序
- 不需要重启程序
- 修改配置文件即可实时生效
- 使用简单
-
但是上述设计存在部分弊端,随着参数量的上升,上一版的设计要求设置的
yaml文件的参数只存在于同一级,因此当参数量越来越多的时候会很难维护与阅读
yaml
drone_pid_x_p: 1.0
drone_pid_x_i: 1.0
drone_pid_x_d: 1.0
drone_pid_y_p: 1.0
drone_pid_y_i: 1.0
drone_pid_y_d: 1.0
drone_pid_z_p: 1.0
drone_pid_z_i: 1.0
drone_pid_z_d: 1.0
- 因此本次的修改考虑引入
yaml多级参数的设计,当程序可以直接读取多级参数的配置,达到和之前一样的效果:
yaml
drone:
pid:
x:
kp: 0.12
ki: 0.0
kd: 0.0
y:
kp: 0.1
ki: 0.0
kd: 0.0
z:
kp: 0.1
ki: 0.0
kd: 0.0
1 实现
1-1 问题分析
- 除了要保持和之前版本一样的需求,还需要考虑多级菜单的引入不能丢失原本高性能的需求,因此每次读取参数时候的设计必须丢弃这样的设计:
cpp
root["robot"]["pid"]["kp"]
- 这种方式存在两个问题:
- 每次访问都需要多次 map 查找
- 随着层级增加,访问复杂度线性上升
- PS:设计的时候需要注意不要暴露
YAML::Node的接口,用户无需关心其中具体实现。
1-2 解决思路
- 为保证访问效率,本方案采用扁平化存储结构(Flattening):
cpp
"robot.pid.kp" -> 0.3
"robot.pid.ki" -> 0.3
"robot.pid.kd" -> 0.3
- 在加载 YAML 时,将树结构递归展开为:
cpp
unordered_map<string, YAML::Node>
- 这样最终单次访问可以退化为
O(1)哈希查找
1-3 API接口
1-3-1 参数读取
- 保持原有接口:
cpp
template<typename T>
T getParameter(const std::string& key, const T& defaultValue);
1-3-2 存在性检查(新增)
- 新增接口, 用于:
- 防止默认值掩盖错误
- 配置校验
- 调试阶段检查
cpp
bool hasParameter(const std::string& key);
1-3-3 分组访问(新增)
- 支持按前缀获取子参数用于:
- 动态模块加载
- 遍历 PID / 控制轴
- 插件式配置结构
cpp
std::vector<std::string> getChildren(const std::string& prefix);
- 例如:
cpp
getChildren("drone.pid")
-
返回:
x, y, z
1-3-4 参数列表(新增)
- 用于调试与可视化:
cpp
std::vector<std::string> listParameters();
1-3-5 复杂度说明
| 操作 | 复杂度 |
|---|---|
| getParameter | O(1) |
| hasParameter | O(1) |
| listParameters | O(N) |
| getChildren | O(N) |
1-4 具体实现
ParamsServerUltra.hpp
cpp
#pragma once
#include <yaml-cpp/yaml.h>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <filesystem>
#include <unordered_map>
#include <mutex>
#include <algorithm>
#include <unordered_set>
class ParamsServerUltra {
public:
// 单例
static ParamsServerUltra* getInstance(std::string file_path) {
static ParamsServerUltra* instance =
new ParamsServerUltra(file_path);
return instance;
}
ParamsServerUltra(const ParamsServerUltra&) = delete;
ParamsServerUltra& operator=(const ParamsServerUltra&) = delete;
template<typename T>
T getParameter(const std::string& key, const T& defaultValue) {
std::lock_guard<std::mutex> lock(mutex_);
try {
checkReload();
auto it = flatParams_.find(key);
if (it == flatParams_.end()) {
std::cerr<< "[ParamsServerUltra][WARN] "<< "Parameter not found: " << key << ", using default value: " << defaultValue << std::endl;
return defaultValue;
}
return it->second.as<T>();
} catch (const std::exception& e) {
std::cerr << "[ParamsServerUltra] error: " << e.what() << std::endl;
}
return defaultValue;
}
bool hasParameter(const std::string& key)
{
std::lock_guard<std::mutex> lock(mutex_);
checkReload();
return flatParams_.find(key) != flatParams_.end();
}
std::vector<std::string> listParameters()
{
std::lock_guard<std::mutex> lock(mutex_);
checkReload();
std::vector<std::string> result;
result.reserve(flatParams_.size());
for (const auto& [key, value] : flatParams_)
{
result.push_back(key);
}
std::sort(result.begin(),result.end());
return result;
}
std::vector<std::string>getChildren(const std::string& prefix)
{
std::lock_guard<std::mutex> lock(mutex_);
checkReload();
std::unordered_set<std::string> unique;
std::string searchPrefix = prefix.empty()? "": prefix + ".";
for (const auto& [key, value] : flatParams_)
{
if (key.rfind(searchPrefix, 0) != 0)
{
continue;
}
std::string remain =key.substr(searchPrefix.size());
size_t pos =remain.find('.');
if (pos != std::string::npos)
{
unique.insert(remain.substr(0, pos));
}
else
{
unique.insert(remain);
}
}
return { unique.begin(), unique.end()};
}
// 修改 YAML 路径
void setYamlFilePath(const std::string& newPath) {
std::lock_guard<std::mutex> lock(mutex_);
filename_ = newPath;
loadParameters();
}
private:
// 构造
ParamsServerUltra(const std::string& filename)
: filename_(filename) {
loadParameters();
}
// reload 检测
void checkReload() {
auto t = std::filesystem::last_write_time(filename_);
if (t > lastLoadTime_) {
loadParameters();
}
}
// 加载 YAML + flatten
void loadParameters() {
try {
std::ifstream file(filename_);
if (!file.is_open()) {
std::cerr << "[ParamsServerUltra] Cannot open file: "
<< filename_ << std::endl;
return;
}
YAML::Node root = YAML::Load(file);
std::unordered_map<std::string, YAML::Node> newMap;
flatten(root, "", newMap);
flatParams_.swap(newMap);
lastLoadTime_ = std::filesystem::last_write_time(filename_);
std::cout << "[ParamsServerUltra] reloaded & flattened: "
<< flatParams_.size() << " params\n";
} catch (const YAML::Exception& e) {
std::cerr << "[ParamsServerUltra] parse error: " << e.what() << std::endl;
}
}
// YAML flatten
void flatten(const YAML::Node& node,
const std::string& prefix,
std::unordered_map<std::string, YAML::Node>& out)
{
if (node.IsMap()) {
for (auto it : node) {
std::string key = it.first.as<std::string>();
std::string fullKey = prefix.empty()
? key
: prefix + "." + key;
flatten(it.second, fullKey, out);
}
}
else if (node.IsSequence()) {
int idx = 0;
for (auto it : node) {
std::string fullKey = prefix + "[" + std::to_string(idx++) + "]";
flatten(it, fullKey, out);
}
}
else {
out[prefix] = node;
}
}
private:
std::string filename_;
std::unordered_map<std::string, YAML::Node> flatParams_;
std::filesystem::file_time_type lastLoadTime_;
std::mutex mutex_;
};
2 测试
2-1 参数文件 config.yaml
yaml
drone:
pid:
x:
kp: 0.1
ki: 0.0
kd: 0.001
y:
kp: 0.12
ki: 0.0
kd: 0.003
odom:
enable_t265: true
2-2 正常使用测试
cpp
#include "../include/test_pkg/ParamsServerUltra.hpp"
#include <iostream>
#include <thread>
#include <chrono>
int main()
{
auto* params_server =ParamsServerUltra::getInstance("src/test_pkg/params/config.yaml");
while (true)
{
bool enable_t265 =params_server->getParameter<bool>("drone.odom.enable_t265",true);
std::cout<< "enable_t265 = "<< enable_t265<< std::endl;
// 打印全部参数
for (const auto& key : params_server->listParameters())
{
std::cout << key << std::endl;
}
// 获取 pid 轴
auto axes =params_server->getChildren("drone.pid");
for (const auto& axis : axes)
{
double kp = params_server->getParameter<double>("drone.pid." + axis + ".kp",0.0);
double ki =params_server->getParameter<double>("drone.pid." + axis + ".ki",0.0);
double kd = params_server->getParameter<double>("drone.pid." + axis + ".kd",0.0);
std::cout << axis << ": " << kp << ", " << ki << ", " << kd << std::endl;
}
std::cout << "==============" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(200));
}
return 0;
}

2-3 单次访问测速
cpp
#include "../include/test_pkg/ParamsServerUltra.hpp"
#include <iostream>
#include <chrono>
int main()
{
auto* params =
ParamsServerUltra::getInstance(
"src/test_pkg/params/config.yaml");
// 预热(避免第一次 load + page fault 干扰)
for (int i = 0; i < 1000; i++)
{
params->getParameter<double>("drone.pid.x.kp",0.0);
}
const int N = 1000000;
auto start = std::chrono::high_resolution_clock::now();
double sum = 0.0;
for (int i = 0; i < N; i++)
{
sum += params->getParameter<double>("drone.pid.x.kp",0.0);
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);
double avg_ns =duration.count() / (double)N;
std::cout << "Total: " << duration.count()<< " ns\n";
std::cout << "Average: "<< avg_ns << " ns/op\n";
std::cout << "sum = " << sum << std::endl;
return 0;
}

- 也就是
0.000788823 ms
总结
- 本文通过对 YAML 多级结构进行扁平化设计,在保留可读性的同时,将参数访问统一收敛为 O(1) 哈希查询,从而实现了高性能、支持热更新的 C++ 参数服务器。
- 如有错误,欢迎指出!
- 感谢观看!
