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

Posted

技术标签:

【中文标题】为啥 ProtoBuf 在第一次调用时这么慢,但在循环内部却非常快?【英文标题】:Why is ProtoBuf so slow on the 1st call but very fast inside loops?为什么 ProtoBuf 在第一次调用时这么慢,但在循环内部却非常快? 【发布时间】:2012-12-06 00:55:11 【问题描述】:

灵感来自this question。我创建了一个小型基准程序来比较 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

免责声明

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

    对于 ServiceStack,Json 和 JSV 得分来自两个单独的运行。由于它们共享相同的底层 ServiceStack 库,因此一个接一个地运行会影响下一次运行的“冷启动”1 循环分数(它的“热启动”速度很快)

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

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

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

【问题讨论】:

也许你可以尝试预编译你的程序。为此,您需要为二进制文件添加一个强名称并运行 ngen。 @jpa “预编译”和“添加强名称”是两个基本不相关的主题... @MarcGravell, IIRC 如果使用 ngen 将 .NET 二进制文件预编译到全局程序集缓存,它只能使用强名称。但当然还有其他方法可以达到同样的效果。 @jpa 啊,对;恩根。这在这里无济于事,因为 JIT 编译不是问题。问题是该库进行元编程 - 即它在运行时编写 IL 来处理它接收到的数据(它不提前知道) 【参考方案1】:

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

因为分析模型和准备策略需要做大量的工作;我花了很多时间使生成的策略尽可能快,但可能是我忽略了元编程层的优化。我很高兴将其添加为要查看的项目,以减少第一次通过的时间。当然,另一方面,元编程层的速度仍然是 Json.NET 的等效预处理的两倍;p

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

多种选择:

    使用“预编译”工具作为构建过程的一部分,将编译后的序列化程序生成为单独的完全静态编译的 dll,您可以像往常一样引用和使用:然后发生完全零元编程

    在启动时明确告诉模型“根”类型,并存储Compile()的输出

    static TypeModel serializer;
    ...
    RuntimeTypeModel.Default.Add(typeof(Foo), true);
    RuntimeTypeModel.Default.Add(typeof(Bar), true);
    serializer = RuntimeTypeModel.Default.Compile();
    

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

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

    RuntimeTypeModel.Default.Add(typeof(Foo), true);
    RuntimeTypeModel.Default.Add(typeof(Bar), true);
    for(int i = 0 ; i < 5 ; i++) 
        RuntimeTypeModel.Default.CompileInPlace();
    
    

另外,我应该:

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

最后的想法:CompileCompileInPlace 之间的主要区别在于如果您忘记添加某些类型会发生什么; CompileInPlace 适用于现有模型,因此您仍然可以稍后添加新类型(隐式或显式),它会“正常工作”; Compile 更加严格:一旦你通过它生成了一个类型,它就固定了,并且可以处理它在编译时可以推断出的类型。

【讨论】:

太棒了,我认为 CompileInPlace() 最适合大多数应用程序。我认为这是一个内存编译,所以假设没有人在托管应用程序进程上拔掉插头,那么编译结果的生命周期是多少? @Sid 是的,这完全在内存中; CompileInPlace 使用 DynamicMethod 工作,每个类型单独(每个类型的方法根据需要延迟编译) 太好了,谢谢!我在哪里可以阅读或询问 ProtoBuf 的线程安全性? “静态”和“内存中”引发了一些危险信号。我可以在这里问,但如果你有更好的场地可以改道...... @Sid 实际上,唯一的 staticRuntimeTypeModel.Default;核心从 static 移动到 v2 中的实例,RuntimeTypeModel.Default 提供了预先存在的 Serializer.* 方法的实现。但是:任何TypeModel(基本上是序列化程序实例)都是完全线程安全的。这包括所有公开为 Serializer.* 的方法(代理到 RuntimeTypeModel.Default.*

以上是关于为啥 ProtoBuf 在第一次调用时这么慢,但在循环内部却非常快?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个涉及 list.index() 调用的 lambda 这么慢?

MSCK REPAIR TABLE 在幕后做了啥,为啥这么慢?

为啥通过weak_ptr调用这么慢?

为啥我的haskell程序这么慢? Haskell 编程,人生游戏

为啥插入 set<vector<string>> 这么慢?

Node - 为啥我的 gif 在使用 GifEncoder 时这么慢