月度归档:2023年09月

2023版:深度比较几种.NET Excel导出库的性能差异

引言

背景和目的

本文介绍了几个常用的电子表格处理库,包括EPPlus、NPOI、Aspose.Cells和DocumentFormat.OpenXml,我们将对这些库进行性能测评,以便为开发人员提供实际的性能指标和数据。

下表将功能/特点、开源/许可证这两列分开,以满足需求:

功能 / 特点EPPlusNPOIAspose.CellsDocumentFormat.OpenXml
开源
许可证MITApache商业MIT
支持的 Excel 版本Excel 2007 及更高版本Excel 97-2003Excel 2003 及更高版本Excel 2007 及更高版本

测评电脑配置

组件规格
CPU11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz,2496 Mhz,4 个内核,8 个逻辑处理器
内存40 GB DDR4 3200MHz
操作系统Microsoft Windows 10 专业版
电源选项已设置为高性能
软件LINQPad 7.8.5 Beta
运行时.NET 6.0.21

准备工作

使用Bogus库生成6万条标准化的测试数据。

void Main()
{
    string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data.json");
    using var file = File.Create(path);
    using var writer = new Utf8JsonWriter(file, new JsonWriterOptions { Indented = true });
    var data = new Bogus.Faker<Data>()
        .RuleFor(x => x.Id, x => x.IndexFaker + 1)
        .RuleFor(x => x.Gender, x => x.Person.Gender)
        .RuleFor(x => x.FirstName, (x, u) => x.Name.FirstName(u.Gender))
        .RuleFor(x => x.LastName, (x, u) => x.Name.LastName(u.Gender))
        .RuleFor(x => x.Email, (x, u) => x.Internet.Email(u.FirstName, u.LastName))
        .RuleFor(x => x.BirthDate, x => x.Person.DateOfBirth)
        .RuleFor(x => x.Company, x => x.Person.Company.Name)
        .RuleFor(x => x.Phone, x => x.Person.Phone)
        .RuleFor(x => x.Website, x => x.Person.Website)
        .RuleFor(x => x.SSN, x => x.Person.Ssn())
        .GenerateForever().Take(6_0000)
        .Dump();
    JsonSerializer.Serialize(writer, data);
    Process.Start("explorer", @$"/select, ""{path}""".Dump());
}

Bogus输出结果

IdGenderFirstNameLastNameEmailBirthDateCompanyPhoneWebsiteSSN
1MaleAntonioPaucekAntonio.Paucek@gmail.com1987/10/31 5:46:50Moen, Willms and Maggio(898) 283-1583 x88626pamela.name850-06-4706
2MaleKurtGerholdKurt.Gerhold40@yahoo.com1985/11/1 18:41:01Wilkinson and Sons(698) 637-0181 x49124cordelia.net014-86-1757
3MaleHowardHegmannHoward2@hotmail.com1979/7/20 22:35:40Kassulke, Murphy and Volkman(544) 464-9818 x98381kari.com360-23-1669
4FemaleRosemariePowlowskiRosemarie.Powlowski48@hotmail.com1964/5/18 1:35:45Will Group1-740-705-6482laurence.net236-10-9925
5FemaleEuniceRogahnEunice84@gmail.com1979/11/25 11:53:14Rippin – Rowe(691) 491-2282 x3466yvette.net219-75-6886
……

创建公共类方便正式测评使用

void Main()
{
    string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json";
    LoadUsers(path).Dump();
}

List<User> LoadUsers(string jsonfile)
{
    string path = jsonfile;
    byte[] bytes = File.ReadAllBytes(path);
    return JsonSerializer.Deserialize<List<User>>(bytes);
}

IObservable<object> Measure(Action action, int times = 5)
{
    return Enumerable.Range(1, times).Select(i =>
    {
        var sw = Stopwatch.StartNew();

        long memory1 = GC.GetTotalMemory(true);
        long allocate1 = GC.GetTotalAllocatedBytes(true);
        {
            action();
        }
        long allocate2 = GC.GetTotalAllocatedBytes(true);
        long memory2 = GC.GetTotalMemory(true);

        sw.Stop();
        return new
        {
            次数 = i, 
            分配内存 = (allocate2 - allocate1).ToString("N0"),
            内存提高 = (memory2 - memory1).ToString("N0"), 
            耗时 = sw.ElapsedMilliseconds,
        };
    }).ToObservable();
}

class User
{
    public int Id { get; set; }
    public int Gender { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public DateTime BirthDate { get; set; }
    public string Company { get; set; }
    public string Phone { get; set; }
    public string Website { get; set; }
    public string SSN { get; set; }
}

代码解释

1、上面的代码单位是字节 (bytes)

2 、其中IObservable(System.IObservable)是用于处理事件流的接口,它实现了观察者模式。它表示一个可观察的序列,可以产生一系列的事件,并允许其他对象(观察者)来订阅和接收这些事件。IObservable 适用于动态的、实时的事件流处理,允许观察者以异步方式接收事件,可以用于响应式编程、事件驱动的编程模型等。

3、GC.GetTotalAllocatedBytes(true) 获取分配内存大小
GC.GetTotalMemory(true) 获取占用内存大小

性能测评

EPPlus

string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json";
List<User> users = LoadUsers(path);

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.epplus.xlsx");
}).Dump("EPPlus");

void Export<T>(List<T> data, string path)
{
    using var stream = File.Create(path);
    using var excel = new ExcelPackage(stream);
    ExcelWorksheet sheet = excel.Workbook.Worksheets.Add("Sheet1");
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
    {
        sheet.Cells[1, i + 1].Value = props[i].Name;
    }
    for (var i = 0; i < data.Count; ++i)
    {
        for (var j = 0; j < props.Length; ++j)
        {
            sheet.Cells[i + 2, j + 1].Value = props[j].GetValue(data[i]);
        }
    }
    excel.Save();
}

