Go语言中如何将unique包与字符串内化成?
摘要:最近在做老系统优化,正好遇到了需要使用字符串内部化的场景,所以今天就来说说字符串内部化这种优化技巧。 什么是字符串内部化 熟悉Java或者python的开发者应该对“内部化”这种技术不陌生。内部化指的是对于内容完全相同的字符串变量,内存中只
最近在做老系统优化,正好遇到了需要使用字符串内部化的场景,所以今天就来说说字符串内部化这种优化技巧。
什么是字符串内部化
熟悉Java或者python的开发者应该对“内部化”这种技术不陌生。内部化指的是对于内容完全相同的字符串变量,内存中只保留一份数据,所有的变量都引用同一份数据,从而节约内存。
举个Java的例子:
public class StringInternDemo {
public static void main(String[] args) {
String s1 = new String("hello");
String s2 = "hello";
// 使用 intern 方法
String s3 = s1.intern();
System.out.println(s1 == s2); // false,因为 s1 是堆中新建的对象
System.out.println(s2 == s3); // true,因为 s3 指向字符串常量池中的 "hello"
}
}
例子中s3和s1是不同的两个字符串变量,但它们共享同一份字符串数据。在python中可以用sys.intern(str)实现类似的功能,而且python更进一步——对于长度短且不包含特殊字符的字符串默认会自动进行内部化。
可以看到所谓内部化,其实相当于创建了一个“字符串缓存”,我们可以把字符串放进缓存里,然后在需要的时候取出来复用,取的时候既可以用变量也可以用常量。不过这么做需要字符串类型本身是不可变的,因为所有相同内容的字符串变量共享同一份数据,如果其中一个变量意外修改了这份数据,其他和它共享的字符串变量都将受到“污染”。好在不管Java、python还是Golang,字符串类型都是不可变的。
那么字符串内部化的好处是什么呢?好处在于可以减少内存分配次数且节约内存用量,我们不必为了内容相同的字符串反复申请内存空间。不过和其他类型的缓存一样,如果命中率不够高那么这个缓存不仅不会带来任何提升反而还会浪费大量内存并加重gc负担。
字符串内部化在处理数据的反序列化时是一种很重要的优化手段。
举个例子,某个公司需要汇总处理每个员工的业绩数据,数据中包含员工所在部门和职位等信息。我们知道一个公司的员工可能有几千个几万个甚至几十万个,但公司里的部分数量往往不会超过三位数,职位分类也是如此,即使数据再多它们也只会有固定数量的取值不会增多,反过来员工的姓名就很少会有重复数据,你几乎可以总是预估姓名的数量小于等于员工总数且随着员工数量增加而增加。如果没有字符串内部化,每收到一条数据,我们就要重复创建部门名称和职务头衔这些字符串,数据量越大浪费的内存就越多;而如果我们能把这些名称头衔的字符串全部缓存起来,后续只要让新变量共享这些数据,就能带来非常可观的内存利用率提升和性能改善。
另外除了字符串,其他符合“低基数”(取值重量有限,但整体数量很大,比如上文的部门名称)特征的数据都可以利用内部化进行优化。
Golang中手动实现内部化
在了解了什么是内部化,并且看了Java的例子,现在我们可以讲讲在Golang里如何实现这一技术了。
最原始的实现是这样的:
type StringIntern struct {
m map[string]string
}
func (s *StringIntern) Intern(str string) string {
ret, ok := s.m[str]
if !ok {
ret = strings.Clone(str)
s.m[str] = ret
}
return ret
}
var si StringIntern
s1 := "hello"
s2 := si.Intern(s1)
用法上没有和Java有多少区别,代码也很简单,唯一需要解释的是在字符串存进map的时候我们需要clone一次,这是为了避免参数str是某个长字符串的子串,因为我们的map需要长期持有str,如果是上述情况,这个长字符串就会无法释放从而造成泄露。
如果是在反序列化场景使用,可能需要调用unsafe.String(bytes, length)来获取字符串避免不必要的内存分配。
这个方案足够应付大多数场景,但还有一个比较麻烦的问题——我们没有实现淘汰机制,这会导致内部化池的规模越来越大。
