WCF绑定使用的序列化性能测试

12

我有以下对象:

public partial class Game
{
    public bool Finished { get; set; }

    public Guid GameGUID { get; set; }

    public long GameID { get; set; }

    public bool GameSetup { get; set; }

    public Nullable<int> MaximumCardsInDeck { get; set; }

    public Player Player { get; set; }

    public Player Player1 { get; set; }

    public bool Player1Connected { get; set; }

    public bool Player1EnvironmentSetup { get; set; }

    public long Player1ID { get; set; }

    public int Player1Won { get; set; }

    public bool Player2Connected { get; set; }

    public bool Player2EnvironmentSetup { get; set; }

    public long Player2ID { get; set; }

    public int Player2Won { get; set; }

    public int Round { get; set; }

    public Nullable<int> RoundsToWin { get; set; }

    public bool Started { get; set; }

    public string StateXML { get; set; }

    public Nullable<DateTime> TimeEnded { get; set; }

    public Nullable<int> TimeLimitPerTurn { get; set; }

    public byte[] TimeStamp { get; set; }

    public Nullable<DateTime> TimeStarted { get; set; }    
}

这个类将填充一些测试数据

我需要比较不同绑定形式的WCF服务所使用的不同序列化程序的性能:

  • basicHttpBinding => SoapFormatter(TextFormatter?)
  • binaryBinding => BinaryFormatter
  • XMLFormatter

我需要详细了解以下内容:

  • 获取要序列化的对象的大小
  • 获取序列化后的大小
  • 进行序列化所需的时间
  • 进行反序列化所需的时间

我已经尝试了一些东西,但还是有点困难。 也许已经有一些简单的代码可以进行此类测量。


(注意:我在发布后不久编辑了我的第二个答案,因为DataContractSerializer测试是错误的;已修复-确保您看到更新的版本,尽管;p) - Marc Gravell
5个回答

36

