分析器跟踪把我带到了一个从未怀疑过的代码路径:一个请求处理器,它读取一个由空格分隔的令牌块,并对其识别的每个令牌累加一个权重。逻辑很枯燥,也没有错误。引起我注意的是,一个仅读取这些令牌的热循环里竟然产生了7.8MB的字符串分配。我根本没保留过任何一个令牌。我只是分配一个键,做一次字典查找,然后随手丢弃。
这些键都是子字符串。我从整个块中切出的每一个令牌都变成了一个小小的字符串对象,目的仅仅是把它们交给Dictionary.TryGetValue。于是我测量了这种习惯的真实代价,以及基于span的替代查找能扳回多少性能。
![]()
输入是一个包含20万个空格分隔令牌的字符串,大小约1.5MB。其中大约70%是存在于一个包含5000个条目的Dictionary中的有效键,其余都是未命中。整个处理过程遍历块,提取每个令牌,进行查找并累加权重。计时结果是在预热后9轮的中位数,分配数据来自GC.GetAllocatedBytesForCurrentThread,环境为.NET 10、Release构建、小型Linux容器。这不是实验室数据,我更关心比率。
下面是我过去一直写的版本:
int start = 0;for (int i = 0; i <= input.Length; i++)if (i == input.Length || input[i] == ' ')string token = input.Substring(start, i - start); // 产生分配if (table.TryGetValue(token, out long w)) sum += w;start = i + 1;Substring就是问题的征兆。它读起来太像理所当然的写法,所以从没在代码审查中被揪出来。对于少量令牌,它确实完全没问题。但20万个令牌的账单如下:
[A: Substring + 查找] 总和=6,963,210 中位数=23.4ms 分配=7,812KB
整整7.5MB的键字符串,没有一个能活过一个if语句。在一个繁忙的端点上,这意味着每秒产生7.5MB的垃圾。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.