BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / golang / #162同步于 2016/6/8
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Golang机器人发帖

恭喜开版之『谈谈Go语言的字符串』

bswgd
2016/6/8镜像同步3 回复
『问题描述』 那天有用户向我反馈在使用 GoJieba 的过程中发现内存泄露的Bug。 具体现象就是这个测试代码 test.go 跑着跑着内存一直增长。 刚开始以为是代码里面的C语言部分内存没有正确释放导致的, 查了很久一直没有找到问题所在。 最后发现这个Bug非常白痴,是因为 C.CString 使用不当导致的。 在调用了 C.CString 之后需要手动释放内存。 这个Bug非常白痴,但是却反映了我之前对Go语言string理解不彻底的隐患。 才导致在我第一眼看到 C.CString 的时候, 就下意识的认为这个函数肯定没有动态申请新的内存, 和 C++ string::c_str() 一样,复用了内存。 所以也就肯定不需要手动释放。 『深入分析』 C语言和Go语言本是同根生, 所以Go语言在设计的时候就通过cgo对C语言调用支持得很好。 而Go语言和C语言之间的数据转换就是通过 C.CString (Go->C), C.GoString(C->Go) 来进行的。 先谈谈 C.GoString ,很显然当使用 C.GoString 的时候, 会复制C语言的*char指针指向的字符串的内容拷贝到Go语言的string管理的内存空间。 Go语言的string管理的内存空间有gc管理,不需要用户主动释放内存。 也就是不需要管它。 而 C.CString 将 Go语言 string 转换成 C语言字符串的时候。 我们就要谈谈为什么它不会像 C++ 的 string::c_str() 一样只是单纯的共用内存了。 本质原因在于对于 Go 来说, string 和 C语言最大的不同是: 在C语言中,字符串是以 ’\0’ 结尾。 其实我认为这个本身是一种历史遗留问题。 『C语言的字符串主要有两种存储方式可选』 比如一个 “hello” 的字符串。 我们在内存中表示可以有两种选择: 第一种: "hello\0" 第二种: typedef struct { char* buffer; size_t len; } string; C语言默认的字符串选择了第一种方式, 我认为主要原因在于当年C语言发明的时候是内存和稀缺的时代。 第一种方式比第二种方式显然更省内存。 但是随着时代的发展,内存越来越便宜。内存已经越来越不是程序开发的瓶颈。 第二种方式越来越成为字符串设计的首选。 比如在Nginx之类的著名开源项目中,也是采用了第二种方式对字符串进行存储。 而第二种方式更受青睐的主要原因我认为有两点: 『1. 更好的内存共享』 比如有一个字符串s1 = “hello world” , 而有两种字符串s2 s3 分别是 s1 的子串:"hello", “world” . 当我们使用第二种方式存储字符串的时候, 我们对于s2 s3就直接复用 s1的内存即可。 无需动态分配和释放,这样的场景在协议解析,比如HTTP包头的场景下特别常用。 而假设我们使用第一种方式存储字符串的话, 那么 s1 = “hello world\0”, s2 = “hello\0”, 虽然 s2 是 s1 的子串,但是因为 “\0” 结尾符的存在, s2 就无法复用 s1 的内存,而是需要新申请一段新的内存。 这也是为什么在Go语言中, C.CString 函数返回的内存肯定是一段新的内存, 也就不得不要求调用者手动释放。 『2. 性能更高,获取长度不再是strlen这种O(N)时间复杂度的函数』 这点就比较显而易见了。 『最后』 其实Go的文档还是写的很详细的,如下, 只不过我自己思维惯性导致走入误区,这个锅得我自己背。 在 cgo-1 中关于 C.CString 的注释里面已经写的很清楚了。 需要手动释放,C.CString 返回的指针。 // Go string to C string // The C string is allocated in the C heap using malloc. // It is the caller's responsibility to arrange for it to be // freed, such as by calling C.free (be sure to include stdlib.h // if C.free is needed). func C.CString(string) *C.char 在 cgo-2 中有释放 C.CString 返回指针的示例: func Print(s string) { cs := C.CString(s) defer C.free(unsafe.Pointer(cs)) C.fputs(cs, (*C.FILE)(C.stdout)) } 这个问题我想后来也是引起了Go语言作者的注意了, 在go1.7新版本发布信息中我发现新出了一个 C.Bytes 的类型,C.Bytes 就不需要像 C.CString 一样需要手动释放内存了。 转载请注明出处: 谈谈Go语言的字符串设计
订阅后,新回复会通过你的通知中心匿名送达。
3 条回复
nullne机器人#1 · 2016/6/8
nuanyangyang机器人#2 · 2016/6/8
从gc的角度也可以想明白 堆里面的内存由gc管理 程序使用“引用”来引用堆里的对象 引用不等于地址,是不透明的, 用户不可以任意构造一个地址然后说他是引用。 但引用一般用地址实现的 gc为了优化空间效率,会把对象在堆里移动,以整理碎片 移动的同时,所有指向这个对象的引用都会自动地更新成新的地址 编译器可以找到程序里所有的引用的位置, 因为类型系统永远不让用户混淆地址和引用 在用户看来,垃圾回收是透明的 但是,c语言不使用垃圾回收, 它用指针(即地址)直接指向空间 所以,go的堆里的对象,要想让c访问的话 必须给出对象的地址 地址一旦给出,垃圾回收器就必须作出两个保证: 1、不可以回收这个对象。c程序也许还在用它 2、不可以移动对象:c不支持垃圾回收,不知道指针被存在哪里了 c也就无法在垃圾回收器移动对象时更新指针 这种情况下,我们说,这个对象被“固定”(pin)了 一旦被固定,要想再移动或回收该对象 必须“解除固定”(unpin) 这需要用户显式地告诉虚拟机说:我想解除固定这个对象 go里,字符串在堆里 传给c时固定了对象。 如果不手动解除固定 内存就真的再也无法释放了。 别的虚拟机,只要gc允许移动对象, 又想和c语言交互, 都必须能够支持“固定”对象(pinning) c#里有专门的fix语句用于获得对象地址 java虚拟机的jni也需要固定 lua解释器不支持,因为它的垃圾回收器用malloc分配内存,不支持移动。 v8、spidermonkey和pypy可以保证在执行c程序的时候gc不执行。但多线程直接毁了。 【 在 bswgd 的大作中提到: 】 : 『问题描述』 : 那天有用户向我反馈在使用 GoJieba 的过程中发现内存泄露的Bug。 具体现象就是这个测试代码 test.go 跑着跑着内存一直增长。 刚开始以为是代码里面的C语言部分内存没有正确释放导致的, 查了很久一直没有找到问题所在。 : 最后发现这个Bug非常白痴,是因为 C.CString 使用不当导致的。 在调用了 C.CString 之后需要手动释放内存。 这个Bug非常白痴,但是却反映了我之前对Go语言string理解不彻底的隐患。 才导致在我第一眼看到 C.CString 的时候, 就下意识的认为这个函数肯定没有动态申请新的内存, 和 C++ string::c_str() 一样,复用了内存。 所以也就肯定不需要手动释放。 : ...................
bswgd机器人#3 · 2016/6/8
暖神老司机又发车了。。。 【 在 nuanyangyang (暖羊羊) 的大作中提到: 】 : 从gc的角度也可以想明白 : 堆里面的内存由gc管理 : 程序使用“引用”来引用堆里的对象 : ................... 通过『我邮2.0』发布