序列化与反序列化之Flatbuffers(一):初步使用

2023-10-30

序列化与反序列化之Flatbuffers(一):初步使用

一: 前言

在MNN中, 一个训练好的静态模型是经过Flatbuffers序列化之后保存在硬盘中的. 这带来两个问题: 1.为什么模型信息要序列化不能直接保存 2.其他框架如caffe和onnx都是用Protobuf序列化, 为什么MNN要用Flatbuffers, 有啥优势? 在解答这两个问题之前, 我们先要了解什么是序列化和反序列化.

二: 什么是序列化和反序列化

什么是序列化和反序列化:

序列化是指把一个实例对象变成二进制内容,本质上就是一个byte[]数组。 为什么要把实例对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把实例对象存储到文件或者通过网络传输出去了。 有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回实例对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”实例对象,或者从网络上读取byte[]并把它“变回”实例对象

  • 序列化:把对象转换为字节序列的过程。
  • 反序列化:把字节序列恢复为对象的过程。

对象的序列化主要有两种用途:

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(持久化对象)
  • 在网络上传送对象的字节序列。(网络传输对象)

举例来说, 比如我用C++训练好一个模型, 然后在代码里是用一个类来描述这个模型的:

class net{
private:
    string name;
    vector<layer> layers;
    ...
}

我可以把这个类的指针指向的内存块整个保存到硬盘中么? 要恢复的时候直接load到内存中, 这样不是最快的嘛? 但是这样会引入几个问题

  • 我从32位机器保存的文件用64位机器打开就还原不了, 因为里面一些类型的sizeof不一样
  • 直接保存文件大小会比较大, 有些信息其实是可以进行压缩减小所需占用的硬盘空间
  • TODO:

总之没有人会这样做, 大家都是通过某种序列化协议, 将要保存的对象经过"某种转换"变成包含同样信息的不同形式保存到硬盘中或者进行传送.

三: 为什么要使用Flatbuffers

明白了保存文件时序列化的必要性后, 我们在选择序列化协议的时候主要考虑以下几点:

  • 协议是否支持跨平台
  • 序列化的速度
  • 序列化出来的大小

而Flatbuffers官网对自己的介绍是这样的:

  • Access to serialized data without parsing/unpacking
  • Memory efficiency and speed
  • Flexible
  • Tiny code footprint
  • Strongly typed
  • Convenient to use
  • Cross platform code with no dependencies

其实Flatbuffers与Protobuf相比有以下几个优势:

  • 最大的一个特点是序列化和反序列化速度更快. 这是由于Flatbuffers将数据序列化成二进制buffer,之后的数据反序列化的时候直接根据一些偏移信息读取这个buffer即可, 就是完善版的"我可以把这个类的指针指向的内存块整个保存到硬盘中么? 要恢复的时候直接load到内存中,这样不是最快的嘛?". 因此Flatbuffers经常用于游戏中与服务器频繁的通信, 但是感觉用于保存加载神经网络模型时, 相比于Protobuf应该优势不明显, 因为模型的load过程weight的访问占据了主要时间, 而反序列化模型的结构的时间减少应该对load过程加速不明显. 下次有空弄几个模型测一下.mark一下
  • 占用空间小, 使用简单, 适合移动端使用. Protobuf的头文件和库文件加起来有十几兆, 而Flatbuffers使用的时候只需要include一个头文件即可, 更加省空间. 同时简易程度简直是新手福音
  • 再加一个两者都有的优点. 代码的自动化生成. 编写一个fbs或者proto文件来描述需要管理的对象结构, 就可以一行命令生成所有对应的cpp类代码, 十分易于管理和修改. 可以节省很多头发. 我觉得这点才是这些开源神经网络框架使用Protobuf和Flatbuffers的最重要的原因吧.

四: 如何使用Flatbuffers

下面的重点是用于个人记录如何使用Flatbuffers来描述一个神经网络模型(其实就是MNN的方案)以及如何用C++代码进行序列化保存和反序列化读取. 本文的相关代码已经上传到仓库中, 欢迎使用和star
详细内容强烈推荐阅读官方文档

4.1 安装

git clone https://github.com/google/flatbuffers
cd flatbuffers
mkdir build
cd build
cmake .. && cmake --build . --target flatc -- -j4

最终在目录flatbuffers/build下得到一个flatc的可执行文件即可. 安装过程一气呵成, 对比Protobuf的版本问题和一堆依赖库问题, 简直不要太舒服