输出结果

EPPlus (6.2.8) (2023/8/15)输出结果

次数ΞΞ分配内存ΞΞ内存提高ΞΞ耗时ΞΞ
1454,869,176970,1602447
2440,353,4881761776
3440,062,26401716
4440,283,58401750
5440,653,26401813

EPPlus (4.5.3.2)(2019/6/16)输出结果

次数ΞΞ分配内存ΞΞ内存提高ΞΞ耗时ΞΞ
1963,850,944192,0482765
2509,450,7926001897
3509,872,1604241920
4509,858,5764241989
5509,651,5124242076

由此看出 相比2019,到了2023年EPPlus的性能得到了略微的提升

NPOI

示例代码一:XSSFWorkbook

List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.npoi.xlsx");
}).Dump("NPOI");

void Export<T>(List<T> data, string path)
{
    IWorkbook workbook = new XSSFWorkbook();
    ISheet sheet = workbook.CreateSheet("Sheet1");

    var headRow = sheet.CreateRow(0);
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
    {
        headRow.CreateCell(i).SetCellValue(props[i].Name);
    }
    for (var i = 0; i < data.Count; ++i)
    {
        var row = sheet.CreateRow(i + 1);
        for (var j = 0; j < props.Length; ++j)
        {
            row.CreateCell(j).SetCellValue(props[j].GetValue(data[i]).ToString());
        }
    }

    using var file = File.Create(path);
    workbook.Write(file);
    workbook.Close();
}

输出结果

NPOI (2.6.1)(2023/7/12)输出结果

次数ΞΞ分配内存内存提高耗时ΞΞ
11,589,285,792567,2725549
21,577,028,664967043
31,577,398,488488107
41,576,360,696-90,5129336
51,576,226,688-3,1208289

NPOI (2.4.1)(2018/12/18)输出结果

次数ΞΞ分配内存内存提高耗时ΞΞ
11,648,548,696526,8246947
21,633,685,1361207921
31,634,033,296248864
41,634,660,176-90,2008945
51,634,205,368-2,5848078

示例代码二:SXSSFWorkbook

List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.npoi.xlsx");
}).Dump("NPOI");

void Export<T>(List<T> data, string path)
{
    IWorkbook workbook = new SXSSFWorkbook();
    ISheet sheet = workbook.CreateSheet("Sheet1");

    var headRow = sheet.CreateRow(0);
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
    {
        headRow.CreateCell(i).SetCellValue(props[i].Name);
    }
    for (var i = 0; i < data.Count; ++i)
    {
        var row = sheet.CreateRow(i + 1);
        for (var j = 0; j < props.Length; ++j)
        {
            row.CreateCell(j).SetCellValue(props[j].GetValue(data[i]).ToString());
        }
    }

    using var file = File.Create(path);
    workbook.Write(file);
    workbook.Close();
}

输出结果

NPOI (2.6.1)(2023/7/12)输出结果

次数分配内存内存提高耗时
1571,769,14411,495,4882542
2482,573,584965106
3481,139,296241463
4481,524,384481510
5481,466,616481493

NPOI (2.4.1)(2018/12/18)输出结果

次数分配内存内存提高耗时
1660,709,472537,5127808
2650,060,3768,1288649
3649,006,9524,1367064
4649,267,920-89,7766973
5649,955,024486538

经过测试 发现SXSSFWorkbook 确实比XSSFWorkbook 性能好,有显著提升
由此看出 相比2018,到了2023年NPOI的性能得到了略微的提升

Aspose.Cells

Util.NewProcess = true;
List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

SetLicense();

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.aspose2.xlsx");
}, 5).Dump("Aspose");

void Export<T>(List<T> data, string path)
{
    using var excel = new Workbook();
    excel.Settings.MemorySetting = MemorySetting.Normal;
    excel.Settings.CheckExcelRestriction = false;
    Worksheet sheet = excel.Worksheets["Sheet1"];
    sheet.Cells.ImportCustomObjects(data, 0, 0, new ImportTableOptions
    {
        IsFieldNameShown = true, 
        DateFormat = "MM/DD/YYYY hh:mm:ss AM/PM", 
        ConvertNumericData = false, 
    });
    excel.Save(path);
}

void SetLicense()
{
    Stream stream = new MemoryStream(Convert.FromBase64String(@"密钥"));
    stream.Seek(0, SeekOrigin.Begin);
    new Aspose.Cells.License().SetLicense(stream);
}

输出结果

Aspose.Cells (23.8.0)(2023/8/9)输出结果

次数分配内存内存提高耗时
1443,025,1123,471,9842889
2392,090,30430,2081863
3391,419,072-81716
4392,041,144241797
5392,078,992241689

Aspose.Cells (19.8.0)(2019/8/20)输出结果

次数分配内存内存提高耗时
1552,862,0562,987,0002913
2508,337,87249,7761750
3507,922,728241933
4507,949,584241781
5508,368,208241773

由此看出 相比2019,到了2023年Aspose.Cells的性能还是一样差不多,只是内存占用减少了

DocumentFormat.OpenXml

List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.openXml.xlsx");
}).Dump("OpenXML");

