c#压缩字符串的方法

 更新时间:2020年11月3日 15:20  点击:1913

一:背景

1. 讲故事

在我们的一个全内存项目中,需要将一家大品牌店铺小千万的trade灌入到内存中,大家知道trade中一般会有订单来源,省市区 ,当把这些字段灌进去后,你会发现他们特别侵蚀内存,因为都是字符串类型,不知道大家对内存侵蚀性是不是很清楚,我就问一个问题。

Question: 一个空字符串占用多大内存? 你知道吗?

思考之后,下面我们就一起验证下,使用windbg去托管堆一查究竟,代码如下:

 static void Main(string[] args)
 {
  string s = string.Empty;

  Console.ReadLine();
 }

0:000> !clrstack -l
OS Thread Id: 0x308c (0)
 Child SP  IP Call Site
ConsoleApp6.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp6\Program.cs @ 19]
 LOCALS:
 0x00000087391febd8 = 0x000002605da91420
0:000> !DumpObj /d 000002605da91420
Name: System.String
String: 
Fields:
  MT Field Offset   Type VT Attr  Value Name
00007ff9eb2b85a0 4000281 8  System.Int32 1 instance  0 m_stringLength
00007ff9eb2b6838 4000282 c  System.Char 1 instance  0 m_firstChar
00007ff9eb2b59c0 4000286 d8 System.String 0 shared  static Empty
     >> Domain:Value 000002605beb2230:NotInit <<
0:000> !objsize 000002605da91420
sizeof(000002605da91420) = 32 (0x20) bytes (System.String)

从图中你可以看到,仅仅一个空字符串就要占用 32byte,如果500w个空字符串就是: 32byte x 500w = 152M,是不是不算不知道,一算吓一跳。。。 这还仅仅是一个什么都没有的空字符串哦。

2. 回归到Trade

问题也已经摆出来了,接下来回归到Trade中,为了方便演示,先模拟以文件的形式从数据库读取20w的trade。

 class Program
 {
 static void Main(string[] args)
 {
  var trades = Enumerable.Range(0, 20 * 10000).Select(m => new Trade()
  {
  TradeID = m,
  TradeFrom = File.ReadLines(Environment.CurrentDirectory + "//orderfrom.txt")
     .ElementAt(m % 4)
  }).ToList();

  GC.Collect(); //方便测试,把临时变量清掉
  Console.WriteLine("执行成功");
  Console.ReadLine();
 }
 }

 class Trade
 {
 public int TradeID { get; set; }
 public string TradeFrom { get; set; }
 }

然后用windbg去跑一下托管堆,再量一下trades的大小。

0:000> !dumpheap -stat
Statistics:
  MT Count TotalSize Class Name
00007ff9eb2b59c0 200200 7010246 System.String

0:000> !objsize 0x000001a5860629a8
sizeof(000001a5860629a8) = 16097216 (0xf59fc0) bytes (System.Collections.Generic.List`1[[ConsoleApp6.Trade, ConsoleApp6]])

从上面输出中可以看到托管堆有200200 = 20w(程序分配)+ 200(系统分配)个,然后再看size: 16097216/1024/1024= 15.35M,这就是展示的所有原始情况。

二:压缩技巧分析

1. 使用字典化处理

其实在托管堆上有20w个字符串,但你仔细观察一下会发现其实就是4种状态的重复显示,要么一淘,要么淘宝。。。这就给了我优化机会,何不在获取数据的时候构建好OrderFrom的字典,然后在trade中附增一个TradeFromID记录字典中的映射值,因为特征值少,用byte就可以了,有了这个思想,可以把代码修改如下:

 class Program
 {
 public static Dictionary<int, string> orderfromDict = new Dictionary<int, string>();

 static void Main(string[] args)
 {
  var trades = Enumerable.Range(0, 20 * 10000).Select(m =>
  {
  var tradefrom = File.ReadLines(Environment.CurrentDirectory + "//orderfrom.txt")
     .ElementAt(m % 4);

  var kv = orderfromDict.FirstOrDefault(k => k.Value == tradefrom);

  if (kv.Key == 0)
  {
   orderfromDict.Add(orderfromDict.Count + 1, tradefrom);
  }

  var trade = new Trade() { TradeID = m, TradeFromID = (byte)kv.Key };

  return trade;

  }).ToList();

  GC.Collect(); //方便测试,把临时变量清掉

  Console.WriteLine("执行成功");

  Console.ReadLine();
 }
 }

 class Trade
 {
 public int TradeID { get; set; }

 public byte TradeFromID { get; set; }

 public string TradeFrom
 {
  get
  {
  return Program.orderfromDict[TradeFromID];
  }
 }
 }

代码还是很简单的,接下来用windbg看一下空间到底压缩了多少?

0:000> !dumpheap -stat
Statistics:
  MT Count TotalSize Class Name
00007ff9eb2b59c0 204 10386 System.String

0:000> !clrstack -l
OS Thread Id: 0x2ce4 (0)
 Child SP  IP Call Site
ConsoleApp6.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp6\Program.cs @ 42]
 LOCALS:
 0x0000006f4d9ff078 = 0x0000016fdcf82ab8

0000006f4d9ff288 00007ff9ecd96c93 [GCFrame: 0000006f4d9ff288] 
0:000> !objsize 0x0000016fdcf82ab8
sizeof(0000016fdcf82ab8) = 6897216 (0x693e40) bytes (System.Collections.Generic.List`1[[ConsoleApp6.Trade, ConsoleApp6]])