4.2 编写fbs

使用Flatbuffers和Protobuf很相似, 都会用到先定义一个schema文件, 用于定义我们要序列化的数据结构的组织关系. 下面我们以描述一个极简神经网络模型PiNet为例(没错, 就是浓缩版的MNN), 介绍Flatbuffers常用结构int, string, enum, union, vector, table的使用方法

namespace PiNet;//命名空间千万不能与内部的对象重名!!!!!!

table Pool {
    padX:int;
    padY:int;
    // ...
}
table Conv {
    kernelX:int = 1;
    kernelY:int = 1;
    // ...
}
union OpParameter {
    Conv,
    Pool,
}
enum OpType : int {
    Conv,
    Pool,
}
table Op {
    type: OpType;
    parameter: OpParameter;
    name: string;
    inputIndexes: [int];
    outputIndexes: [int];
}
table Net {
    oplists: [Op];
    tensorName: [string];
}
root_type Net;

我们的根类型是Net代表一个神经网络模型, 是net是table类型, 该类型应该是最常用的类型, 类似Python中的字典, 冒号左边是名key, 右边是数据类型value. []代表是数组vector. 由此可见一个Net包含多个层Op, 多个tensor. 然后我们重点看Op, 用一个enum来代表Op的类型, 以及一个union来表示该Op的parameter. enum的概念在C/C++中也有, 这里也是一样的, 就是内存空间复用. 而union其实就是非int的enum的, 这里是一个table的enum. 对每种Op的parameter都定义了一个table来描述, Conv层这里只包含了两个参数kernelX和kernelY. 其他都好理解, 这里稍微需要注意的是这个union概念的理解.

4.3 产生generated.h文件

/flatbuffers/build/flatc net.fbs --cpp --binary --reflect-names --gen-object-api --gen-compare

将编写好的fbs文件转换成可用的.h文件. 命令中–gen-object-api表示.h文件中会产生方便使用的xxT类. --gen-compare表示.h文件中每个类都会产生Operator==方法, 用于比较各对象是否相等.
我们来大致看一下产生的.h文件中有哪些内容.

enum OpType : int32_t {
  OpType_Conv = 0,
  OpType_Pool = 1,
  OpType_MIN = OpType_Conv,
  OpType_MAX = OpType_Pool
};

fbs中的enum就转换成了C/C++的enum, 这个好理解不用多说

struct Op;
struct OpBuilder;
struct OpT;

fbs中定义的table结构都变成了struct, 以Op为例, 产生了3个结构, 其中Op是用于描述序列化后的Op对象, OpT是用于描述未序列化的Op对象, 这个XXT是需要我们在编译的时候加上选项–gen-object-api才会产生

struct OpT : public flatbuffers::NativeTable {
  typedef Op TableType;
  PiNet::OpType type = PiNet::OpType_Conv;
  PiNet::OpParameterUnion parameter{};
  std::string name{};
  std::vector<int32_t> inputIndexes{};
  std::vector<int32_t> outputIndexes{};
};

OpT的结构其实就是我们想要描述Op的样子. 由此可见, 使用Flatbuffers的一大好处就是方便, 只需要用fbs几行描述好即可自动产生对应的类对象代码. 抛开序列化不说, 光这代码自动生成就够让人心动了, 简直是懒人福音

struct Op FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
  enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
    VT_TYPE = 4,
    VT_PARAMETER_TYPE = 6,
    VT_PARAMETER = 8,
    VT_NAME = 10,
    VT_INPUTINDEXES = 12,
    VT_OUTPUTINDEXES = 14
  };
  PiNet::OpType type() const ;
  PiNet::OpParameter parameter_type() const ;
  const void *parameter() const ;
  template<typename T> const T *parameter_as() const;
  const PiNet::Conv *parameter_as_Conv() const;
  const PiNet::Pool *parameter_as_Pool() const;
  const flatbuffers::String *name() const ;
  const flatbuffers::Vector<int32_t> *inputIndexes() const ;
  const flatbuffers::Vector<int32_t> *outputIndexes() const ;
  bool Verify(flatbuffers::Verifier &verifier) const ;
  OpT *UnPack() const;
  void UnPackTo() const;
  static flatbuffers::Offset<Op> Pack();
};

再看Op这个结构, Op描述的是序列化后的对象, 成员函数中主要是包含了从Op直接访问成员变量的方法(其实就是反序列化), 还有一个VTableOffset的enum, 这个我们下篇详解的时候会用到, 这里暂且不表. 另外还包含了两个Pack和UnPack方法, 顾名思义, 这就是序列化和反序列化方法. UnPack可以将序列化后的对象Op转换成未序列化的对象OpT, Pack可以将OpT转换成Op.

