Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >聊一聊字符串内部化

聊一聊字符串内部化

作者头像
poslua
发布于 2019-08-21 08:27:57
发布于 2019-08-21 08:27:57
57100
代码可运行
举报
文章被收录于专栏:posluaposlua
运行总次数:0
代码可运行

缘起

字符串作为一种不可变值类型,在多数的语言里,其底层基本都是个只读的字节数组:一旦被创建,则不可被改写。正是因为其只读特性,如果有大量相同的字符串需要处理,那么在内存中就会保存多份,显然是非常浪费内存的。

对于 C 来说字符串本质上就是 constchar*;而对于 Lua,虽然字符串并不是以 \0 结尾,但是 TString 的数据本质上也是一个 constchar*

所谓字符串内部化(string interning),就是一种技术手段让相同的字符串在内存中只保留一份。这样就可以大幅降低内存占用,缩短字符串比较的时间。因为相同的字符串只需要保存一份在内存中,当用这个字符串做匹配时,比较字符串只需要比较地址是否相同就够了,而不必逐字节比较。于是时间复杂度就从 O(N) 降低到了 O(1)

目前基本上所有主流的语言都使用了这项技术。比如,Lua 5.2 以前所有的字符串会被内部化到一张表中,这张表挂在 global state 结构下,相同的字符串在同一 VM 只会存在一份

而 Go 的字符串,本质上是一个 reflect.StringHeader:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type StringHeader struct {
    Data uintptr
    Len  int
}

其中 Data 指针指向的是一个字符常量的地址,这个地址里面的内容是不可以被改变的,因为它是只读的,但是这个指针可以指向不同的地址。虽然相同的字符串是不同的 StringHeader,但是其内部实际上都指向相同的字节数组:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    str1 := "Hello, World!"
    str2 := "Hello, World!"
    // 这两个地址并不相同
    fmt.Printf("str add: %p, %p\n", &str1, &str2)

    x1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
    x2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))
    // 底层都是指向相同的 []byte
    fmt.Printf("data add: %#v, %#v\n", x1.Data, x2.Data)
}

需要注意的是 Go 的 string intern 仅仅针对的是编译期可以确定的字符串常量,如果是运行期间产生的字符串则不能被内部化。比如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 可以被 intern
s1 := "12"
s2 := "1"+"2"

// 不能被 intern
s3 := "12"
s4 := strconv.Itoa(12)

因为 string 的指针指向的内容是不可以更改的,所以每更改一次字符串,就得重新分配一次内存,之前分配空间的还得由 gc 回收,这是导致 string 操作低效的根本原因。

Hack it

了解了它的机制之后,我们可以试着来绕过其限制,来完成一个可以内部化所有字符串的实现。首先我们需要一个 pool,把所有的字符串都放到这个 pool 里,只要字符串在这个 pool 里只有一份(例如 Map 就是一个非常好的选择),就可以认为已经被 intern 了。下面是一个老外的实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
    "fmt"
    "strconv"
)

type stringInterner map[string]string

func (si stringInterner) Intern(s string) string {
    if interned, ok := si[s]; ok {
        return interned
    }
    si[s] = s
    return s
}

func main() {
    si := stringInterner{}
    s1 := si.Intern("12")
    s2 := si.Intern(strconv.Itoa(12))
    fmt.Println(stringptr(s1) == stringptr(s2)) // true
}

他在优化了他们的一个服务后,可以看到内存占用下降的非常明显:

string intern 除了可以显著降低内存外,还有一个优点就是降低相同字符串的比较时间:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
    "strings"
    "testing"
)

type stringInterner map[string]string

func (si stringInterner) Intern(s string) string {
    if interned, ok := si[s]; ok {
        return interned
    }
    si[s] = s
    return s
}

func benchmarkStringCompare(b *testing.B, count int) {
    s1 := strings.Repeat("a", count)
    s2 := strings.Repeat("a", count)
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        if s1 != s2 {
            b.Fatal()
        }
    }
}