好的,我来试试……这里有一些原始的序列化器度量数据(强调:您可能需要考虑使用base-64/MTOM来获取总带宽要求,以及WCF添加的任何固定开销(包括空间和CPU),但首先是结果:

BinaryFormatter
Length: 1314
Serialize: 6746
Deserialize: 6268

XmlSerializer
Length: 1049
Serialize: 3282
Deserialize: 5132

DataContractSerializer
Length: 911
Serialize: 1411
Deserialize: 4380

NetDataContractSerializer
Length: 1139
Serialize: 2014
Deserialize: 5645

JavaScriptSerializer
Length: 528
Serialize: 12050
Deserialize: 30558

(protobuf-net v2)
Length: 112
Serialize: 217
Deserialize: 250

因此我得出结论,protobuf-net v2是胜者...

数字已经更新到.NET 4.5和当前库构建,并在较新的计算机上进行测试:

BinaryFormatter
Length: 1313
Serialize: 2786
Deserialize: 2407

XmlSerializer
Length: 1049
Serialize: 1265
Deserialize: 2165

DataContractSerializer
Length: 911
Serialize: 574
Deserialize: 2011

NetDataContractSerializer
Length: 1138
Serialize: 850
Deserialize: 2535

JavaScriptSerializer
Length: 528
Serialize: 8660
Deserialize: 8468

(protobuf-net v2)
Length: 112
Serialize: 78
Deserialize: 134

使用测试装置(编译优化后,在命令行中运行):

(请注意,我不得不创造Player类和一些示例数据):

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Web.Script.Serialization;
using System.Xml.Serialization;
using ProtoBuf.Meta;


static class Program
{
    static void Main()
    {
        var orig = new Game {
             Finished = true, GameGUID = Guid.NewGuid(), GameID = 12345, GameSetup = false, MaximumCardsInDeck = 20,
             Player = new Player { Name = "Fred"}, Player1 = new Player { Name = "Barney"}, Player1Connected = true,
             Player1EnvironmentSetup = true, Player1ID = 12345, Player1Won = 3, Player2Connected = true, Player2EnvironmentSetup = true,
             Player2ID = 23456, Player2Won = 0, Round = 4, RoundsToWin = 5, Started = true, StateXML = "not really xml",
             TimeEnded = null, TimeLimitPerTurn = 500, TimeStamp = new byte[] {1,2,3,4,5,6}, TimeStarted = DateTime.Today};
        const int LOOP = 50000;

        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();
        using (var ms = new MemoryStream())
        {
            var ser = new BinaryFormatter();
            Console.WriteLine();
            Console.WriteLine(ser.GetType().Name);
            ser.Serialize(ms, orig);
            Console.WriteLine("Length: " + ms.Length);
            ms.Position = 0;
            ser.Deserialize(ms);

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ms.SetLength(0);
                ser.Serialize(ms, orig);
            }
            watch.Stop();
            Console.WriteLine("Serialize: " + watch.ElapsedMilliseconds);
            watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ser.Deserialize(ms);
            }
            watch.Stop();
            Console.WriteLine("Deserialize: " + watch.ElapsedMilliseconds);
        }

        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();
        using (var ms = new MemoryStream())
        {
            var ser = new XmlSerializer(typeof(Game));
            Console.WriteLine();
            Console.WriteLine(ser.GetType().Name);
            ser.Serialize(ms, orig);
            Console.WriteLine("Length: " + ms.Length);
            ms.Position = 0;
            ser.Deserialize(ms);

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ms.SetLength(0);
                ser.Serialize(ms, orig);
            }
            watch.Stop();
            Console.WriteLine("Serialize: " + watch.ElapsedMilliseconds);
            watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ser.Deserialize(ms);
            }
            watch.Stop();
            Console.WriteLine("Deserialize: " + watch.ElapsedMilliseconds);
        }

        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();
        using (var ms = new MemoryStream())
        {
            var ser = new DataContractSerializer(typeof(Game));
            Console.WriteLine();
            Console.WriteLine(ser.GetType().Name);
            ser.WriteObject(ms, orig);
            Console.WriteLine("Length: " + ms.Length);
            ms.Position = 0;
            ser.ReadObject(ms);

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ms.SetLength(0);
                ser.WriteObject(ms, orig);
            }
            watch.Stop();
            Console.WriteLine("Serialize: " + watch.ElapsedMilliseconds);
            watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ser.ReadObject(ms);
            }
            watch.Stop();
            Console.WriteLine("Deserialize: " + watch.ElapsedMilliseconds);
        }

        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();
        using (var ms = new MemoryStream())
        {
            var ser = new NetDataContractSerializer();
            Console.WriteLine();
            Console.WriteLine(ser.GetType().Name);
            ser.Serialize(ms, orig);
            Console.WriteLine("Length: " + ms.Length);
            ms.Position = 0;
            ser.Deserialize(ms);

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ms.SetLength(0);
                ser.Serialize(ms, orig);
            }
            watch.Stop();
            Console.WriteLine("Serialize: " + watch.ElapsedMilliseconds);
            watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ser.Deserialize(ms);
            }
            watch.Stop();
            Console.WriteLine("Deserialize: " + watch.ElapsedMilliseconds);
        }

        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();
        {
            var sb = new StringBuilder();
            var ser = new JavaScriptSerializer();
            Console.WriteLine();
            Console.WriteLine(ser.GetType().Name);
            ser.Serialize(orig, sb);
            Console.WriteLine("Length: " + sb.Length);
            ser.Deserialize(sb.ToString(), typeof(Game));

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                sb.Length = 0;
                ser.Serialize(orig, sb);
            }
            watch.Stop();
            string s = sb.ToString();
            Console.WriteLine("Serialize: " + watch.ElapsedMilliseconds);
            watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ser.Deserialize(s, typeof(Game));
            }
            watch.Stop();
            Console.WriteLine("Deserialize: " + watch.ElapsedMilliseconds);
        }

        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();
        using (var ms = new MemoryStream())
        {
            var ser = CreateProto();
            Console.WriteLine();
            Console.WriteLine("(protobuf-net v2)");
            ser.Serialize(ms, orig);
            Console.WriteLine("Length: " + ms.Length);
            ms.Position = 0;
            ser.Deserialize(ms, null, typeof(Game));

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ms.SetLength(0);
                ser.Serialize(ms, orig);
            }
            watch.Stop();
            Console.WriteLine("Serialize: " + watch.ElapsedMilliseconds);
            watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOP; i++)
            {
                ms.Position = 0;
                ser.Deserialize(ms, null, typeof(Game));
            }
            watch.Stop();
            Console.WriteLine("Deserialize: " + watch.ElapsedMilliseconds);
        }

        Console.WriteLine();
        Console.WriteLine("All done; any key to exit");
        Console.ReadKey();
    }
    static TypeModel CreateProto()
    {
        var meta = TypeModel.Create();
        meta.Add(typeof(Game), false).Add(Array.ConvertAll(typeof(Game).GetProperties(),prop=>prop.Name));
        meta.Add(typeof(Player), false).Add(Array.ConvertAll(typeof(Player).GetProperties(),prop=>prop.Name));
        return meta.Compile();
    }
}