void Export<T>(List<T> data, string path)
{
    using SpreadsheetDocument excel = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook);

    WorkbookPart workbookPart = excel.AddWorkbookPart();
    workbookPart.Workbook = new Workbook();

    WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
    worksheetPart.Worksheet = new Worksheet(new SheetData());

    Sheets sheets = excel.WorkbookPart.Workbook.AppendChild<Sheets>(new Sheets());
    Sheet sheet = new Sheet
    {
        Id = excel.WorkbookPart.GetIdOfPart(worksheetPart),
        SheetId = 1,
        Name = "Sheet1"
    };
    sheets.Append(sheet);

    SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>();

    PropertyInfo[] props = typeof(User).GetProperties();
    {    // header
        var row = new Row() { RowIndex = 1 };
        sheetData.Append(row);
        row.Append(props.Select((prop, i) => new Cell
        {
            CellReference = ('A' + i - 1) + row.RowIndex.Value.ToString(),
            CellValue = new CellValue(props[i].Name),
            DataType = new EnumValue<CellValues>(CellValues.String),
        }));
    }
    sheetData.Append(data.Select((item, i) => 
    {
        var row = new Row { RowIndex = (uint)(i + 2) };
        row.Append(props.Select((prop, j) => new Cell
        {
            CellReference = ('A' + j - 1) + row.RowIndex.Value.ToString(),
            CellValue = new CellValue(props[j].GetValue(data[i]).ToString()),
            DataType = new EnumValue<CellValues>(CellValues.String),
        }));
        return row;
    }));
    excel.Save();
}

输出结果

DocumentFormat.OpenXml (2.20.0)(2023/4/7)输出结果

次数ΞΞ分配内存内存提高耗时ΞΞ
1614,013,080421,5523909
2613,007,112963487
3613,831,6721043465
4613,058,344243650
5613,161,096243521

DocumentFormat.OpenXml (2.9.1)(2019/3/14)输出结果

次数ΞΞ分配内存内存提高耗时ΞΞ
1542,724,752139,0803504
2542,478,208962897
3543,030,904242826
4542,247,544242957
5542,763,312242941

由此看出 相比2019,到了2023年DocumentFormat.OpenXml的性能反而越差啦

结论和总结

结论一:如果你想找开源,(旧版本免费),(最新版收费)EPPlus 依旧是最佳选择

次数ΞΞ分配内存ΞΞ内存提高ΞΞ耗时ΞΞ
1454,869,176970,1602447
2440,353,4881761776
3440,062,26401716
4440,283,58401750
5440,653,26401813

结论二:如果你想找速度快,很稳定,但收费的,Aspose.Cells 依旧是最佳选择

次数分配内存内存提高耗时
1443,025,1123,471,9842889
2392,090,30430,2081863
3391,419,072-81716
4392,041,144241797
5392,078,992241689

总结:
1、EPPlus表现不错,内存和耗时在开源组中表现最佳
2、收费的Aspose.Cells表现最佳,内存占用最低,用时也最短

作者 => 百宝门瞿佑明

此文章是对此前《.NET骚操作》2019年写的文章的更新和扩展
https://www.cnblogs.com/sdflysha/p/20190824-dotnet-excel-compare.html

深度比较常见库中序列化和反序列化性能的性能差异

背景和目的

本文介绍了几个常用的序列化和反序列化库,包括System.Text.Json、Newtonsoft.Json、 Protobuf-Net、MessagePack-Net,我们将对这些库进行性能测评

库名称介绍Github地址
System.Text.Json.NET Core 3.0及以上版本的内置JSON库,用于读写JSON文本。它提供了高性能和低分配的功能。System.Text.Json
Newtonsoft.Json也被称为Json.NET,是.NET中最常用的JSON序列化库之一。它提供了灵活的方式来转换.NET对象为JSON字符串,以及将JSON字符串转换为.NET对象。Newtonsoft.Json
Protobuf-Net.NET版本的Google’s Protocol Buffers序列化库。Protocol Buffers是一种语言中立、平台中立、可扩展的序列化结构数据的方法。Protobuf-Net
MessagePack-NetMessagePack是一个高效的二进制序列化格式,它允许你在JSON-like的格式中交换数据,但是更小、更快、更简单。MessagePack-Net

性能测试

测评电脑配置

组件规格
CPU11th Gen Intel(R) Core(TM) i5-11320H
内存40 GB DDR4 3200MHz
操作系统Microsoft Windows 10 专业版
电源选项已设置为高性能
软件LINQPad 7.8.5 Beta
运行时.NET 7.0.10

准备工作

0、导入Nuget包

1、Bogus(34.0.2)
2、MessagePack(2.5.124)
3、Newtonsoft.Json(13.0.3)
4、protobuf-net(3.2.26)
5、System.Reactive(6.0.0)

1、性能测试函数

IObservable<object> Measure(Action action, int times = 5)
{
    return Enumerable.Range(1, times).Select(i =>
    {
        var sw = Stopwatch.StartNew();

        long memory1 = GC.GetTotalMemory(true);
        long allocate1 = GC.GetTotalAllocatedBytes(true);
        {
            action();
        }
        long allocate2 = GC.GetTotalAllocatedBytes(true);
        long memory2 = GC.GetTotalMemory(true);

        sw.Stop();
        return new
        {
            次数 = i,
            分配内存 = (allocate2 - allocate1).ToString("N0"),
            内存提高 = (memory2 - memory1).ToString("N0"),
            耗时 = sw.ElapsedMilliseconds,
        };
    }).ToObservable();
}

这个测量函数的它的作用

多次执行指定的动作,并测量每次执行该动作时的内存分配和执行时间。

然后,对于每次操作,它创建并返回一个新的匿名对象,该对象包含以下属性:

次数:操作的次数。
分配内存:操作期间分配的内存量(操作结束后的已分配字节减去操作开始前的已分配字节)。
内存提高:操作期间内存的增加量(操作结束后的总内存减去操作开始前的总内存)。
耗时:操作的执行时间(以毫秒为单位)。

2、生成随机数据的函数