从上面的输出中可以看到,托管堆上string现在是:204 = 4(程序分配) + 200(系统分配)个,这4个就是字典中的4个哦,空间的话:6897216 /1024/1024= 6.57M,对应之前的 15.35M优化了将近60%。

虽然优化了60%,但这种优化是破坏性的优化,需要修改我的Trade结构,同时还要定义个Dictionary,而且还有不小幅度的修改业务逻辑,大家都知道线上的代码是能不改则不改,不改肯定没错,改出问题肯定是你兜着走,是吧,那问题就来了,如何最小化的修改而且还能压缩空间,有这样两全其美的事情吗???

2. 利用字符串驻留池

貌似一说出来,大家都如梦初醒,驻留池的出现就是为了解决这个问题,CLR会在内部维护了一个我刚才定义的字典机制,重复的字符串就不需要在堆上再次分配,直接存它的引用地址即可,如果你不清楚驻留池,建议看一下我这篇: https://www.jb51.net/article/189450.htm

接下来只需要在tradefrom 字段包一层 string.Intern 即可,改动不要太小,代码如下:

 static void Main(string[] args)
 {
  var trades = Enumerable.Range(0, 20 * 10000).Select(m => new Trade()
  {
  TradeID = m,
  TradeFrom = string.Intern(File.ReadLines(Environment.CurrentDirectory + "//orderfrom.txt")
     .ElementAt(m % 4)), //包一层 string.Intern
  }).ToList();

  GC.Collect(); //方便测试,把临时变量清掉
  Console.WriteLine("执行成功");
  Console.ReadLine();
 }

然后用windbg抓一下托管堆。

0:000> !dumpheap -stat 
Statistics:
  MT Count TotalSize Class Name
00007ff9eb2b59c0 204 10386 System.String

0:000> !clrstack -l
OS Thread Id: 0x13f0 (0)
 Child SP  IP Call Site

ConsoleApp6.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp6\Program.cs @ 27]
 LOCALS:
 0x0000005e4d3ff0a8 = 0x000001f8a15129a8

0000005e4d3ff2b8 00007ff9ecd96c93 [GCFrame: 0000005e4d3ff2b8] 
0:000> !objsize 0x000001f8a15129a8
sizeof(000001f8a15129a8) = 8497368 (0x81a8d8) bytes (System.Collections.Generic.List`1[[ConsoleApp6.Trade, ConsoleApp6]])

观察后发现,当用了驻留池之后空间为: 8497368 /1024/1024 =8.1M,你可能有疑问,为什么和字典化相比内存要大24%呢? 仔细观察你会发现,当用驻留池后,List<Trade> 中的TradeFrom存的是string在堆中的内存地址,在x64机器上要占用8个字节,而字典化方式内存堆上Trade是不分配TradeFrom,而是用了一个byte来替代,总体来说相当于一个trade省了7byte的空间,然后用windbg看一下。

0:000> !da -length 1 -details 000001f8b16f9b68
Name: ConsoleApp6.Trade[]
Size: 2097176(0x200018) bytes
Array: Rank 1, Number of elements 262144, Type CLASS

 Fields:
   MT Field Offset   Type VT Attr  Value Name
 00007ff9eb2b85a0 4000001 10  System.Int32 1 instance   0 <TradeID>k__BackingField
 00007ff9eb2b59c0 4000002 8  System.String 0 instance 000001f8a1516030 <TradeFrom>k__BackingField

0:000> !DumpObj /d 000001f8a1516030
Name: System.String
String: WAP

可以看到, 000001f8a1516030 就是 堆上 string=Wap的引用地址,这个地址占用了8byte空间。

再回头dump一下使用字典化方式的Trade,可以看到它是没有 <TradeFrom>k__BackingField 字段的。

0:000> !da -length 1 -details 000001ed52759ac0
Name: ConsoleApp6.Trade[]
Size: 262168(0x40018) bytes
Array: Rank 1, Number of elements 32768, Type CLASS
 Fields:
   MT Field Offset   Type VT Attr  Value Name
 00007ff9eb2b85a0 4000002 8  System.Int32 1 instance   0 <TradeID>k__BackingField
 00007ff9eb2b7d20 4000003 c  System.Byte 1 instance   0 <TradeFromID>k__BackingField

三:总结

大家可以根据自己的情况使用,使用驻留池方式是改变最小的,简单粗暴,自己构建字典化虽然最省内存,但需要修正业务逻辑,这个风险自担哦。。。

以上就是c#压缩字符串的方法的详细内容,更多关于c#压缩字符串的资料请关注猪先飞其它相关文章!

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • c#中判断字符串是不是数字或字母的方法

    这篇文章介绍了C#判断字符串是否数字或字母的实例,有需要的朋友可以参考一下...2020-06-25
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • PostgreSQL判断字符串是否包含目标字符串的多种方法

    这篇文章主要介绍了PostgreSQL判断字符串是否包含目标字符串的多种方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-02-23
  • 详解C++ string常用截取字符串方法

    这篇文章主要介绍了C++ string常用截取字符串方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-04-25
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25