1. 高精地图数据模型与内存布局
做高精地图系统这几年,我踩过最大的坑,就是内存爆炸。
记得有一次,我们的自动驾驶测试车在园区跑得好好的,突然地图加载卡死。排查了一整天,最后发现是某个路口的矢量要素在内存里膨胀了 40 倍。嗯,从那以后,我养成了一个习惯——写代码前先算清楚内存账。
今天我们就来聊聊,高精地图的三种核心数据模型——矢量、栅格、语义要素——在内存里到底长什么样。说白了,就是搞清楚你的数据是怎么躺在内存里的,以及为什么有的模型吃内存,有的模型省内存。
1.1 矢量数据模型:点线面的内存博弈
矢量数据,是高精地图的骨架。车道线、路沿、交通标志的位置,都是用矢量表达的。
你想想看,一个车道边界线,在内存里其实就是一串点坐标。每个点包含 x、y、z,如果是 double 精度,那就是 24 个字节。一条 100 米的车道线,采样间隔 0.5 米,就是 200 个点。光坐标就占了 4800 字节。
我建议你注意一个细节:内存对齐。C++ 里如果你用 struct 存点,编译器可能会在成员之间塞 padding。比如:
// 不推荐:内存浪费
struct Point3D {
double x;
double y;
double z;
int id; // 4字节,但后面会填充4字节
}; // 实际占用 32 字节,而不是 28
// 推荐:按 8 字节对齐排列
struct Point3D {
double x;
double y;
double z;
int id;
// 编译器自动填充4字节到 32
}; // 还是 32?不对,把 id 放前面试试
// 最优方案:大字段在前
struct Point3D {
int id; // 4字节
double x; // 8字节,从偏移8开始
double y;
double z;
}; // 实际占用 28 字节?不,还是 32,因为结构体整体对齐到最大成员
关键结论:矢量模型的内存占用 = 点数 × (坐标字节数 + 属性字节数) + 拓扑关系开销。拓扑关系(比如邻接链表)往往比坐标本身更吃内存。
我在项目中遇到过一种情况:用 shared_ptr 存点云数据,每个智能指针本身 16 字节,再加上引用计数和控制块,一个点凭空多出 24 字节开销。对于百万级点云,这就是 24MB 的浪费。后来我全部换成了 vector 连续存储,内存直接砍半。
1.2 栅格数据模型:分辨率与内存的生死抉择
栅格模型,说白了就是一张大图片。每个格子存一个值,比如高程、占用概率、语义标签。
它的内存公式很简单:宽度 × 高度 × 每个格子的字节数。
举个例子。一个 1km × 1km 的区域,用 10cm 分辨率,就是 10000 × 10000 = 1 亿个格子。每个格子存一个 float(4 字节),那就是 400MB。你想想看,如果车上同时加载 10 个这样的区域,4GB 内存就没了。
| 分辨率 | 区域大小 | 格子数 | float 内存 | uint8 内存 |
|---|---|---|---|---|
| 1m | 1km² | 1,000,000 | 4 MB | 1 MB |
| 10cm | 1km² | 100,000,000 | 400 MB | 100 MB |
| 5cm | 1km² | 400,000,000 | 1.6 GB | 400 MB |
我曾经犯过的错:在嵌入式平台上用 float 存栅格概率值,后来发现概率值范围 0~1,完全可以用 uint8 量化到 0~255。精度损失不到 0.4%,内存却省了 75%。
我个人习惯的做法是:分层存储。高频更新的层(比如动态障碍物)用低分辨率 + uint8,静态背景层(比如道路边界)用高分辨率 + 稀疏编码。别把所有数据塞进同一个栅格金字塔里。
1.3 语义要素模型:对象化的内存代价
语义要素,就是地图里的「对象」。比如一个红绿灯,它不是一个点,而是一个包含位置、朝向、灯组数量、相位信息、关联车道 ID 的复杂结构体。
这种模型的内存开销,主要来自三个方面:
- 对象头开销:虚函数表指针(vptr)在 64 位系统上占 8 字节,如果类有继承关系,每个对象都带一个 vptr
- 字符串开销:要素的 ID、类型名称,如果用 std::string,每个字符串至少 32 字节(SSO 优化后短字符串也在栈上占 32 字节)
- 关联关系:要素之间的引用(比如这个红绿灯控制哪几个车道),如果用 std::vector<ID> 存,每个 ID 8 字节,再加上 vector 本身的 24 字节开销
// 一个典型的语义要素内存布局
class TrafficLight {
// vptr: 8 字节(如果有虚函数)
uint64_t id_; // 8 字节
Point3D position_; // 24 字节(假设优化后)
float heading_; // 4 字节
uint8_t lamp_count_; // 1 字节
// padding: 3 字节
std::string type_; // 32 字节(短字符串)
std::vector<uint64_t> related_lanes_; // 24 字节 + 数据
// 总计:约 104 字节 + 动态数据
};
你想想看,一个城市级高精地图可能有几十万个语义要素。每个要素 100 字节,就是几十 MB。再加上矢量几何数据,轻松破 GB。
我的优化建议:用 SoA(Struct of Arrays) 替代 AoS(Array of Structs)。把所有要素的 ID 放一个数组,位置放另一个数组,类型放第三个数组。这样遍历时 CPU 缓存命中率更高,而且可以按需加载——比如只加载位置信息做碰撞检测时,不需要把字符串也读进来。
1.4 三种模型的对比与选型
说了这么多,到底什么时候用哪种模型?
我个人的经验是:
- 矢量模型:适合表达精确几何边界,比如车道线、路沿。内存中等,但拓扑关系复杂时开销大
- 栅格模型:适合表达连续场信息,比如高程、占用概率。内存与分辨率平方成正比,容易爆炸
- 语义模型:适合表达离散对象,比如红绿灯、标志牌。内存取决于对象数量和属性复杂度
实际项目中,往往是三种模型混合使用。我记得有一次做车道级导航,我们用了矢量存车道几何,栅格存路面坡度,语义存交通规则。结果发现栅格占了 70% 的内存,后来把坡度从 float 降成 uint16,分辨率从 10cm 降到 20cm,内存直接降了 60%。
核心原则:没有完美的数据模型,只有适合场景的取舍。做内存优化前,先搞清楚你的瓶颈在哪——是几何数据太多?还是属性字段太肥?还是关联关系太复杂?
下一章,我会详细讲怎么用内存池和自定义分配器,把矢量数据的分配开销降到接近零。到时候你会看到,同样的数据,换一种内存管理方式,性能能差出 10 倍。