IEnumerable<User> WriteData()
{
    var data = new Bogus.Faker<User>()
        .RuleFor(x => x.Id, x => x.IndexFaker + 1)
        .RuleFor(x => x.Gender, x => x.Person.Gender)
        .RuleFor(x => x.FirstName, (x, u) => x.Name.FirstName(u.Gender))
        .RuleFor(x => x.LastName, (x, u) => x.Name.LastName(u.Gender))
        .RuleFor(x => x.Email, (x, u) => x.Internet.Email(u.FirstName, u.LastName))
        .RuleFor(x => x.BirthDate, x => x.Person.DateOfBirth)
        .RuleFor(x => x.Company, x => x.Person.Company.Name)
        .RuleFor(x => x.Phone, x => x.Person.Phone)
        .RuleFor(x => x.Website, x => x.Person.Website)
        .RuleFor(x => x.SSN, x => x.Person.Ssn())
        .GenerateForever().Take(6_0000);
    return data;
}

Bogus 是一个非常有用的 C# 库,它可以帮助你生成伪造的数据,或者说“假数据”。这在测试或开发阶段非常有用,你可以使用它来填充数据库,或者在没有实际用户数据的情况下测试应用程序。

如果想详细了解使用请参考 这篇文章https://www.cnblogs.com/sdflysha/p/20190821-generate-lorem-data.html

3、数据实体类

[MessagePackObject, ProtoContract]
public class User
{
    [Key(0), ProtoMember(1)]
    public int Id { get; set; }

    [Key(1), ProtoMember(2)]
    public int Gender { get; set; }

    [Key(2), ProtoMember(3)]
    public string FirstName { get; set; }

    [Key(3), ProtoMember(4)]
    public string LastName { get; set; }

    [Key(4), ProtoMember(5)]
    public string Email { get; set; }

    [Key(5), ProtoMember(6)]
    public DateTime BirthDate { get; set; }

    [Key(6), ProtoMember(7)]
    public string Company { get; set; }

    [Key(7), ProtoMember(8)]
    public string Phone { get; set; }

    [Key(8), ProtoMember(9)]
    public string Website { get; set; }

    [Key(9), ProtoMember(10)]
    public string SSN { get; set; }
}

开始性能测试

以下测试代码会加入写入文件,以模拟真实的使用场景,性能怎么样

1、System.Text.Json 性能测试

序列化测试代码

void TextJsonWrite()
{
    var data = WriteData();

    Measure(() =>
        {
            string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data1.json");
            using var file = File.Create(path);
            System.Text.Json.JsonSerializer.Serialize(file,data);
        })
    .Dump();
}

文件大小:14.3MB

测试结果

次数分配内存内存提高耗时
11,429,688,20067,3922494
21,429,960,3523202610
31,429,596,25682615
41,430,126,504-642753
51,429,549,184-4322918

反序列化测试代码

void TextJsonRead()
{
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data1.json");
        byte[] bytes = File.ReadAllBytes(path);
        System.Text.Json.JsonSerializer.Deserialize<List<User>>(bytes);
    }).Dump();
}

测试结果

次数分配内存内存提高耗时
142,958,53643,728212
243,093,44848185
342,884,40824120
442,883,31224129
543,100,89624117

2、Newtonsoft.Json 性能测试

序列化测试代码

void JsonNetWrite()
{
    var data = WriteData();

    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data2.json");
        var jsonData = Newtonsoft.Json.JsonConvert.SerializeObject(data);
        File.WriteAllText(path, jsonData);
    })
    .Dump();
}

文件大小:14.3MB

测试结果

次数分配内存内存提高耗时
11,494,035,69642,6082196
21,494,176,1443202289
31,494,684,672-242899
41,494,292,3762,1523393
51,495,260,472643499

反序列化测试代码

void JsonNetRead()
{
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data2.json");
        var jsonData = File.ReadAllText(path);
        var data = Newtonsoft.Json.JsonConvert.DeserializeObject<List<User>>(jsonData);
    })
    .Dump();
}

测试结果

次数分配内存内存提高耗时
192,556,92063,216275
292,659,78448314
392,407,73624245
492,616,91224276
592,416,12824305

3、ProtobufNet 性能测试

序列化测试代码

void ProtobufNetWrite()
{
    var data = WriteDataTwo();
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data3.bin");
        using var file = File.Create(path);
        Serializer.Serialize(file, data);
    }).Dump();
}

文件大小:7.71MB

测试结果

次数分配内存内存提高耗时
1712,168163,512170
26,760-192111
37,04028097
46,7602466
5244,200068

反序列化测试代码

void ProtobufNetRead()
{
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data3.bin");
        using var file = File.OpenRead(path);
        Serializer.Deserialize<List<UserProtobuf>>(file);
    }
    ).Dump();
}

测试结果

次数分配内存内存提高耗时
129,485,8881,084,240113
228,242,8564896
328,340,6722485
428,333,0882480
528,242,8562476

4、MessagePack-Net 性能测试

序列化测试代码

void MessagePackNetWrite()
{
    var data = WriteDataThreee();
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "UserMessagePackData.bin");
        using var file = File.Create(path);
        MessagePackSerializer.Serialize(file, data);
    }).Dump();
}

文件大小:7.21MB

测试结果

次数分配内存内存提高耗时
180,5529,51252
27,4322446
37,4322445
4120,400-1,07246
57,4322448

反序列化测试代码

void MessagePackNetRead()
{
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "UserMessagePackData.bin");
        byte[] bytes = File.ReadAllBytes(path);
        MessagePackSerializer.Deserialize<List<UserMessagePack>>(bytes);
    }).Dump();
}

测试结果

次数分配内存内存提高耗时
135,804,7282482
235,804,7282465
335,804,7282456
435,804,7283266
535,806,24884880

结论

序列化性能测试结果

性能测试名称分配内存平均数 (bytes)耗时平均数 (ms)文件大小 (MB)分配内存百分比 (%)耗时百分比 (%)文件大小百分比 (%)
Newtonsoft.Json1,494,489,8722,85514.3100100100
System.Text.Json1,429,784,0992,67814.39593.8100
ProtobufNet195,3851027.710.0133.553.9
MessagePack-Net44,649477.210.00291.650.4