[Serializable, DataContract]
public partial class Game
{
    [DataMember]
    public bool Finished { get; set; }
    [DataMember]
    public Guid GameGUID { get; set; }
    [DataMember]
    public long GameID { get; set; }
    [DataMember]
    public bool GameSetup { get; set; }
    [DataMember]
    public Nullable<int> MaximumCardsInDeck { get; set; }
    [DataMember]
    public Player Player { get; set; }
    [DataMember]
    public Player Player1 { get; set; }
    [DataMember]
    public bool Player1Connected { get; set; }
    [DataMember]
    public bool Player1EnvironmentSetup { get; set; }
    [DataMember]
    public long Player1ID { get; set; }
    [DataMember]
    public int Player1Won { get; set; }
    [DataMember]
    public bool Player2Connected { get; set; }
    [DataMember]
    public bool Player2EnvironmentSetup { get; set; }
    [DataMember]
    public long Player2ID { get; set; }
    [DataMember]
    public int Player2Won { get; set; }
    [DataMember]
    public int Round { get; set; }
    [DataMember]
    public Nullable<int> RoundsToWin { get; set; }
    [DataMember]
    public bool Started { get; set; }
    [DataMember]
    public string StateXML { get; set; }
    [DataMember]
    public Nullable<DateTime> TimeEnded { get; set; }
    [DataMember]
    public Nullable<int> TimeLimitPerTurn { get; set; }
    [DataMember]
    public byte[] TimeStamp { get; set; }
    [DataMember]
    public Nullable<DateTime> TimeStarted { get; set; }
}
[Serializable, DataContract]
public class Player
{
    [DataMember]
    public string Name { get; set; }
}

(注:protobuf-net v2 基于“r352”) - Marc Gravell
@Ben - 我猜测WCF二进制(内置)是NetDataContractSerializer。不过在remoting中默认是BinaryFormatter - Marc Gravell
虽然这是一个旧的帖子,但我测试了一下自己创建的自定义二进制序列化器,它只适用于两种已知类型,并且提前完全了解这些类型的格式。这是我能做到的最简洁、最快速的方法。使用上述测试方法,我只能比protoBuf快约35%并且更小 - 因此在实际应用中没有太多超越protobuf的优势。 - Richard Hauer
基于这些结果,protobuf 显然是赢家。对此有什么见解吗?此外,它是否有任何缺点/限制? - Isaac
@Isaac Protobuff现在支持所有类型的对象。我不知道完整的列表,但“Marc Gravell”已经在一封电子邮件中回答了我,并列出了以下内容:
  • 接口(嗯,有一些有限的支持)
  • 委托(我猜不支持这个没关系)
  • 嵌套数组/列表(如果中间有一个中间对象就可以了)
  • 意外类型的继承*(预期类型是可以的)
