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 倍。