反序列化性能测试结果

性能测试名称分配内存平均数 (bytes)耗时平均数 (ms)分配内存百分比 (%)耗时百分比 (%)
Newtonsoft.Json92,531,496283100100
System.Text.Json42,807,42015246.254.7
ProtobufNet28,529,0729030.831.8
MessagePack-Net35,805,0326938.624.3

注:

1、 分配内存比例、耗时比例和文件大小比例都以 Newtonsoft.Json 的数值为基准,计算出的百分比表示在相比于 Newtonsoft.Json 的表现。
2、 分配内存平均数、耗时平均数是通过将给定的五次测试结果取平均值得出的。
3、 文件大小是由测试代码生成的文件大小,计算出的百分比表示在相比于 Newtonsoft.Json 的表现。

基于上述表格,我们可以得出以下结论:

  1. 内存分配:在内存分配方面,ProtobufNet 和 MessagePack-Net 显著优于 System.Text.Json 和 Newtonsoft.Json。它们的内存分配仅为 Newtonsoft.Json 的 0.01% 和 0.003%,这表明它们在处理大数据时的内存效率非常高。
  2. 耗时:在耗时方面,ProtobufNet 和 MessagePack-Net 也表现出超过其他两个库的性能。ProtobufNet 的耗时为 Newtonsoft.Json 的 3.6%,而 MessagePack-Net 的耗时仅为 2.1%。这意味着它们在处理大量数据时的速度非常快。
  3. 文件大小:在生成的文件大小方面,ProtobufNet 和 MessagePack-Net 的文件大小明显小于 System.Text.Json 和 Newtonsoft.Json。ProtobufNet 和 MessagePack-Net 的文件大小分别为 Newtonsoft.Json 文件大小的 53.9% 和 50.4%。这说明它们的序列化效率更高,能够生成更小的文件。
  4. System.Text.Json vs Newtonsoft.Json:在比较这两个库时,System.Text.Json 在内存分配和耗时方面都稍微优于 Newtonsoft.Json,但差距不大。在文件大小方面,它们的表现相同。

综上所述,如果考虑内存分配、处理速度和文件大小,ProtobufNet 和 MessagePack-Net 的性能明显优于 System.Text.Json 和 Newtonsoft.Json。

5、总结

基于上面的数据,个人一些看法,虽然我们平常用的是Newtonsoft.Json,但了解一些其他一些比较好的库的使用可以扩展视野,本次测试的库虽然加入了写入文件这方面的因素,但对性能影响不是很大,本以为ProtobufNet已经是性能最好的了,但上面的测试结果,显然 MessagePack-Net 性能最好,还有一个意外发现,针对NetCore 6.0,新出的库System.Text.Json性能比Newtonsoft.Json好5%

作者 => 百宝门瞿佑明

WPF动画入门教程

Windows Presentation Foundation (WPF)是一种用于创建Windows客户端应用程序的UI框架。它让我们能够创建丰富的图形界面,包括各种各样的动画效果。接下来,我们将介绍如何在WPF中创建简单的动画。文章最后将给出源码,源码包括文章中的动画和一个水印按钮,一个简单的时钟动画,一个复杂的时钟动画。

在WPF中,通常会使用以下的一些标签来创建和控制动画。

  1. Storyboard:

Storyboard 是 Window Presentation Foundation (WPF) 中一种强大的工具,可用于创建自定义动画效果。WPF 中的动画是通过变化特定属性的值来产生的,并且这些变化都是随时间而进行的。

Storyboard 主要特性和功能:

时间线控制: Storyboard 允许你控制动画时间线,包括开始时间,停止时间,持续时间等。

动画类型: Storyboard 支持各种类型的动画,如双精度动画,颜色动画,点动画等。

复杂动画: 通过组合多个动画效果,你可以创建复杂的动画。这可以通过在 Storyboard 中包含多个动画实现。

控制动画流程: Storyboard 提供了开始,暂停,恢复,停止等方法来控制动画的播放流程。

交互性: 在 XAML 中,可以通过Storyboard.TargetNameStoryboard.TargetProperty 属性来指定应用动画的对象与 property。

  1. Animation:

WPF中的动画通常通过更改属性的值来产生动画效果。例如,我们可以使用DoubleAnimation,它可以在指定的时间内将目标属性的值从一个浮点数改变为另一个浮点数。除了DoubleAnimation,WPF还提供了其他类型的Animation,如ColorAnimation、PointAnimation等。

  1. From, To, Duration:

From和To指定了动画的开始和结束值,而Duration决定了动画的持续时间。

  1. Storyboard.TargetName 和 Storyboard.TargetProperty:

这两个属性分别用于指定动画的目标对象和目标属性。

  1. Triggers:

Triggers类用于设定启动动画的条件。我们通常会在其中设定一些事件触发条件,比如按钮被点击。当事件被触发时,设定的动画效果就会开始执行。

以上就是WPF中常用的一些动画元素。要创建复杂的动画效果,你可能还需要了解更多的标签和属性,比如RepeatBehavior(用于设定动画的重复行为)、AutoReverse(用于设定动画播放结束后是否自动倒回)、KeyFrames(用于设定动画的关键帧)等等。

接下来,我们将介绍如何在WPF中创建简单的动画。

需要的工具:

Visual Studio

步骤 1:创建一个新的WPF项目

在Visual Studio中,通过点击文件 -> 新建 -> 项目来创建一个新的WPF应用程序。

步骤 2:向窗体中添加控件

在主窗口 MainWindow.xaml 文件中,我们将添加一个Button控件。我们将为此控件添加一个简单的动画效果。

