[protocolbuffers/protobuf]Java:延迟创建 UnknownFieldSet Builder

2024-05-11 276 views
3

生成的 Java 代码始终分配一个本地UnknownFieldSet.Builder,随着时间的推移,它会增加对象的变动,尤其是在小消息中。未知字段在我们的环境中相当罕见,因此在大多数情况下这种分配是不必要的。

这里是我们在产品中使用的自定义字符串消息的生成代码(它显示在我们的指标中,这意味着逃逸分析未涵盖此内容):

字符串原型

private String(
        com.google.protobuf.CodedInputStream input,
        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
        throws com.google.protobuf.InvalidProtocolBufferException {
      this();
      if (extensionRegistry == null) {
        throw new java.lang.NullPointerException();
      }
      int mutable_bitField0_ = 0;
      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
          com.google.protobuf.UnknownFieldSet.newBuilder();
      try {
        boolean done = false;
        while (!done) {
          int tag = input.readTag();
          switch (tag) {
            case 0:
              done = true;
              ...
            default: {
              if (!parseUnknownField(
                  input, unknownFields, extensionRegistry, tag)) {
                done = true;
              }
              break;
            }
          }
        }
...
      } finally {
        this.unknownFields = unknownFields.build();
        makeExtensionsImmutable();
      }

knownFields可以用null初始化,并且仅在收到实际未知字段时在默认情况下分配。在finally块中,需要检查是否为null。

回答

8

您能在这里详细说明一下这些指标吗?这个要花多少钱?

5

这不是一个大问题,因为我们谈论的是非常短命的对象。尽管如此,我们每分钟都会收到大量此类消息(2 小时内分配了 25 GB 的屏幕截图)

简而言之:这并没有真正伤害我们,但对我来说这似乎是个好主意(提议的更改似乎是相当局部的且很小)

0

更新:随着最近在构建器中添加 TreeMap,问题变得更加严重。在我们的小消息中,即使没有未知字段,现在大约 50% 的已分配内存用于未知字段处理:

图像

正如我在第一篇文章中提到的,可以通过仅在需要时才延迟分配构建器来轻松避免这种情况(大多数情况下并非如此)

1

我认为我可以延迟初始化构建器,但这仍然会在每条消息中留下 UnknownFieldSet 及其 TreeMap 。仅修复构建器值得吗?或者这也需要更仔细地实现 EmptyUnknownFieldSset 对象吗?

3

这确实让我想知道是否值得让 UnknownFieldSet 使用 Collections.emptyMap 作为默认实例而不是空 TreeMap。

7

好吧,可能不会。 UnknownFieldSet 默认实例实际上是一个单例,因此它不会占用太多内存,因为单个实例是共享的。这可能也回答了我之前的问题。这里只有建造者才是真正重要的。

6

正如我之前所说:在大多数情况下这并不是什么大不了的事,而且在大消息中它是相当微不足道的(< 5% 开销)。但在我作为屏幕截图添加的特定情况下,它总计约为 50%,因为它是我们系统中非常小的且广泛使用的消息。自从我们引入它以来它也从未改变过,所以我假设构建器仅用于使用默认实例初始化“unknownFields”成员。

我最近研究了代码并提出了类似的解决方案:

com.google.protobuf.UnknownFieldSet.Builder unknownFields = null;           // init with null
try {
        boolean done = false;
        while (!done) {
          int tag = input.readTag();
          switch (tag) {
            case 0:
              done = true;
...
            default: {
              // lazy init if necessary
              if (unknownFields == null) unknownFields = com.google.protobuf.UnknownFieldSet.newBuilder();
              if (!parseUnknownField(input, unknownFields, extensionRegistry, tag)) {
                done = true;
              }
              break;
            }
          }
        }
      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
...
      } finally {
        // check if exists and init to default instance if not
        this.unknownFields = unknownFields == null ? UnknownFieldSet.getDefaultInstance() : unknownFields.build();
        makeExtensionsImmutable();
      }

但我不能 100% 确定我的逆向工程评估是正确的......

2

事情没那么简单。一些原型模式创建的代码看起来像这样,可能会与未知的枚举一起使用:

            case 34: {
              if (!((mutable_bitField0_ & 0x00000008) != 0)) {
                int32ToEnumField_ = com.google.protobuf.MapField.newMapField(
                    Int32ToEnumFieldDefaultEntryHolder.defaultEntry);
                mutable_bitField0_ |= 0x00000008;
              }
              com.google.protobuf.ByteString bytes = input.readBytes();
              com.google.protobuf.MapEntry<java.lang.Integer, java.lang.Integer>
              int32ToEnumField__ = Int32ToEnumFieldDefaultEntryHolder.defaultEntry.getParserForType().parseFrom(bytes);
              if (foo.Test.TestMap.EnumValue.forNumber(int32ToEnumField__.getValue()) == null) {
                unknownFields.mergeLengthDelimitedField(4, bytes);
              } else {
                int32ToEnumField_.getMutableMap().put(
                    int32ToEnumField__.getKey(), int32ToEnumField__.getValue());
              }
              break;
            }

这发生在默认子句之前,也需要考虑。

6

此代码来自map_field.cc和mutable_map_field.cc

4

仅供参考,我不确定这是否可行。添加额外的几行代码以在使用前延迟初始化未知字段集,这打破了 Java 对于具有许多(1000+)枚举的原型可以包含多少字节代码的 Java 大小限制。可能有一种方法可以解决这个问题,而无需完全改变协议处理枚举的方式,但到目前为止我想到的一切似乎都比延迟初始化解决方案花费更多。

7

谢谢(你的)信息。正如我一开始所说的,在大多数情况下这并不是什么大问题。在不知道要考虑的细节的情况下,对我来说这看起来像是一个简单的改进。我们可以考虑在特殊情况下添加一些特殊的源代码操作(仅限“字符串”消息),因为我们确切地知道要处理什么。

8

一个想法:我们也许能够懒惰地创建 TreeSet,但急切地创建 UnknownFieldSet。

5

关闭,因为它可能不可行,并且节省的资金不足以保证更大的解决方法。