协议缓冲区 GetRepeatedField (反射) 代码优化

protocol buffer GetRepeatedField (reflection) code optimization

本文关键字:代码优化 反射 缓冲区 GetRepeatedField 协议      更新时间:2023-10-16

我正在使用协议缓冲区的反射功能在运行时读取消息字段值。

我拥有的原型:

package xapp.battle;
    message BATTLE_DATA {
        repeated AInfo aInfo = 1;
        repeated BInfo bInfo = 2;
        repeated CInfo cInfo = 3;
        // a lot other repeated messages
    }
    message AInfo {
        int32 test_field = 1;
        // ......
    }
    message BInfo {
        int32 test_field = 1;
        // ......
    }
    message CInfo {
        int32 test_field = 1;
        // ......
    }

现在我有的代码:

void DO_SOMETHING(messageName) {
    const Descriptor* pDescriptor = BATTLE_DATA->GetDescriptor();
    const FieldDescriptor* pMessageField = pDescriptor->FindFieldByName(messageName);
    const Reflection* pReflection = BATTLE_DATA->GetReflection();
    if (messageName == "aInfo") {
        const RepeatedPtrField<::xapp::battle::AInfo> repeated_ptr_field = pReflection->GetRepeatedPtrField<::xapp::battle::AInfo>(*BATTLE_DATA, pMessageField);
        for (int i = 0; i < repeated_ptr_field.size(); i ++) {
            ::xapp::battle::AInfo messageInfo = repeated_ptr_field.Get(i);
            // continue to read the test_field value of messageInfo
        }
    }
    else if (messageName == "bInfo") {
        const RepeatedPtrField<::xapp::battle::BInfo> repeated_ptr_field = pReflection->GetRepeatedPtrField<::xapp::battle::BInfo>(*BATTLE_DATA, pMessageField);
        for (int i = 0; i < repeated_ptr_field.size(); i ++) {
            ::xapp::battle::BInfo messageInfo = repeated_ptr_field.Get(i);
            // continue to read the test_field value of messageInfo
        }
    }
    else if (messageName == "CInfo") {
        const RepeatedPtrField<::xapp::battle::CInfo> repeated_ptr_field = pReflection->GetRepeatedPtrField<::xapp::battle::CInfo>(*BATTLE_DATA, pMessageField);
        for (int i = 0; i < repeated_ptr_field.size(); i ++) {
            ::xapp::battle::CInfo messageInfo = repeated_ptr_field.Get(i);
            // continue to read the test_field value of messageInfo
        }
    }
    // ......
    else {
            LOG("loadOneBin - Unknown messageName");
    }
}

这段代码有效,但显然它不是最好的解决方案,因为有很多重复的"else-if"代码块。

我想要的是类似的东西(至少摆脱那些"else-if"块):

const RepeatedPtrField<::xapp::battle::MESSAGE_NAME> repeated_ptr_field = pReflection->GetRepeatedPtrField<::xapp::battle::MESSAGE_NAME>(*BATTLE_DATA, pMessageField);
for (int i = 0; i < repeated_ptr_field.size(); i ++) {
    ::xapp::battle::MESSAGE_NAME messageInfo = repeated_ptr_field.Get(i);
    // continue to read the test_field value of messageInfo
}

GetRepeatedPtrField 的源代码:

template<typename PB>
inline const RepeatedPtrField<PB>& Reflection::GetRepeatedPtrField(
    const Message& message, const FieldDescriptor* field) const {
  return *static_cast<RepeatedPtrField<PB>* >(
      MutableRawRepeatedField(const_cast<Message*>(&message), field,
          FieldDescriptor::CPPTYPE_MESSAGE, -1,
          PB::default_instance().GetDescriptor()));
}

任何建议将不胜感激,谢谢:)

正确答案

这里的正确答案是,您应该使用 Reflection::GetRepeatedMessage() 来获取重复字段的每个元素的泛const Message&,而不是使用 Reflection::GetRepeatedPtrField<T>() 。不幸的是,您需要为每个元素调用此方法一次(使用 Reflection::FieldSize() 查找大小)。

在每个Message,您可以使用Message::GetDescriptor(),查找名为test_field的字段,然后Message::GetReflection()并使用它来读取该字段的值。作为优化,您可以放心地假设同一重复字段中的所有消息都具有相同的DescriptorReflection对象,因此您只需获取这些对象并为整个数组查找一次FieldDescriptor

类似的东西(未经测试):

int size = pReflection->FieldSize(*BATTLE_DATA, pMessageField);
Reflection* pInnerReflection = NULL;
FieldDescriptor* pTestFieldDesc = NULL;
for (int i = 0; i < size; i++) {
  const Message& msg = pReflection->GetRepeatedMessage(
      *BATTLE_DATA, pMessageField, i);
  if (pInnerReflection == NULL) {
    pInnerReflection = msg.GetReflection();
    pTestFieldDesc = msg.GetDescriptor()
        ->FindFieldByName("test_field");
  }
  int testField = pInnerReflection->GetInt32(msg, pTestFieldDesc);
  // ...
}

一个有趣但糟糕的答案

第二种解决方案的性能会稍微好一些,看起来更漂亮一些。不幸的是,它在技术上是未定义的行为。它在实践中适用于所有编译器,但除非您遇到性能问题,否则您可能不应该这样做。我把它放在这里是为了好玩,因为无论如何你都可能会弄清楚,所以很高兴知道它为什么不好。

可以调用 GetRepeatedPtrField<google::protobuf::Message>(...) 来获取泛型Message对象的RepeatedPtrField。然后,您可以像第一个解决方案一样获取描述符和反射,但不再需要调用Reflection::GetMessage(虚拟调用)来读取每条消息。(不过,您仍然需要调用反射来读取test_field

这在技术上是未定义的行为,因为它假设了几件事:

  • RepeatedPtrField<T>对所有T具有相同的布局。事实上,这是正确的,因为RepeatedPtrField<T>扩展RepeatedPtrFieldBase并且不添加任何新字段,但C++标准并不要求这种情况。
  • 将特定消息类型指针(如 AInfo*)向上转换为Message*不会更改指针的位。在实践中,只要没有多重继承,所有编译器都是如此,Protobufs 不使用多重继承(Google C++ 风格指南甚至禁止它)。
  • 编译器不会根据严格的别名规则进行优化,以至于它最终会对RepeatedPtrField<Message>的访问重新排序,而不是对程序中其他地方发生的RepeatedPtrField<AInfo>(或任何类型)的其他访问进行重新排序。在实践中,想象任何编译器实际上为此用例执行此操作是不合理的,但从技术上讲,C++允许这样做。