<Window x:Class="WpfAnimationDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WPF Animation Demo" Height="350" Width="525">
    <Grid>
        <Button Name="DemoButton" Content="Click me" Width="100" Height="50"/>
    </Grid>
</Window>

步骤 3:编写动画效果

我们创建一个当用户点击按钮时执行的动画效果。这个效果将使按钮的宽度在1秒钟内扩大到200。

通过代码实现

给button增加Click方法

<Button x:Name="DemoButton" Width="100" Height ="100" Content="Animate Me!" Click="DemoButton_Click"
        Grid.Row="0" Grid.Column="0"/>

“`csharp
private void DemoButton_Click(object sender, RoutedEventArgs e)
{
DoubleAnimation widthAnimation = new DoubleAnimation();
widthAnimation.From = 100; // 起始值
widthAnimation.To = 300; // 结束值
widthAnimation.Duration = new Duration(TimeSpan.FromSeconds(1)); // 动画长度

Storyboard storyboard = new Storyboard();
storyboard.Children.Add(widthAnimation);

Storyboard.SetTarget(widthAnimation, DemoButton);
Storyboard.SetTargetProperty(widthAnimation, new PropertyPath(Button.WidthProperty));

storyboard.Begin();

}

这个方法是 DemoButton 的点击事件处理器。当点击这个按钮时,这个方法就会被调用。

点击时将会发生动画效果,按钮的宽度内部值从100逐渐变化到300,过程时间为1秒。这是通过WPF中的 Storyboard 和 DoubleAnimation 来完成的。

Storyboard 是动画的容器,而 DoubleAnimation 是这个动画的定义。设置起始值(From)、结束值(To)、动画的持续时间(Duration),并确认动画的目标(要改变的是哪个元素的哪个属性)。

最后,调用 Storyboard 的 Begin 方法以开始动画。

完整代码如下:

csharp
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

private void DemoButton_Click(object sender, RoutedEventArgs e)
{
    DoubleAnimation widthAnimation = new DoubleAnimation();
    widthAnimation.From = 100; // 起始值
    widthAnimation.To = 300; // 结束值
    widthAnimation.Duration = new Duration(TimeSpan.FromSeconds(2)); // 动画长度

    Storyboard storyboard = new Storyboard();
    storyboard.Children.Add(widthAnimation);

    Storyboard.SetTarget(widthAnimation, DemoButton);
    Storyboard.SetTargetProperty(widthAnimation, new PropertyPath(Button.WidthProperty));

    storyboard.Begin();
}

}

### 通过xaml实现

下面我们用xaml来实现同样的效果。

xml
   

xml

   

这个<Window...>标签用于定义整个窗口的开始和结束。

xml

<Grid> 是WPF内的一种特布面板标签,它提供了一个灵活的格子系统,用于在多行和多列中进行UI元素布局。

xml
在这里,我们定义了一个按钮(Button)。Name属性是给按钮设定的名称,它在XAML和代码之间可以进行关联;Content属性设置按钮的文本为"Click me";Width和Height属性则设置了按钮的宽度和高度。 xml
Triggers标签指定触发器,它定义在一定的条件下触发某些行为。 xml
此处定义了一个EventTrigger事件触发器。该触发器在Button.Click事件——也就是按钮被点击的事件——发生时触发。 xml
BeginStoryboard会使得包含在其中的Storyboard开始播放。 xml
Storyboard是WPF中对动画的最高级别的封装。一个Storyboard可以包含多个动画,这些动画会在BeginStoryboard命令下同步启动。 xml
这段代码定义了一个DoubleAnimation双值动画。 这个动画的目标对象通过Storyboard.TargetName属性设置为myButton,也就是我们前面定义的按钮控件;目标动画属性通过Storyboard.TargetProperty设定为Width;From和To属性定义了动画开始和结束时Width的值;Duration定义了动画从开始到结束的持续时间。这里设定的动画效果是,在1秒的时间内,按钮的宽度从100变为200。 xml







“`

以上是各个元素的结束标签,用于指定相应元素的结束位置。

最终,这段XAML代码定义了一个窗口,窗口中有一个按钮。当该按钮被点击时,它的宽度将在1秒的时间内从100变为200,从而形成一个视觉上的动画效果。

步骤 4:运行你的动画

保存你的代码,运行程序,然后点击按钮观察动画效果。

运行效果

代码位置: https://github.com/DXG88/WPF.Animation

.NET中测量多线程基准性能

多线程基准性能是用来衡量计算机系统或应用程序在多线程环境下的执行能力和性能的度量指标。它通常用来评估系统在并行处理任务时的效率和性能。测量中通常创建多个线程并在这些线程上执行并发任务,以模拟实际应用程序的并行处理需求。

在此,我们用多个线程来完成一个计数任务,简单地测量系统的多线程基准性能,以下的5种测量代码(代码1,代码4,代码5,代码6,代码7)中,都设置了计数器,每一秒计数器的计数量体现了系统的性能。通过对比这些测量方法,可以直观地理解多线程、如何通过多线程充分利用系统性能,以及运行多线程可能存在的瓶颈。

测量方法

先用一个多线程的共享变量自增例子来做多线程基准性能测量:

//代码1:简单的多线程测量多线程基准性能
long totalCount = 0;
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}
while (true)
{
    long t = totalCount;
    Thread.Sleep(1000);
    Console.WriteLine($"{totalCount - t:N0}");
}
void DoWork()
{
    while (true)
    {
        totalCount++;
    }
}

//结果
48,493,031
48,572,321
47,788,843
48,128,734
50,461,679
……

因为在多线程环境中,线程之间的切换会导致一些开销,例如保存和恢复线程上下文的时间。如果上下文切换频繁发生,可能会对性能测试结果产生影响,因此上面的代码根据系统的CPU内核数设定启动测试线程的线程数量,这些线程对一个共享的变量进行自增操作。

