一:背景 1. 讲故事
在我们的一个全内存项目中,需要将一家大品牌店铺小千万的trade灌入到内存中,大家知道trade中一般会有订单来源,省市区 ,当把这些字段灌进去后,你会发现他们特别侵蚀内存,因为都是字符串类型,不知道大家对内存侵蚀性是不是很清楚,我就问一个问题。
Answer: 一个空字符串占用多大内存? 你知道吗?
思考之后,下面我们就一起验证下,使用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]])