为啥使用 System.Text.Json 的 JSON 反序列化这么慢?

Posted

技术标签:

【中文标题】为啥使用 System.Text.Json 的 JSON 反序列化这么慢?【英文标题】:Why is JSON deserialisation with System.Text.Json so slow?为什么使用 System.Text.Json 的 JSON 反序列化这么慢? 【发布时间】:2021-11-24 01:33:27 【问题描述】:

我有一个相同的最小项目,它将用 C# 和 Go 编写的 json 反序列化 100,000 次。性能差异很大。虽然很高兴知道使用 Go 可以实现性能目标,但我更愿意在 C# 中实现类似的结果。鉴于 C# 慢了 193 倍,我认为错误在我这边,但我不知道为什么。

性能

$ dotnet run .
real    1m37.555s
user    1m39.552s
sys     0m0.729s

$ ./jsonperf
real    0m0.478s
user    0m0.500s
sys     0m0.011s

C#源代码

using System;

namespace jsonperf

    class Program
    
        static void Main(string[] args)
        
            var json = "\"e\":\"trade\",\"E\":1633046399882,\"s\":\"BTCBUSD\",\"t\":243216662,\"p\":\"43818.22000000\",\"q\":\"0.00452000\",\"b\":3422298876,\"a\":3422298789,\"T\":1633046399882,\"m\":false,\"M\":true";

            for (int i = 0; i < 100000; i++)
            
                if (0 == i % 1000)
                
                    Console.WriteLine($"Completed: i");
                

                var obj = BinanceTradeUpdate.FromJson(json);
            

            Console.WriteLine("Done");
        
    


using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace jsonperf

    public class BinanceTradeUpdate
    
        [JsonPropertyName("e")]
        public string EventType
        
            get;
            set;
        

        [JsonPropertyName("E")]
        public long EventUnixTimestamp
        
            get;
            set;
        

        [JsonIgnore]
        public DateTime EventTime
        
            get
            
                return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(EventUnixTimestamp);
            
        

        [JsonPropertyName("s")]
        public string MarketSymbol
        
            get;
            set;
        

        [JsonPropertyName("t")]
        public long TradeId
        
            get;
            set;
        

        [JsonPropertyName("p")]
        public double Price
        
            get;
            set;
        

        [JsonPropertyName("q")]

        public double Quantity
        
            get;
            set;
        

        [JsonPropertyName("b")]
        public long BuyerOrderId
        
            get;
            set;
        

        [JsonPropertyName("a")]
        public long SellerOrderId
        
            get;
            set;
        

        [JsonPropertyName("T")]
        public long TradeUnixTimestamp
        
            get;
            set;
        

        [JsonIgnore]
        public DateTime TradeTime
        
            get
            
                return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(TradeUnixTimestamp);
            
        

        [JsonPropertyName("m")]
        public bool BuyerIsMarketMaker
        
            get;
            set;
        

        [JsonPropertyName("M")]
        public bool UndocumentedFlag
        
            get;
            set;
        

        public static BinanceTradeUpdate FromJson(string json)
        
            return JsonSerializer.Deserialize<BinanceTradeUpdate>(
                json,
                new JsonSerializerOptions()
                
                    NumberHandling = JsonNumberHandling.AllowReadingFromString
                );
        
    

源码Go

package main

import (
  "encoding/csv"
  "encoding/json"
  "fmt"
  "os"
  "strconv"
)

type Float64Str float64

func (f *Float64Str) UnmarshalJSON(b []byte) error 
  var s string

  // Try to unmarshal string first
  if err := json.Unmarshal(b, &s); err == nil 

    value, err := strconv.ParseFloat(s, 64)

    if err != nil 
      return err
    

    *f = Float64Str(value)
    return nil
  

  // If unsuccessful, unmarshal as float64
  return json.Unmarshal(b, (*float64)(f))


// Trade represents an exchange of assets in a given market
type Trade struct 
  EventType     string     json:"e"
  EventTime     int64      json:"E"
  MarketSymbol  string     json:"s"
  TradeID       int64      json:"t"
  Price         Float64Str json:"p"
  Quantity      Float64Str json:"q"
  BuyerOrderID  int64      json:"b"
  SellerOrderID int64      json:"a"
  TradeTime     int64      json:"T"
  IsBuyerMaker  bool       json:"m"
  Flag          bool       json:"M"


func main() 

  jsonString := "\"e\":\"trade\",\"E\":1633046399882,\"s\":\"BTCBUSD\",\"t\":243216662,\"p\":\"43818.22000000\",\"q\":\"0.00452000\",\"b\":3422298876,\"a\":3422298789,\"T\":1633046399882,\"m\":false,\"M\":true"

  // open stdout
  var stdwrite = csv.NewWriter(os.Stdout)

  // convert string several times into obj
  var trade = Trade
  counter := 0

  for i := 0; i < 100000; i++ 
    if err := json.Unmarshal([]byte(jsonString), &trade); err != nil 
      stdwrite.Flush()
      panic(err)
     else 
      counter++

      if counter%1000 == 0 
        fmt.Printf("%d elements read\n", counter)
      
    
  

【问题讨论】:

如果你想正确地进行基准测试,那么你需要使用基准测试工具。这将提供更公平的结果。此外,您需要删除所有控制台写入,这些写入非常慢并且会扭曲您的结果。您的 C# 代码也存在一些效率低下的问题,例如为每个交互创建一个新的 JsonSerializerOptions,而这应该是一个全局设置。 虽然你所说的一切都没有错,但请注意dotnet需要远远超过90s才能完成任务。向控制台打印 100 行,即 VM 初始化在这里是舍入错误。我之前通过将 .FromJson() 包装在对 System.Disgnostic.Stopwatch .Start() 和 .Stop() 的调用中进行了测试。相差不到 2 秒。 好吧,如果我运行你的代码,那么我需要大约 83 秒。如果我使用只初始化一次的 JsonSerializerOptions 则需要 232 毫秒。删除 Writeline 可以再节省约 30 毫秒,这意味着性能提升 > 12%(超过 232 毫秒)。 起首,我永远不会认为 JsonSerializerOptions 的初始化是昂贵的 ab 操作。这对我有很大帮助;非常感谢。 快速查看源代码,表明每种类型的元数据都缓存在 JsonSerializerOptions 中。对于您的用例,静态缓存会更快,但这会阻止类型信息被垃圾收集。 github.com/dotnet/runtime/blob/main/src/libraries/… 【参考方案1】:

这需要这么长时间的原因是您每次都在初始化一个新的 JsonSerializerOptions 对象。

初始化序列化器一次,您将看到巨大的性能提升(对我来说是 70% 以上)。

【讨论】:

以上是关于为啥使用 System.Text.Json 的 JSON 反序列化这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

使用 System.Text.Json 从 json 文件读取到 IEnumerable

如何使用 System.Text.Json 忽略错误值

使用 System.Text.Json 修改 JSON 文件

使用 System.Text.Json 转换不一致的 json 值 [重复]

(de) 使用 System.Text.Json 序列化流

使用 .NET core 3.0/System.text.Json 解析 JSON 文件