func benchmarkStringCompareIntern(b *testing.B, count int) {
    si := stringInterner{}
    s1 := si.Intern(strings.Repeat("a", count))
    s2 := si.Intern(strings.Repeat("a", count))
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        if s1 != s2 {
            b.Fatal()
        }
    }
}

func BenchmarkStringCompare1(b *testing.B)   { benchmarkStringCompare(b, 1) }
func BenchmarkStringCompare10(b *testing.B)  { benchmarkStringCompare(b, 10) }
func BenchmarkStringCompare100(b *testing.B) { benchmarkStringCompare(b, 100) }

func BenchmarkStringCompareIntern1(b *testing.B)   { benchmarkStringCompareIntern(b, 1) }
func BenchmarkStringCompareIntern10(b *testing.B)  { benchmarkStringCompareIntern(b, 10) }
func BenchmarkStringCompareIntern100(b *testing.B) { benchmarkStringCompareIntern(b, 100) }

可以看到被 intern 的字符串对比时间基本是个常数,而未被 intern 的字符串比较呈现出一个 O(N) 趋势:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
BenchmarkStringCompare1-2               300000000            4.65 ns/op        0 B/op          0 allocs/op
BenchmarkStringCompare10-2              300000000            5.49 ns/op        0 B/op          0 allocs/op
BenchmarkStringCompare100-2             200000000            8.96 ns/op        0 B/op          0 allocs/op
BenchmarkStringCompareIntern1-2         500000000            3.69 ns/op        0 B/op          0 allocs/op
BenchmarkStringCompareIntern10-2        500000000            3.65 ns/op        0 B/op          0 allocs/op
BenchmarkStringCompareIntern100-2       500000000            3.65 ns/op        0 B/op          0 allocs/op

实际上 Go 对比两个字符串是否相等,首先会对比其长度,长度不同自然是不同的串,时间复杂度为 O(1);如果长度相同,再对比其底层字节数组地址,地址相同肯定是相同的串,时间复杂度仍然可以认为是 O(1);如果地址不同,则需要逐个对比字节,那么时间复杂度也就退化为了 O(N)

string intern 作为一种高效的手段,在 Go 内部也有不少应用,比如在 HTTP 中 intern 公用的请求头来避免多余的内存分配:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// commonHeader interns common header strings.
var commonHeader = make(map[string]string)

func init() {
    for _, v := range []string{
        "Accept",
        "Accept-Charset",
        "Accept-Encoding",
        "Accept-Language",
        "Accept-Ranges",
        "Cache-Control",
        // ...
    } {
        commonHeader[v] = v
    }
}

如果你在做缓存系统,或者是需要操作大量的字符串,不妨也考虑下 string intern 来优化你的应用。