struct OpParameterUnion {
  OpParameter type;
  void *value;

  static void *UnPack();
  flatbuffers::Offset<void> Pack() const;

  PiNet::ConvT *AsConv() {
    return type == OpParameter_Conv ?
      reinterpret_cast<PiNet::ConvT *>(value) : nullptr;
  }
  const PiNet::ConvT *AsConv() const {
    return type == OpParameter_Conv ?
      reinterpret_cast<const PiNet::ConvT *>(value) : nullptr;
  }
  PiNet::PoolT *AsPool() {
    return type == OpParameter_Pool ?
      reinterpret_cast<PiNet::PoolT *>(value) : nullptr;
  }
  const PiNet::PoolT *AsPool() const {
    return type == OpParameter_Pool ?
      reinterpret_cast<const PiNet::PoolT *>(value) : nullptr;
  }
};

再看Union, 包含了两个成员变量, 一个描述类型, 一个存放数据指针. 对于一个实例化的Union, 想要得到其代表的数据, 需要根据类型手动执行相应的AsXXX函数进行cast. 刚才Op的结构里包含的几个函数parameter_type(), parameter(), parameter_as_Conv(), parameter_as_Pool()就是封装了这个Union的成员函数

bool operator==(const PoolT &lhs, const PoolT &rhs);
bool operator!=(const PoolT &lhs, const PoolT &rhs);
bool operator==(const ConvT &lhs, const ConvT &rhs);
bool operator!=(const ConvT &lhs, const ConvT &rhs);
bool operator==(const OpT &lhs, const OpT &rhs);
bool operator!=(const OpT &lhs, const OpT &rhs);
bool operator==(const NetT &lhs, const NetT &rhs);
bool operator!=(const NetT &lhs, const NetT &rhs);

编译选项里开了–gen-compare后就会自动产生这些比较操作符重载代码, 这里再一次展示出了Flatbuffers的方便特性, 试想一下, 如果我有100种Op参数都要手动去写比较操作符重载, 那岂不是得累死.

4.4 序列化代码

#include <fstream>
#include <iostream>

#include "net_generated.h"
using namespace PiNet;

int main() {
    flatbuffers::FlatBufferBuilder builder(1024);

    // table ConvT
    auto ConvT = new PiNet::ConvT;
    ConvT->kernelX = 3;
    ConvT->kernelY = 3;
    // union ConvUnionOpParameter
    OpParameterUnion ConvUnionOpParameter;
    ConvUnionOpParameter.type = OpParameter_Conv;
    ConvUnionOpParameter.value = ConvT;
    // table OpT
    auto ConvTableOpt = new PiNet::OpT;
    ConvTableOpt->name = "Conv";
    ConvTableOpt->inputIndexes = {0};
    ConvTableOpt->outputIndexes = {1};
    ConvTableOpt->type = OpType_Conv;
    ConvTableOpt->parameter = ConvUnionOpParameter;

    // table PoolT
    auto PoolT = new PiNet::PoolT;
    PoolT->padX = 3;
    PoolT->padY = 3;
    // union OpParameterUnion
    OpParameterUnion PoolUnionOpParameter;
    PoolUnionOpParameter.type = OpParameter_Pool;
    PoolUnionOpParameter.value = PoolT;
    // table Opt
    auto PoolTableOpt = new PiNet::OpT;
    PoolTableOpt->name = "Pool";
    PoolTableOpt->inputIndexes = {1};
    PoolTableOpt->outputIndexes = {2};
    PoolTableOpt->type = OpType_Pool;
    PoolTableOpt->parameter = PoolUnionOpParameter;

    // table NetT
    auto netT = new PiNet::NetT;
    netT->oplists.emplace_back(ConvTableOpt);
    netT->oplists.emplace_back(PoolTableOpt);
    netT->tensorName = {"conv_in", "conv_out", "pool_out"};
    netT->outputName = {"pool_out"};
    // table Net
    auto net = CreateNet(builder, netT);
    builder.Finish(net);

    // This must be called after `Finish()`.
    uint8_t* buf = builder.GetBufferPointer();
    int size = builder.GetSize();  // Returns the size of the buffer that
                                   //`GetBufferPointer()` points to.
    std::ofstream output("net.mnn", std::ofstream::binary);
    output.write((const char*)buf, size);

    return 0;
}

