Protobuf-源码中值得学习的地方

本文深入探讨了Protobuf源码中的一些优秀实践,包括使用宏提高代码可读性,如CHARACTER_CLASS和PROTOBUF_DEFINE_ACCESSOR等;资源分配的lazy机制,如DescriptorPool的分层设计和GeneratedMessageFactory的延迟加载;内存复用策略,如RepeatedPtrFieldBase如何实现message的clear与复用;以及封装多种类型和模版参数的处理方式。通过对这些设计的学习,可以提升代码质量和效率。

使用宏来提高代码可读性(代码的美感)

例1.宏CHARACTER_CLASS

定义

tokenizer.cc文件中,需要判断某个character是属于哪种类型的字符,通过宏CHARACTER_CLASS来定义字符类型,并且定义static类型的InClass()接口来判断。

    #define CHARACTER_CLASS(NAME, EXPRESSION)      \
      class NAME {                                 \
       public:                                     \
        static inline bool InClass(char c) {       \
          return EXPRESSION;                       \
        }                                          \
      }

    CHARACTER_CLASS(Whitespace, c == ' ' || c == '\n' || c == '\t' ||
                                c == '\r' || c == '\v' || c == '\f');

    CHARACTER_CLASS(Unprintable, c < ' ' && c > '\0');

    CHARACTER_CLASS(Digit, '0' <= c && c <= '9');
    CHARACTER_CLASS(OctalDigit, '0' <= c && c <= '7');
    CHARACTER_CLASS(HexDigit, ('0' <= c && c <= '9') ||
                              ('a' <= c && c <= 'f') ||
                              ('A' <= c && c <= 'F'));

    CHARACTER_CLASS(Letter, ('a' <= c && c <= 'z') ||
                            ('A' <= c && c <= 'Z') ||
                            (c == '_'));

    CHARACTER_CLASS(Alphanumeric, ('a' <= c && c <= 'z') ||
                                  ('A' <= c && c <= 'Z') ||
                                  ('0' <= c && c <= '9') ||
                                  (c == '_'));

    CHARACTER_CLASS(Escape, c == 'a' || c == 'b' || c == 'f' || c == 'n' ||
                            c == 'r' || c == 't' || c == 'v' || c == '\\' ||
                            c == '?' || c == '\'' || c == '\"');

    #undef CHARACTER_CLASS
使用

使用时可以直接使用InClass():

    template<typename CharacterClass>
    inline bool Tokenizer::TryConsumeOne() {
      if (CharacterClass::InClass(current_char_)) {
        NextChar();
        return true;
      } else {
        return false;
      }
   }

例2.宏 PROTOBUF_DEFINE_ACCESSOR

FieldDescriptor类,因为可能类型是多样的,在实现对外暴露default数据的函数时,为了提高代码可读性,使用了如下宏的方式(文件descriptor.cc中):

定义
    // These macros makes this repetitive code more readable.
    #define PROTOBUF_DEFINE_ACCESSOR(CLASS, FIELD, TYPE) \
      inline TYPE CLASS::FIELD() const { return FIELD##_; }
使用
 PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_int32 , int32 )
 PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, has_default_value, bool)

例3.宏BUILD_ARRAY

定义