- Iúri dos Anjos
显示剩余2条评论

20

我还有一份用于对比.NET中不同序列化程序性能的基准图表,其中显示@Marc Gravell的二进制protobuf-net序列化程序是明显的赢家。虽然我维护着最快的文本序列化程序之一,它们最接近匹配protobuf-net并且比.NET BCL中提供的所有序列化程序都要快得多。

这些基准测试基于Microsoft的Northwind示例数据库,并显示每个序列化程序相对于Protobuf-net有多慢。

ProtoBuf.net(v1)                      1x
ServiceStack TypeSerializer           2.23x
ServiceStack JsonSerializer           2.58x
Microsoft DataContractSerializer      6.93x
NewtonSoft.Json                       7.83x
Microsoft BinaryFormatter             9.21x
Microsoft JsonDataContractSerializer  9.31x

完整的基准测试结果可以在这里找到。

所以,如果您偏爱/需要使用快速文本序列化器,则以下是Service Stack开源文本序列化器的链接:

顺便说一句,Microsoft 的 JavaScriptSerializer 性能最差,有时比 protobuf-nets 慢40x-100x。由于会拖慢我的基准测试,所以我将其移除了 :)


同上;我对最慢的序列化器有不同的迭代次数;p 我必须找时间尝试一下你的基准测试与v2相比。虽然我认为我会优先完成它;p - Marc Gravell
@marcgravell 绝对的,我也一直在等待v2的完成 :) 我想将其包含为另一个ServiceStack端点。 - mythz

5

我修改了@Marc的基准源代码,并添加了ServiceStack的JSV和JSON序列化器的结果。以下是我3年前的iMac上的结果:

BinaryFormatter
Length: 1313
Serialize: 3959
Deserialize: 3395

XmlSerializer
Length: 1049
Serialize: 1710
Deserialize: 2716

DataContractSerializer
Length: 911
Serialize: 712
Deserialize: 2117

NetDataContractSerializer
Length: 1138
Serialize: 1093
Deserialize: 4825

TypeSerializer
Length: 431
Serialize: 496
Deserialize: 887

JsonSerializer
Length: 507
Serialize: 558
Deserialize: 1213

这是我添加到@Marc的基准测试中的源代码。

GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
var sbJsv = new StringBuilder(4096);
using (var sw = new StringWriter(sbJsv))
{
    Console.WriteLine();
    Console.WriteLine(typeof(TypeSerializer).Name);
    TypeSerializer.SerializeToWriter(orig, sw);
    var jsv = sbJsv.ToString();
    Console.WriteLine("Length: " + sbJsv.Length);
    TypeSerializer.DeserializeFromString<Game>(jsv);

    var watch = Stopwatch.StartNew();
    for (int i = 0; i < LOOP; i++)
    {
        sbJsv.Length = 0;
        TypeSerializer.SerializeToWriter(orig, sw);
    }
    watch.Stop();
    Console.WriteLine("Serialize: " + watch.ElapsedMilliseconds);

    watch = Stopwatch.StartNew();
    for (int i = 0; i < LOOP; i++)
    {
        TypeSerializer.DeserializeFromString<Game>(jsv);
    }
    watch.Stop();
    Console.WriteLine("Deserialize: " + watch.ElapsedMilliseconds);
}

GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
var sbJson = new StringBuilder(4096);
using (var sw = new StringWriter(sbJson))
{
    Console.WriteLine();
    Console.WriteLine(typeof(JsonSerializer).Name);
    JsonSerializer.SerializeToWriter(orig, sw);
    var json = sbJson.ToString();
    Console.WriteLine("Length: " + sbJson.Length);
    JsonSerializer.DeserializeFromString<Game>(json);

    var watch = Stopwatch.StartNew();
    for (int i = 0; i < LOOP; i++)
    {
        sbJson.Length = 0;
        JsonSerializer.SerializeToWriter(orig, sw);
    }
    watch.Stop();
    Console.WriteLine("Serialize: " + watch.ElapsedMilliseconds);

    watch = Stopwatch.StartNew();
    for (int i = 0; i < LOOP; i++)
    {
        JsonSerializer.DeserializeFromString<Game>(json);
    }
    watch.Stop();
    Console.WriteLine("Deserialize: " + watch.ElapsedMilliseconds);
}

