为什么ProtoBuf在第一次调用时这么慢,但内部循环非常快?

Dee*_*101 5 protocol-buffers protobuf-net

灵感来自这个问题.我创建了一个小的基准程序来比较ProtoBuf,BinaryFormatter和Json.NET.基准测试本身是一个基于控制台的小型控制台,位于https://github.com/sidshetye/SerializersCompare.可以随意添加/改进,添加新的串行器非常简单.无论如何,我的结果是:

        Binary Formatter         ProtoBuf          Json.NET     ServiceStackJson   ServiceStackJSV
 Loop     Size:512 bytes    Size:99 bytes    Size:205 bytes      Size:205 bytes     Size:181 bytes
    1         16.1242 ms      151.6354 ms       277.2085 ms         129.8321 ms        146.3547 ms
    2          0.0673 ms        0.0349 ms         0.0727 ms           0.0343 ms          0.0370 ms
    4          0.0292 ms        0.0085 ms         0.0303 ms           0.0145 ms          0.0148 ms
    8          0.0255 ms        0.0069 ms         0.0017 ms           0.0216 ms          0.0129 ms
   16          0.0011 ms        0.0064 ms         0.0282 ms           0.0114 ms          0.0120 ms
   32          0.0164 ms        0.0061 ms         0.0334 ms           0.0112 ms          0.0120 ms
   64          0.0347 ms        0.0073 ms         0.0296 ms           0.0121 ms          0.0013 ms
  128          0.0312 ms        0.0058 ms         0.0266 ms           0.0062 ms          0.0117 ms
  256          0.0256 ms        0.0097 ms         0.0448 ms           0.0087 ms          0.0116 ms
  512          0.0261 ms        0.0058 ms         0.0307 ms           0.0127 ms          0.0116 ms
 1024          0.0258 ms        0.0057 ms         0.0309 ms           0.0113 ms          0.0122 ms
 2048          0.0257 ms        0.0059 ms         0.0297 ms           0.0125 ms          0.0121 ms
 4096          0.0247 ms        0.0060 ms         0.0290 ms           0.0119 ms          0.0120 ms
 8192          0.0247 ms        0.0060 ms         0.0286 ms           0.0115 ms          0.0121 ms
Run Code Online (Sandbox Code Playgroud)

免责声明:

  1. 上面的结果来自Windows VM - 与裸机操作系统相比,非常小的间隔的秒表/计时器值可能不是100%准确.因此,请忽略上表中的超低值.

  2. 对于ServiceStack,Json和JSV得分来自两个单独的运行.由于它们共享相同的底层ServiceStack库,因此在另一个之后运行一个影响下一次运行的"冷启动"1循环分数(它的"热启动"快速)

BinaryFormatter是最大的,但对于单个序列化=>反序列化循环也是最快的.但是,一旦我们围绕序列化=>反序列化代码进行紧密循环,ProtoBuf就会非常快.

问题1:为什么ProtoBuf对于单个序列化=>反序列化循环要慢得多?

问题2:从实际角度来看,我们可以做些什么才能超越"冷启动"?通过它运行至少一个对象(任何类型)?通过它运行每个(关键)对象类型?

Mar*_*ell 10

问题1:为什么ProtoBuf对于单个序列化=>反序列化循环要慢得多?

因为分析模型并制定策略需要做大量的工作; 我花了很多时间让生成的策略尽可能快地完成,但可能是因为我在元编程层中优化了.我很乐意将其作为一个项目添加,以减少第一次通过的时间.当然,另一方面,元编程层仍然是Json.NET等效预处理的两倍; p

问题2:从实际角度来看,我们可以做些什么才能超越"冷启动"?通过它运行至少一个对象(任何时间)?通过它运行每个(关键)对象类型?

各种选择:

  1. 使用"预编译"工具作为构建过程的一部分,将编译后的序列化程序生成为一个单独的全静态编译dll,您可以像平常一样引用和使用:完全零元编程然后发生
  2. 在启动时明确告诉模型"root"类型,并存储输出 Compile()

    static TypeModel serializer;
    ...
    RuntimeTypeModel.Default.Add(typeof(Foo), true);
    RuntimeTypeModel.Default.Add(typeof(Bar), true);
    serializer = RuntimeTypeModel.Default.Compile();
    
    Run Code Online (Sandbox Code Playgroud)

    (该Compile()方法将从根类型进行分析,添加所需的任何其他类型,返回已编译的生成实例)

  3. 明确告诉模型启动时的"根"类型,并调用CompileInPlace()"几次"; CompileInPlace()不会完全扩展模型 - 但是调用它几次应该涵盖大多数基础,因为编译一个层会将其他类型带入模型

    RuntimeTypeModel.Default.Add(typeof(Foo), true);
    RuntimeTypeModel.Default.Add(typeof(Bar), true);
    for(int i = 0 ; i < 5 ; i++) {
        RuntimeTypeModel.Default.CompileInPlace();
    }
    
    Run Code Online (Sandbox Code Playgroud)

另外,我应该:

  1. 添加一个方法来完全展开CompileInPlace场景的模型
  2. 花一些时间优化元编程层

最后的思考:之间的主要区别Compile,并CompileInPlace在这里将是,如果你已经忘记添加一些类型会发生什么; CompileInPlace适用于现有模型,因此您仍可以稍后添加新类型(隐式或显式),它将"正常工作"; Compile更刚性的:一旦你通过生成一个类型,它是固定的,可以处理只有它可以在编译它的时间推断的类型.