由于我们开启了–gen-object-api选项会产生XXT的结构, 我们只需要对各个层次的数据结构进行赋值即可, 最后只要对根节点进行一次Create即完成序列化, 很简单方便. 相比于官网的创建monster那样每个层次的数据都要Create序列化一下, 代码结构能精简不少.

4.5 反序列化

#include <fstream>
#include <iostream>
#include <vector>

#include "net_generated.h"
using namespace PiNet;

int main() {
    std::ifstream infile;
    infile.open("net.mnn", std::ios::binary | std::ios::in);
    infile.seekg(0, std::ios::end);
    int length = infile.tellg();
    infile.seekg(0, std::ios::beg);
    char* buffer_pointer = new char[length];
    infile.read(buffer_pointer, length);
    infile.close();

    auto net = GetNet(buffer_pointer);

    auto ConvOp = net->oplists()->Get(0);
    auto ConvOpT = ConvOp->UnPack();

    auto PoolOp = net->oplists()->Get(1);
    auto PoolOpT = PoolOp->UnPack();

    auto inputIndexes = ConvOpT->inputIndexes;
    auto outputIndexes = ConvOpT->outputIndexes;
    auto type = ConvOpT->type;
    std::cout << "inputIndexes: " << inputIndexes[0] << std::endl;
    std::cout << "outputIndexes: " << outputIndexes[0] << std::endl;

    PiNet::OpParameterUnion OpParameterUnion = ConvOpT->parameter;
    switch (OpParameterUnion.type) {
        case OpParameter_Conv: {
            auto ConvOpParameterUnion = OpParameterUnion.AsConv();
            auto k = ConvOpParameterUnion->kernelX;
            std::cout << "ConvOpParameterUnion, k: " << k << std::endl;
            break;
        }
        case OpParameter_Pool: {
            auto PoolOpParameterUnion = OpParameterUnion.AsPool();
            auto k = PoolOpParameterUnion->padX;
            std::cout << "PoolOpParameterUnion, k: " << k << std::endl;
            break;
        }
        default:
            break;
    }
    return 0;
}

五: 总结

Flatbuffers的使用还是挺简单的, 理解常见的几种数据类型, 再把官网那个monster的例子看几遍就还挺好懂的. 本篇只是初步使用, 下一篇我们再做深入剖析.

序列化与反序列化之Flatbuffers(二):深入剖析

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

序列化与反序列化之Flatbuffers(一):初步使用 的相关文章

  • 大数据用户画像实战之业务数据调研及ETL

    整个用户画像 UserProfile 项目中 数据 业务及技术流程图如下所示 其中数据源存储在业务系统数据库 MySQL 数据库中 采用SQOOP全量 增量将数据抽取到 HDFS Hive表中 通过转换为HFile文件加载到HBase表 1