有多线程编程经验的人不难看出,上面的代码没有正确地保护共享资源,会出现竞态条件。这可能导致数据不一致,操作顺序不确定,或者无法重现一致的性能结果。我们将用代码展示这种情况。

//代码2:展示出竞态条件的代码
long totalCount = 0;
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}
void DoWork()
{
    while (true)
    {
        totalCount++;
        Console.Write($"{totalCount}"+",");
    }
}
//结果
1,9,10,3,12,13,4,14,15,16……270035,269913,270037,270038,270036,270040,269987,270042,270043……

代码2的运行结果可以看到,由于被不同线程操作,这些线程同时访问和修改totalCount的值,打印出来的totalCount不是顺序递增的。

可见,代码1没有线程同步机制,我们不能准确测量多线程基准性能。
C#中线程的同步方式,比如传统的锁机制(如lock语句、Monitor类、Mutex类、Semaphore类等)通常使用互斥机制来保护共享资源,以确保同一时间只有一个线程可以访问资源,避免竞争条件。这些锁机制会在代码块被锁定期间阻塞其他线程的访问,使得同一时间只有一个线程可以执行被锁定的代码。
这里使用lock锁作为线程同步机制,修正上面的代码,对共享的变量进行保护,避免共享变量同时被多个线程修改。

//代码3:使用lock锁
long totalCount = 0;
int threadCount = Environment.ProcessorCount;
object totalCountLock = new object();

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}

void DoWork()
{
    while (true)
    {
        lock (totalCountLock)
        {
            totalCount++;
            Console.Write($"{totalCount}"+",");
        }
    }
}

//结果
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30……

这时的结果就是顺序输出。

我们用含lock的代码来测量多线程基准性能:

//代码4:运用含lock锁的代码测量多线程基准性能
long totalCount = 0;
int threadCount = Environment.ProcessorCount;
object totalCountLock = new object();

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}
while (true)
{
    long t = totalCount;
    Thread.Sleep(1000);
    Console.WriteLine($"{totalCount - t:N0}");
}
void DoWork()
{
    while (true)
    {
        lock (totalCountLock)
        {
            totalCount++;
        }
    }
}

//结果
16,593,517
16,694,824
16,514,421
16,517,431
16,652,867
……

保证多线程环境下线程安全性,还有一种方式是使用原子操作Interlocked。与传统的锁机制(如lock语句等)不同,Interlocked类提供了一些特殊的原子操作,如Increment、Decrement、Exchange、CompareExchange等,用于对共享变量进行原子操作。这些原子操作是直接在CPU指令级别上执行的,而不需要使用传统的阻塞和互斥机制。它通过硬件级别的操作,确保对共享变量的操作是原子性的,避免了竞争条件和数据不一致的问题。
它更适合用于特定的原子操作,而不是用作通用的线程同步机制。

//代码5:运用原子操作的代码测量多线程基准性能
long totalCount = 0;
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}

while (true)
{
    long t = totalCount;
    Thread.Sleep(1000);
    Console.WriteLine($"{totalCount - t:N0}");
}

void DoWork()
{
    while (true)
    {
        Interlocked.Increment(ref totalCount);
    }
}
//结果
37,230,208
43,163,444
43,147,585
43,051,419
42,532,695
……

除了使用互斥锁、原子操作,我们也可以设法对多个线程进行数据隔离。ThreadLocal类提供了线程本地存储功能,用于在多线程环境下的数据隔离。每个线程都会有自己独立的数据副本,被储存在ThreadLocal实例中,每个ThreadLocal可以被对应线程访问到。

//代码6:运用含ThreadLocal的代码测量多线程基准性能
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
ThreadLocal<long> count = new ThreadLocal<long>(trackAllValues: true);

for (int i = 0; i < threadCount; ++i)
{
    int threadId = i;
    tasks[i] = Task.Run(() => DoWork(threadId));
}

while (true)
{
    long old = count.Values.Sum();
    Thread.Sleep(1000);
    Console.WriteLine($"{count.Values.Sum() - old:N0}");
}

void DoWork(int threadId)
{
    while (true)
    {
        count.Value++;
    }
}

//结果
177,851,600
280,076,173
296,359,986
296,140,821
295,956,535
……

上面的代码使用了ThreadLocal类,我们也可以自定义一个类,给每个线程创建一个对象作为上下文,代码如下:

//代码7:运用含自定义上下文的代码测量多线程基准性能
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
Context[] ctxs = new Context[threadCount];

for (int i = 0; i < threadCount; ++i)
{
    int threadId = i;
    ctxs[i] = new Context();
    tasks[i] = Task.Run(() => DoWork(threadId));
}

while (true)
{
    long old = ctxs.Sum(v => v.TotalCount);
    Thread.Sleep(1000);
    Console.WriteLine($"{ctxs.Sum(v => v.TotalCount) - old:N0}");
}

void DoWork(int threadId)
{
    while (true)
    {
        ctxs[threadId].TotalCount++;
    }
}

class Context
{
    public long TotalCount = 0;
}

//结果:
1,067,502,570
1,100,966,648
1,145,726,019
1,110,163,963
1,069,322,606
……

系统配置

组件    规格                                
CPU      11th Gen Intel(R) Core(TM) i5-11300H
内存    16 GB DDR4                  
操作系统Microsoft Windows 10 家庭中文版          
电源选项已设置为高性能                      
软件    LINQPad 7.8.5 Beta                  
运行时  .NET 7.0.10                          

测量结果

测量方法1秒计数      性能百分比
未做线程同步    50,461,679118.6%
lock锁   16,652,86739.2%
原子操作(Interlocked)   42,532,695100%
ThreadLocal    295,956,535695.8%
自定义上下文(Context)    1,069,322,6062514.1%