参考文献

  • String interning in Go
  • Strings in Go
  • Go黑技巧
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 poslua 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Golang-optimization「2」: 字符串
本系列的第二个部分,本文来谈谈程序员们喜闻乐见的string在Golang中有哪些值得注意的优化点
Kevinello
2022/11/17
3700
Golang-optimization「2」: 字符串
Golang 语言怎么高效使用字符串?
在 Golang 语言中,string 类型的值是只读的,不可以被修改。如果需要修改,通常的做法是对原字符串进行截取和拼接操作,从而生成一个新字符串,但是会涉及内存分配和数据拷贝,从而有性能开销。本文我们介绍在 Golang 语言中怎么高效使用字符串。
frank.
2021/04/16
1.9K0
Go字符串拼接 ,那种性能最佳?
字符串是每一门编程语言必不可缺的数据类型,作为强大的Go语言也一样。在日常的开发工作中,对Go字符串的操作是必不可少的,但不同的操作方式,其性能也是不同的。本文将从五大操作方式来进行演示,到底哪一种操作方式最佳。
兔云小新LM
2023/02/28
3.2K0
Go字符串拼接 ,那种性能最佳?
对比Golang不同方式拼接字符串性能
在Golang的代码开发中,我们经常会用到字符串的拼接。Golang提供了不同的字符串拼接方式,性能也不尽相同。有时候在做性能优化的时候,往往会看到有些同学想当然的选择一些自认为性能比较高的方法。但是实际情况是否真的能提升性能呢?我们一起来看一下。
KunkkaWu
2023/05/09
7010
对比Golang不同方式拼接字符串性能
golang 字符串拼接方法对比
执行 go test string_test.go -benchmem -bench=".*" 结果: BenchmarkFmtSprintf-4 2962962 400.6 ns/op 112 B/op 3 allocs/op BenchmarkAdd-4 6629833 207.7 ns/op 64 B/op 2 allocs/op BenchmarkStringsJoin-4 4255318 291.6 ns/op 112 B/op 3 allocs/op BenchmarkBuffer-4 2948402 368.3 ns/op 176 B/op 4 allocs/op BenchmarkBuilder-4 3149605 352.1 ns/op 160 B/op 4 allocs/op PASS ok command-line-arguments 8.219s 执行效率排序+>join>fmt.Sprintf>strings.Builder>bytes.Buffer
孤烟
2023/09/06
2490
Go语言字符串高效拼接(二)
在上一篇关于字符串拼接的文章 Go语言字符串高效拼接(一) 中,我们演示的多种字符串拼接的方式,并且使用一个例子来测试了他们的性能,通过对比发现,我们觉得性能高的Builder并未发挥出其应该的性能,反而+号拼接,甚至strings.Join方法的性能更优越,那么这到底是什么原因呢?今天我们开始解开他们神秘的面纱,解开谜底。
飞雪无情
2018/12/06
6410
Go语言字符串高效拼接(一)
在我们变成的时候,和字符串打交道是必不可少的,我们对数据库里文本的处理,Web文本的显示,文本数据的存储等都需要和字符串打交道,那么对于字符串来说,查找、拼接这些都是常用的操作,尤其是以拼接使用的比较多,比如把一个人的姓名和年龄拼接在一起显示。
飞雪无情
2018/12/04
2.2K0
再不Go就来不及了!Go高性能编程技法解读
导语 | 代码的稳健、可读和高效是我们每一个coder的共同追求。本文将结合Go语言特性,为书写高效的代码,力争从常用数据结构、内存管理两个方面给出相关建议。话不多说,让我们一起学习Go高性能编程的技法吧。 一、常用数据结构 (一)反射虽好,切莫贪杯 标准库reflect为Go语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率。 Go语言标准库以及很多开源软件中都使用了Go语言的反射能力,例如用于序列化和反序列化的json、ORM框架 gorm、xorm等
腾讯云开发者
2022/03/29
8510
【Go】高效截取字符串的一些思考
最近我在 Go Forum 中发现了 [SOLVED] String size of 20 character 的问题,“hollowaykeanho” 给出了相关的答案,而我从中发现了截取字符串的方案并非最理想的方法,因此做了一系列实验并获得高效截取字符串的方法,这篇文章将逐步讲解我实践的过程。
thinkeridea
2019/11/04
1.7K0
【Go】slice的一些使用技巧
slice 是 Go 语言十分重要的数据类型,它承载着很多使命,从语言层面来看是 Go 语言的内置数据类型,从数据结构来看是动态长度的顺序链表,由于 Go 不能直接操作内存(通过系统调用可以实现,但是语言本身并不支持),往往 slice 也可以用来帮助开发者申请大块内存实现缓冲、缓存等功能。
thinkeridea
2019/11/04
1.4K0
Go 高性能编程技法
作者:dablelv,腾讯 IEGggG 后台开发工程师 代码的稳健、可读和高效是我们每一个 coder 的共同追求。本文将结合 Go 语言特性,为书写效率更高的代码,从常用数据结构、内存管理和并发,三个方面给出相关建议。话不多说,让我们一起学习 Go 高性能编程的技法吧。 常用数据结构 1.反射虽好,切莫贪杯 标准库 reflect 为 Go 语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率。 Go 语言标准库以及很多开源软件中都使用了 Go 语言的反
腾讯技术工程官方号
2022/03/18
2.1K0
Go | 字符串比较方式总结
如上所示,我们发现,Compare 内部也是调用了 == , 而且该函数的注释中也说了,这个函数 only for symmetry with package bytes。而且推荐我们直接使用 == 和 >、<。
CnPeng
2021/03/04
14.6K0
Go | 字符串比较方式总结
Golang高性能编程实践
一个例子,content-service 在压测的时候发现过一个问题: 旧逻辑为了简化编码,在进行协议转换前,会对某些字段做一个 DeepCopy,因为转换过程需要原始数据,但我们完全可以通过一些处理逻辑的调整,比如调整先后顺序等移除 DeepCopy。优化前后性能对比如下:
腾讯技术工程官方号
2023/09/15
7521
Golang高性能编程实践
Go基础——字符串
字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。字符串其实是一个结构体,因此字符串的赋值操作也就是reflect.StringHeader结构体的复制过程,并不会涉及底层字节数组的复制。在前面数组一节提到的[2]string字符串数组对应的底层结构和[2]reflect.StringHeader对应的底层结构是一样的,可以将字符串数组看作一个结构体数组。 我们可以看看字符串“Hello, world”本身对应的内存结构:
羊羽shine
2019/05/28
5420
9.Go编程快速入门学习
描述: 日常开发中, 测试是不能缺少的. 通常国内的程序员都不太关注单元测试这一部分, 俗话说不写测试的开发不是好程序猿,我认为每一位开发者都应该了解 TDD(Test Driven Development-测试驱动开发),所以本章将主要介绍下在Go语言中如何做单元测试和基准测试。
全栈工程师修炼指南
2022/09/29
7210
9.Go编程快速入门学习
Go语言字符串高效拼接(三)
在上一篇关于字符串拼接的文章Go语言字符串高效拼接(二) 中,我们终于为Builder拼接正名了,果真不负众望,尤其是拼接的字符串越来越多时,其性能的优越性更加明显。
飞雪无情
2018/12/12
1.1K0
【初识Go】| Day5 字典、字符串
字典/哈希表是一种巧妙并且实用的数据字结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。
yussuy
2020/12/18
4080
【初识Go】| Day5 字典、字符串
【Go】string 优化误区及建议
初学 Go 语言的朋友总会在传 []byte 和 string 之间有着很多纠结,实际上是没有了解 string 与 slice 的本质,而且读了一些程序源码,也发现很多与之相关的问题,下面类似的代码估计很多初学者都写过,也充分说明了作者当时内心的纠结:
thinkeridea
2019/11/04
9840
【Go】string 优化误区及建议
golang 几种字符串的连接方式
最近在做性能优化,有个函数里面的耗时特别长,看里面的操作大多是一些字符串拼接的操作,而字符串拼接在 golang 里面其实有很多种实现。 实现方法 直接使用运算符 func BenchmarkAddStringWithOperator(b *testing.B) { hello := "hello" world := "world" for i := 0; i < b.N; i++ { _ = hello + "," + world } } golang 里面
李海彬
2018/03/19
1.1K0
【Go】深入剖析slice和array
array 和 slice 看似相似,却有着极大的不同,但他们之间还有着千次万缕的联系 slice 是引用类型、是 array 的引用,相当于动态数组, 这些都是 slice 的特性,但是 slice 底层如何表现,内存中是如何分配的,特别是在程序中大量使用 slice 的情况下,怎样可以高效使用 slice? 今天借助 Go 的 unsafe 包来探索 array 和 slice 的各种奥妙。
thinkeridea
2019/11/04
4890
【Go】深入剖析slice和array
相关推荐
Golang-optimization「2」: 字符串
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验