随机推荐

  • 主线程中捕获子线程异常

    需求 主线程独立执行 无需等待子线程执行完毕 子线程如有异常抛出可自行catch 网上介绍的方法一般是 1 在线程内部进行try catch捕获异常 2 通过线程池的submit方法 获取Future对象 然后try catch Futur
  • rz 传输错误问题 的几种解决方案

    在使用rz传输文件的时候 会遇到传输错误的问题 如下图 情况1 目录不对 一般情况下 切换到 tmp 目录 先将文件传输到 tmp 下 之后再复制到目标位置 情况2 硬盘空间不足 segmentation fault 使用df h查看磁盘空
  • go语言开发环境的搭建-安装和配置SDK

    一 基本介绍 1 SDK全称 software development kit 软件开发工具包 2 SDK是提供给开发人员使用的 其中包含了对应的开发语言的工具包 运行的工具 开发的工具 以及开发所需要的api 这是把我们的源代码编译成二进
  • 记mysql-connector-java:8.0.28的bug排查,你可能也踩坑了

    前言 如标题 最终查明问题是因为 mysql connector java 8 0 28 的一个 bug 导致的 但是在真相未浮出之前 整个问题可谓扑朔迷离 博主好久没有排查过如此得劲的 bug 随着一层层的 debug 深入 真相也随之浮
  • PCB布线线宽和过孔孔径设置为多少合适?

    作者 李大闯 2017 08 19 22 16 对于很多新入行的人来说 不清楚PCB的线宽应该设置为多少 这里作一下解释 对于PCB布线线宽的设置 主要要考虑两个问题 一是流过的电流大小 比如对于电源线来说 需要考虑电路工作时流过的电流 如
  • ios开发App的图标背景色不能是透明

    在ios开发中 App的图标背景不能是透明的 否则打包出来以后 在界面上的显示会是一个带有把黑色背景的图标 换成白色背景后
  • C++ 解析Json——jsoncpp

    C 解析Json
  • SKU 模块丨前端uniapp微信小程序项目

    小兔鲜儿 SKU 模块 学会使用插件市场 下载并使用 SKU 组件 实现商品详情页规格展示和交互 存货单位 SKU SKU 概念 存货单位 Stock Keeping Unit 库存管理的最小可用单元 通常称为 单品 SKU 常见于电商领域
  • 【2023】java多线程——锁的使用及分类

    锁简介 java多线程的锁都是基于对象的 每个对象都可以作为一个锁 类锁也是对象锁 java6以后 一个对象分为了4种锁的状态 级别由低到高依次是 无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态 java对象头 每个Java对象都有对象头
  • SpringBoot 如何使用 @RequestBody 进行数据校验

    SpringBoot 如何使用 RequestBody 进行数据校验 在 Web 开发中 前台向后台发送数据是非常常见的场景 而在 SpringBoot 框架中 我们通常使用 RequestBody 注解来接收前台发送的 JSON 数据 并
  • PDF 的各种操作,我用 Python 来实现(附网站和操作指导)

    导言 PDF 处理是日常工作中的常见需求 包括 PDF 合并 删除 提取等 更复杂的任务如 将 PDF 转换成 图像 下面通过几个简单的例子和一份代码 帮助大家解决上面的需求 操作非常简单 在文末我会提供一份源码和一个神奇的 PDF 处理网
  • outside of class is not definition

    有一种可能的情况 You have semicolons at the end of all your function definitions making the compiler think they re declarations
  • 解决Base64报java.lang.IllegalArgumentException: Illegal base64 character 20

    报错 java lang IllegalArgumentException Illegal base64 character 20 原因 base64编码时使用加号 在URL传递时加号会被当成空格让base64字符串更改 服务器端解码出错
  • ROS主从机配置,并实现远程登陆

    第一步 主从机配置 首先确保主从机在同一个局域网中 1 编辑主机的bashrc文件 机器人平台 gedit bashrc 主机的bashrc文件添加如下的内容 export ROS MASTER URI http 主机的ip 11311 e
  • stm32F4 IAP实现原理讲解以及中断向量表的偏移

    一 IAP原理 IAP即是在应用编程 IAP 是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写 目的是为了在产品发布后可以方便地通过预留的通信口对产 品中的固件程序进行更新升级 通常实现IAP 功能时 即用户程序运行中
  • 生命在于磨炼——连续两年参加4C大赛心得

    一 4C大赛简介 1 大赛简介 中国大学生计算机设计大赛 下面简称 大赛 是由教育部高等学校计算机类专业教学指导委员会 教育部高等学校软件工程专业教学指导委员会 教育部高等学校大学计算机课程教学指导委员会 教育部高等学校文科计算机基础教学指
  • 操作系统笔记五(Linux存储管理)

    1 Buddy内存管理算法 内部碎片就是已经被分配出去 能明确指出属于哪个进程 却不能被利用的内存空间 外部碎片指的是还没有被分配出去 不属于任何进程 但由于太小了无法分配给申请内存空间的新进程的内存空闲区域 目的 努力让内存分配与相邻内存
  • Task2_MySQL_basic

    MySQL表数据类型 用SQL语句创建表 创建MySQL数据表需要以下信息 表名 表字段名 定义每个表字段 语句解释 设定列类型 大小 约束 设定主键 用SQL语句向表中添加数据 语句解释 多种添加方式 指定列名 不指定列名 用SQL语句删
  • Ubuntu16.04下搭建LAMP环境

    Ubuntu16 04下搭建LAMP环境 Ubuntu16 04下搭建LAMP环境 1 安装 Apache2 2 重启 apache2 3 测试apache2是否安装成功 4 安装php7 5 测试php是否安装成功 6 安装mysql数据
  • 序列化与反序列化之Flatbuffers(一):初步使用

    序列化与反序列化之Flatbuffers 一 初步使用 一 前言 在MNN中 一个训练好的静态模型是经过Flatbuffers序列化之后保存在硬盘中的 这带来两个问题 1 为什么模型信息要序列化不能直接保存 2 其他框架如caffe和onn