【学到一个新名词】String interning(字符串驻留/字符串内部化)

发布时间 2023-11-23 15:50:43作者: ahfuzhang

作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!


在阅读 VictoriaMetrics v1.95.1 的命令行手册的时候,发现这样一段:

  -internStringCacheExpireDuration duration
     The expiry duration for caches for interned strings. See https://en.wikipedia.org/wiki/String_interning . See also -internStringMaxLen and -internStringDisableCache (default 6m0s)

什么是 String interning 呢?我通过了 wiki 链接学习了一下。
并且,我还找到了一个使用 String interning 技术的 golang 项目:https://github.com/josharian/intern . 作者还写了 blog: Interning strings in Go 来进一步介绍细节。

String interning 可以翻译为 字符串驻留 或者 字符串内部化。这个技巧用于节约频繁出现的字符串的空间占用,还可以用于频繁出现的字符串的比较的加速。
它的处理思路如下:

  1. 首先有一个全局的线程安全的键值对的字符串池;
类似于: map[string]string

然后把出现频率超级高的字符串存储在其中。

  1. 当出现新的字符串的时候,要先去字符串池中匹配。
    匹配到以后,程序就可以引用字符串池中的对象,而把当前引用的对象释放掉。
    当存在大量的这样内容相同的字符串的时候,这样做无疑是可以节省空间的。
    在这样的场景下,相当于时间换空间。

  2. 当字符串都来自字符串池,且需要频繁比较的时候,直接比较指针就可以确定是否是同一个字符串,而无需逐个字符比较。
    在这样的场景下,相当于空间换时间。

让我们再看看那个简单的 golang 实现的字符串内部化的源码:
see: https://github.com/josharian/intern/blob/master/intern.go

package intern

import "sync"

var (
	pool sync.Pool = sync.Pool{   // 作者想用 sync.Pool 来解决不引用时候的释放问题。但是并发环境下可能导致分配了多个键值对的字符串池。
		New: func() interface{} {    // sync.Pool 能够在并发环境下工作,不管怎么说,并发情况下使用不会出错。
			return make(map[string]string)
		},
	}
)

// String returns s, interned.
func String(s string) string {
	m := pool.Get().(map[string]string)
	c, ok := m[s]  // 这里要经过  1.计算字符串 hashcode; 2.hash 查找; 3.字符串内容比较。时间换空间的成本还是挺高的。
	if ok {
		pool.Put(m)
		return c  // 如果字符串池中存在,就置换为字符串池中的对象
	}
	m[s] = s  // 这里不会发生并发问题
	pool.Put(m)
	return s
}

// Bytes returns b converted to a string, interned.
func Bytes(b []byte) string {
	m := pool.Get().(map[string]string)
	c, ok := m[string(b)]  // string(b) 这里有个隐含的知识点:这种情况下编译器不会分配新的字符串对象。
	if ok {
		pool.Put(m)
		return c
	}
	s := string(b)
	m[s] = s
	pool.Put(m)
	return s
}

// todo: 这里还缺乏一个内容:当字符串都来自字符串池的时候,可以提供按照指针比较的方法
// 类似于:
//  isSmae := &str1==&str2 || str1==str2

看完了源码,这个字符串内部化似乎也没有很复杂很高深。或许某个存在大量重复字符串的场景中,我们很能用上这个技术。
Have fun. ?