注意:我无法获取@Marc用于此的protobuf-net v2 r352 dlls,因此不得不注释掉protobuf-net基准测试。

实际上,你的测试会使时间膨胀(即比它需要显示的更慢),因为它似乎是扩展而不是覆盖缓冲区。由你决定,但使用更可比较的测试可能会更快。 - Marc Gravell
是的,你说得对。我在重构代码时想要重置 StringWriter 的位置,但找不到方法,所以就把它保留了下来 - 没有考虑过重置 StringBuilder.Length :) - mythz

4
在最简单的层面上,只需对大量的数据进行序列化,然后测试时间并测量带宽。而“大量”应包括大型和小型但数量众多的有效载荷。
您还应该考虑使用/不使用MTOM。尽管我可能有偏见,但建议您包括替代WCF序列化程序(例如protobuf-net),如果需要帮助,请告诉我如何钩入。从这个领域的许多工作中可以看出,它通常在各个方面都能击败你提到的所有序列化程序。
然而,其中很多涉及的内容都可以在序列化器级别进行调查,甚至无需触及WCF,但这将绕过base-64 / MTOM,因此不是100%准确。
但我们无法为您定义措施。您自己可以决定什么是关键。我确实有一些衡量标准 - 通常只是:
- 对MemoryStream进行一次序列化(和反序列化);这会为您获得大小并启动JIT。 - 现在保留该memory-stream(作为方便的缓冲区)并(在秒表内)进行数千次序列化。除以数千次。每次重绕,因此您正在覆盖(而不是扩展)。 - 重复但反序列化数千次。除以数千次。

1

使用一个大小恒定的对象;从类型中获取“大小”信息很麻烦,而且不会在确定“最佳”方案方面带来太多好处。任何被装饰为DataContract的对象都可以序列化为二进制(DataContract继承自Serializable)、基本XML(任何具有默认构造函数的对象都可以序列化为XML)或DataContract XML(这需要最多的标记开始,但它非常简单)。

为了进行运行测试,创建一个方法,该方法将接受一个对象和一个序列化器。它应该创建一个MemoryStream并启动一个StopWatch,然后将对象序列化到MemoryStream中(确保Flush())。然后停止秒表并将结果作为TimeSpan和流的长度返回给您。然后重置并启动秒表,并反序列化流,并记住那个时间。您可以将返回结果设置为一个简单的结构体。

对于要测试的每个序列化器,使用相同的对象运行此操作。将每个结果输出到控制台或调试输出,并让最好的序列化器获胜。

一般来说,我认为您会发现:

  • BinarySerializer 是最快和最小的,因为在序列化时要写入的字节开销最少。然而,.NET 的二进制序列化是特定于平台的;如果你想与除了另一个知道你确切类型的 .NET 程序集之外的任何东西交流,那就别想了。

  • XMLSerializer、SoapSerializer 和 DataContractSerializer 都输出各种形式的 XML。DataContract 实际上是最简单的格式(XML 非常基本,因为握手和其他协议/通信信息是分开的),可能会相当快。SOAP 在序列化文件中有很多冗余,因为它包含了传输和元数据信息,但由于它是一个相当严格的格式,所以很容易生成。基本的 XML 序列化由于非常灵活,因此具有很大的开销,但可以生成非常简单或非常复杂的模式。


1
请注意,通常需要大量的循环才能进行任何比较。而且我不确定是否同意某些观点;BinaryFormatter 可能会因数据而过重,而 XmlSerializer 通常比 DataContractSerializer 更简单。当然,这完全取决于数据。 - Marc Gravell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接