结果分析

未作线程同步测量到的结果是不准确的,不能作为依据。

根据程序运行的结果可以看到,使用传统的lock锁机制,效率不高。使用原子操作Interlocked,效率比传统锁要高近2倍。
而实现了线程间隔离的2种方法,效率都比前面的方法要高。使用自定义上下文的程序效率是最高的。

线程间隔离的两种代码,它们主要区别在于线程安全性的实现方式。代码6使用ThreadLocal 类来实现,而代码7使用了自定义的上下文,用一个数组来为每个线程提供一个唯一的上下文。代码6使用的是线程本地存储(Thread Local Storage,TLS)来实现其功能。它是一种全局变量,可以被正在运行的所有线程访问,但每个线程所看到的值都是私有的。虽然这个特性使ThreadLocal在多线程编程中变得非常有用,但为了实现这个特性,它在内部实现了一套复杂的机制,比如它会创建一个弱引用的哈希表来存储每个线程的数据。这个内部实现细节增加了相应的计算和访问开销。

对于代码7,它创建了一个名为Context的类数组,每个线程都有其自己的Context对象,并在执行过程中修改这个对象。由于每个线程自身管理其Context对象,不存在任何线程间冲突,这就减少了许多额外的开销。

因此,虽然代码6代码7都实现了线程数据隔离,但代码7避开了ThreadLocal的额外开销,因此在性能上表现得更好。

结论

如果能实现线程间的隔离,可以大幅提高多线程代码效率,测量出系统的最大性能值。

作者:百宝门-后端组-周智

C#中的ConcurrentExclusiveSchedulerPair类

为什么使用ConcurrentExclusiveSchedulerPair?

现实生活中的例子是一个停车场的入口和出口,多辆车可以同时进入和离开停车场,但是只有一个车辆可以进入或离开一次。

这时候就需要保证同时只有一个车辆能够访问停车场的入口或出口,避免出现多辆车同时进出停车场的竞态条件和导致车辆堵塞的问题。

使用ConcurrentExclusiveSchedulerPair可以将需要独占访问的停车场入口和出口操作加入ExclusiveScheduler中,从而保证在任何时候都只有一个车辆能够进入或离开停车场,避免了竞态条件和数据不一致的情况。

ConcurrentExclusiveSchedulerPair类介绍

ConcurrentExclusiveSchedulerPair类是.NET Framework 4.0中引入的一种新的多线程编程工具,它提供了两个调度器,一个是并发调度器(ConcurrentScheduler),另一个是独占调度器(ExclusiveScheduler)。通过这两个调度器,可以实现多个任务的并行执行和互斥访问。

以下是创建ConcurrentExclusiveSchedulerPair对象的基本代码:

var pair = new ConcurrentExclusiveSchedulerPair();

在上述代码中,我们创建了一个ConcurrentExclusiveSchedulerPair对象。这个对象包含了两个调度器:并发调度器和独占调度器。

并发调度器

并发调度器是一种可以让多个任务并行执行的调度器。在并发调度器中,任务可以同时执行,而不需要等待其他任务完成。

以下是使用并发调度器来执行任务的示例:

var pair = new ConcurrentExclusiveSchedulerPair();
var concurrentScheduler = pair.ConcurrentScheduler;

Task.Factory.StartNew(() =>
{
    // 任务执行的代码
}, CancellationToken.None, TaskCreationOptions.None, concurrentScheduler);

在上述代码中,我们获取了ConcurrentExclusiveSchedulerPair对象的并发调度器,并使用Task.Factory.StartNew方法来创建一个任务,并使用并发调度器来调度任务的执行。

独占调度器

独占调度器是一种可以让任务独占执行的调度器。在独占调度器中,只有一个任务可以执行,其他任务必须等待前一个任务完成后才能执行。

以下是使用独占调度器来执行任务的示例:

var pair = new ConcurrentExclusiveSchedulerPair();
var exclusiveScheduler = pair.ExclusiveScheduler;

Task.Factory.StartNew(() =>
{
    // 任务执行的代码
}, CancellationToken.None, TaskCreationOptions.None, exclusiveScheduler);

在上述代码中,我们获取了ConcurrentExclusiveSchedulerPair对象的独占调度器,并使用Task.Factory.StartNew方法来创建一个任务,并使用独占调度器来调度任务的执行。

下面是完整案例

var pair = new ConcurrentExclusiveSchedulerPair();

var concurrentTaskFactory = new TaskFactory(pair.ConcurrentScheduler);
var exclusiveTaskFactory = new TaskFactory(pair.ExclusiveScheduler);
// 调度独占任务
exclusiveTaskFactory.StartNew(() =>
{
    Console.WriteLine("线程:{0}上正在执行独占任务1", Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(1000);
});
exclusiveTaskFactory.StartNew(() =>
{
    Console.WriteLine("线程:{0}上正在执行独占任务2", Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(1000);
});

// 等待所有任务完成
Task.WaitAll(
concurrentTaskFactory.StartNew(() =>
{
    Console.WriteLine("并发任务3在线程:{0}上执行", Thread.CurrentThread.ManagedThreadId);
}),
exclusiveTaskFactory.StartNew(() =>
{
    Console.WriteLine("独占任务3正在线程:{0}上执行", Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(1000);
}));

输出结果

线程:15上正在执行独占任务1

线程:15上正在执行独占任务2

独占任务3正在线程:15上执行

并发任务3在线程:15上执行

结论

总之,使用 ConcurrentExclusiveSchedulerPair 的目的是为了保证在高并发情况下,多个任务对共享资源进行读写操作时不会产生竞态条件和数据不一致的问题。这可以提高应用程序的稳定性和可靠性。

作者 => 百宝门瞿佑明