BUILD_ARRAY宏定义如下,这里的INPUT是proto;OUTPUT是proto对应的descriptor;NAME是需要完成创建的成员;METHOD是创建descriptor成员时需要调用的函数;PARENT是发生嵌套时的上一级。

    // A common pattern:  We want to convert a repeated field in the descriptor
    // to an array of values, calling some method to build each value.
    #define BUILD_ARRAY(INPUT, OUTPUT, NAME, METHOD, PARENT)             \
      OUTPUT->NAME##_count_ = INPUT.NAME##_size();                       \
      AllocateArray(INPUT.NAME##_size(), &OUTPUT->NAME##s_);             \
      for (int i = 0; i < INPUT.NAME##_size(); i++) {                    \
        METHOD(INPUT.NAME(i), PARENT, OUTPUT->NAME##s_ + i);             \
     }
使用

DescriptorBuilder::BuildFile()中,利用FileDescriptorProto& proto来构建对应的descriptor:

  // Convert children.
  BUILD_ARRAY(proto, result, message_type, BuildMessage  , NULL);
  BUILD_ARRAY(proto, result, enum_type   , BuildEnum     , NULL);
  BUILD_ARRAY(proto, result, service     , BuildService  , NULL);
  BUILD_ARRAY(proto, result, extension   , BuildExtension, NULL);

说明

各个Descriptor类中,使用count + 连续内存来保存成员,例如:

    class LIBPROTOBUF_EXPORT FileDescriptor {

        //省略其它代码

        int message_type_count_;
        Descriptor* message_types_;

       //省略其它代码

   }

资源分配/处理的lazy机制

例1.类DescriptorPool数据分层设计

DescriptorPool的数据管理分为了多层(忽略了仅在protobuf内部使用 && 不推荐使用的underlay一层):

  • 最顶层:DescriptorPool::Tables tables_,保存name->descriptor;
  • 最底层:DescriptorDatabase* fallback_database_,保存name->file_descriptor_proto(而不是直接的file_descriptor)

查找时,如果第一层tables_没找到,最终会到fallback_database_中找对应proto,并且调用临时构造的DescriptorBuilder::Build*()系列接口把生成的descriptor添加到tables_中,然后再从tables_中找。

这样数据分层设计的目的是:

  1. 用于定制地(on-demand)从某种”大”的database加载产生DescriptorPool。因为database太大,逐个调用DescriptorPool::BuildFile() 来处理原database中的每一个proto文件是低效的。
    为了提升效率,使用DescriptorPool来封装DescriptorDatabase,并且只建立正真需要的descriptor。
  2. 针对编译依赖的每个proto文件,并不是在进程启动时,直接构建出proto中所包含的所有descriptor,而是hang on,直到某个descriptor真的被需要:
    (1)用户调用例如descriptor(), GetDescriptor(), GetReflection()的方法,需要返回descriptor;
    (2)用户从DescriptorPool::generated_pool()中查找descriptor;

可以看到descriptor的构建是hang-on的,只有需要使用某个descriptor时,才构建。适合依赖了很多的proto文件,但仅仅使用其中的少数proto的场景。

例2.类GeneratedMessageFactory映射关系加载

GeneratedMessageFactory类,管理的从Descriptor* -> Message*映射关系,并不是一开始就注册好的。仅仅在需要从descriptr查找message时(调用GeneratedMessageFactory::GetPrototype()),才会:

  1. 通过file_name找到注册函数;
  2. 调用注册函数,完成Descriptor* -> Message*映射关系的注册;
  3. hash_map<const Descriptor*, const Message*> type_map_查找到对应Message*返回;

资源管理/内存复用

类RepeatedPtrFieldBase

RepeatedPtrFields的父类(不是模板类,提供了多个模板函数),本身保存/管理的数据类型为void*(message对象的实际地址,也是通过连续内存array来保存)。

因为array中保存的是同一个descriptor对应的message,只是各个message中所包含的数据不一样,为了节省下message对象分配/释放的成本,所以message可以被clear(clear操作会将primitive类型的field设置为0,其余类型field调用自身的clear()接口处理,类似std::string::clear(),只清理数据并不回收内存)。
然后保留原有的内存地址在array中。下次需要从array中分配message时,优先使用这一批被clear的message(实现在RepeatedPtrFieldBase::AddFromCleared() ,参考GeneratedMessageReflection::AddMessage()中的调用方式)。

为了管理cleared状态的message指针,引入了多个游标来标记数据:

  • current_size_: 当前待处理的message地址;
  • allocated_size_:已经分配message的数据,current_size_ <= allocated_size_,从current_size_allocated_size_之间的message就是被cleared的;
  • total_size_:elements_[]的长度,但从allocated_size_total_size_之间的void*是无效的,并没有指向任何message;

对应内存分布如下图所示:

avatar

封装多种类型,统一对外的服务

针对数据/行为简单的类型,使用轻量级的方案(struct/enum/union/switch-case),来实现类型的封装,而不是采用继承方式来实现。

Symbol可能有多种类型,enum Type表示具体类型,union让多种类型都复用同一个内存地址:

    struct Symbol {
      enum Type {
        NULL_SYMBOL, MESSAGE, FIELD, ENUM, ENUM_VALUE, SERVICE, METHOD, PACKAGE
      };
      Type type;
      union {
        const Descriptor* descriptor;
        const FieldDescriptor* field_descriptor;
        const EnumDescriptor* enum_descriptor;
        const EnumValueDescriptor* enum_value_descriptor;
        const ServiceDescriptor* service_descriptor;
        const MethodDescriptor* method_descriptor;
        const FileDescriptor* package_file_descriptor;
      };

    inline Symbol() : type(NULL_SYMBOL) { descriptor = NULL; }
    …… //省略部分

CONSTRUCTOR帮助提高代码可读性,来实现不同类型Symbol的构造函数:

    #define CONSTRUCTOR(TYPE, TYPE_CONSTANT, FIELD)  \
      inline explicit Symbol(const TYPE* value) {    \
        type = TYPE_CONSTANT;                        \
        this->FIELD = value;                         \
      }

CONSTRUCTOR的使用:

      CONSTRUCTOR(Descriptor         , MESSAGE   , descriptor             )
      CONSTRUCTOR(FieldDescriptor    , FIELD     , field_descriptor       )
      CONSTRUCTOR(EnumDescriptor     , ENUM      , enum_descriptor        )
      CONSTRUCTOR(EnumValueDescriptor, ENUM_VALUE, enum_value_descriptor  )
      CONSTRUCTOR(ServiceDescriptor  , SERVICE   , service_descriptor     )
      CONSTRUCTOR(MethodDescriptor   , METHOD    , method_descriptor      )
      CONSTRUCTOR(FileDescriptor     , PACKAGE   , package_file_descriptor)
    #undef CONSTRUCTOR

具体应用时,根据type来区分处理:

      const FileDescriptor* GetFile() const {
        switch (type) {
          case NULL_SYMBOL: return NULL;
          case MESSAGE    : return descriptor           ->file();
          case FIELD      : return field_descriptor     ->file();
          case ENUM       : return enum_descriptor      ->file();
          case ENUM_VALUE : return enum_value_descriptor->type()->file();
          case SERVICE    : return service_descriptor   ->file();
          case METHOD     : return method_descriptor    ->service()->file();
          case PACKAGE    : return package_file_descriptor;
        }
        return NULL;
      }
    };

不同的类作为模版参数时,提供类独有的类型

GenericTypeHandler和类StringTypeHandler 需要作为模版类型参数(typehandler),在子类RepeatedPtrField在调用父类RepeatedPtrFieldBase的模板函数时,通过模板参数直接传入父类RepeatedPtrFieldBase,这里需要根据不同的typehandler,返回对应不同的类型:

    template <typename TypeHandler>
    inline const typename TypeHandler::Type&
    RepeatedPtrFieldBase::Get(int index) const {
      GOOGLE_DCHECK_LT(index, size());
      return *cast<TypeHandler>(elements_[index]);
   }

所以有如下方式,在不同模版参数类型中通过typedef方式来实现类型名称的统一,因为对于模版来说,关键点就是有统一的名称。

GenericTypeHandler

    template <typename GenericType>
    class GenericTypeHandler {
     public:
      typedef GenericType Type;

      static GenericType* New() { return new GenericType; }
      static void Delete(GenericType* value) { delete value; }
      static void Clear(GenericType* value) { value->Clear(); }
      static void Merge(const GenericType& from, GenericType* to) {
        to->MergeFrom(from);
      }
      static int SpaceUsed(const GenericType& value) { return value.SpaceUsed(); }
    };

StringTypeHandler

    // HACK:  If a class is declared as DLL-exported in MSVC, it insists on
    //   generating copies of all its methods -- even inline ones -- to include
    //   in the DLL.  But SpaceUsed() calls StringSpaceUsedExcludingSelf() which
    //   isn't in the lite library, therefore the lite library cannot link if
    //   StringTypeHandler is exported.  So, we factor out StringTypeHandlerBase,
    //   export that, then make StringTypeHandler be a subclass which is NOT
    //   exported.
    // TODO(kenton):  There has to be a better way.

    class LIBPROTOBUF_EXPORT StringTypeHandlerBase {
     public:
      typedef string Type;
      static string* New();
      static void Delete(string* value);
      static void Clear(string* value) { value->clear(); }
      static void Merge(const string& from, string* to) { *to = from; }
    };

    class LIBPROTOBUF_EXPORT StringTypeHandler : public StringTypeHandlerBase {
     public:
      static int SpaceUsed(const string& value)  {
        return sizeof(value) + StringSpaceUsedExcludingSelf(value);
      }
    };

对应类的关系图:

avatar

低配版release来节省资源

在proto文件中增加配置,产出不支持reflection/descriptor的MessageLite子类,而不是Message子类。

    option optimize_for = LITE_RUNTIME
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值