谈谈cgo字符串传递过程中的一些优化

前言

最近做了一些关于golang的开发,由于有些相关模块是c++编译得到的动态库so,且不打算用golang重构一遍,想通过golang直接调用so的方式,来完成模块搭建。

查阅了一些golang调用so的方式,最终决定通过cgo的方式去调用so。

cgo是golang调用C语言的一个自带工具,由于这边有的so是c++编译的,所以使用cgo之前,对这些c++ so又做了一层c的封装。

因为业务关系,golang、c之间会传递一串很大的字符串。在性能调优的过程中,发现代码内存使用量发生了翻倍(因为传递的字符串很大,理想情况下进程内存使用≈字符串的内存使用,但此处≈n*字符串的内存使用)。

很明显字符串发生了多余的拷贝操作,下面就围绕cgo字符串传递过程分析问题的原因以及解决方案。

正文

v1.0实现

package main

// #include <stdio.h>
// #include <stdlib.h>
//
// void TestCString(char* s) {
//   printf("%s\n", s);
// }
import "C"
import "unsafe"

func main() {
	var s string = "test cString"
	cString := C.CString(s)
	C.TestCString(cString)
	C.free(unsafe.Pointer(cString))
}
14:14 $ go run cstring.go 
test cString

这是最一开始的实现方式。测试发现,当字符串s很大时候,整个进程的内存使用量是约等于两倍的字符串s的内存使用量,字符串s发生了一次内存拷贝。如果这个字符串只是想做读操作,并不想对其本身做修改,很明显这一次的拷贝是多余。

多余的拷贝操作很可能就是C.CString构造的时候,又重新发生了一次拷贝,看一下cgo中golang string转换c string的文档,其中有这么一段(line 254):

……

A few special functions convert between Go and C types
by making copies of the data. In pseudo-Go definitions:
	// 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
	// Go []byte slice to C array
	// The C array 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.CBytes([]byte) unsafe.Pointer
	// C string to Go string
	func C.GoString(*C.char) string
	// C data with explicit length to Go string
	func C.GoStringN(*C.char, C.int) string
	// C data with explicit length to Go []byte
	func C.GoBytes(unsafe.Pointer, C.int) []byte

……

可以看到,确实在构造CString的时候发生了拷贝。(这里额外提一句,文档里说当使用了C.CString,需要用户自行调用C.free释放内存,否则会内存泄漏。

如果我们不会对传入的字符串做修改操作,

  • 如何避免这一次多余的拷贝呢?
  • 是否能直接把字符串的首指针直接传递给c呢?

v1.1实现

golang string数据结构

先看一下go内置类型string的定义

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string

其中,string对象的值不可修改,这一点很重要,后面会涉及到。
再看一下go内置类型string的数据结构

type stringStruct struct {
	str unsafe.Pointer
	len int
}

结构很简单,字符串首地址+字符串长度。
到这,既然golang string有字符串首地址,直接当参数通过cgo传给c,不在构造CString,那就可以解决多余拷贝问题了吧?看一下下面这个代码(先说一下下面这段代码是有问题的):

package main

// #include <stdio.h>
// #include <stdlib.h>
//
// void TestCString(char* s) {
//   printf("%s\n", s);
// }
import "C"

func main() {
	var s string = "test cString"
	C.TestCString(s.str)
}

按上面这么写是有问题的:是因为golang结构体中首字母没有大写,别的包是无法调用的,因为golang string本意就不想让用户直接对str、len进行访问,防止用户对其做修改导致一些不安全的问题。那如何获取golang string的字符串首地址呢?

最终代码

直接看这段代码:

package main

// #include <stdio.h>
// #include <stdlib.h>
//
// void TestCString(char* s, unsigned int l) {
//   unsigned int i=0;
//   for(;i<l;i++){
//     printf("%c", *s);
//     s += 1;
//   }
//   printf("\n");
// }
import "C"
import "unsafe"

type MyString struct {
	Str *C.char
	Len int
}

func main() {
	var s string = "test cString"
	ms := (*MyString)(unsafe.Pointer(&s))
	C.TestCString(ms.Str, C.uint(ms.Len))
}

我们通过定义一个和string结构体几乎一样的MyString结构体,且MyString的成员是可访问的。然后将golang string对象转换成MyString对象,然后就能通过访问MyString.Str拿到原来字符串的首地址,传递给c,既没有发生多余的拷贝操作,c中打印出来也能完全无误地得到传递过来的内容。

  • golang其他一些数据类型和c的相互转换参考这里

注意

这边我们额外多传了一个字符串长度给c,为什么?先看下面这个不传长度的例子:

package main

// #include <stdio.h>
// #include <stdlib.h>
//
// void TestCString1(char* s) {
//   printf("%s\n", s);
// }
//
// void TestCString2(char* s, unsigned int l) {
//   unsigned int i=0;
//   for(;i<l;i++){
//     printf("%c", *s);
//     s += 1;
//   }
//   printf("\n");
// }
import "C"
import "unsafe"

type MyString struct {
	Str *C.char
	Len int
}

func test1() {
	var s string = "test cString"
	ms := (*MyString)(unsafe.Pointer(&s))
	C.TestCString2(ms.Str, C.uint(ms.Len))
}

func test2() {
	var s string = "test cString"
	ms := (*MyString)(unsafe.Pointer(&s))
	C.TestCString1(ms.Str)
}

func test3() {
	var s string = "test c\000String"
	ms := (*MyString)(unsafe.Pointer(&s))
	C.TestCString1(ms.Str)
}

func main() {
	test1()
	test2()
	test3()
}
  • test1:正确的使用方式
  • test2:因为c自动判断字符串结束的方式是以"\0"为结尾的,如果不给它显式指明读多长,它会自动读到"\0"前所有的字符出来。运气好一点会抛异常发现;运气不好,读出来"test cString"后面再拖一大堆未知字符,不可预期且非常危险。
  • test3:道理同test2,这边test3会打印"test c"。如果传递的只是可见ascii字符串,又不想传长度过去,在原来的字符串后面加个"\0",第一种打印方式也是可以行得通的。但如果传递的内容是所有ascii字符都有可能出现的那种内容,记得再传递个长度过去。

参考

  • https://github.com/golang/go/blob/master/src/runtime/string.go
  • https://github.com/golang/go/blob/master/src/builtin/builtin.go
  • https://github.com/golang/go/blob/master/src/cmd/cgo/doc.go
  • https://zhuanlan.zhihu.com/p/73505526
  • https://blog.csdn.net/weixin_36771703/article/details/89003014
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CGO是Go语言提供的一种机制,可以让Go程序调用C语言编写的函数库。在CGO,char*通常用于表示C语言字符串类型。 在Go语言,char*通常会被映射为Go语言的[]byte类型,可以直接在Go语言进行处理。在CGO,可以使用C.CString()函数将Go语言字符串转换为C语言的char*类型,也可以使用C.GoString()函数将C语言的char*类型转换为Go语言字符串类型。 下面是一个例子,演示如何在Go语言调用一个C语言函数,该函数接受一个char*类型的字符串,并返回一个新的char*类型的字符串。 ``` package main /* #include <stdlib.h> #include <string.h> char* reverse(char* s) { size_t len = strlen(s); char* result = malloc(len + 1); for (size_t i = 0; i < len; i++) { result[i] = s[len - 1 - i]; } result[len] = '\0'; return result; } */ import "C" import ( "fmt" "unsafe" ) func main() { s := "hello" cs := C.CString(s) defer C.free(unsafe.Pointer(cs)) cresult := C.reverse(cs) defer C.free(unsafe.Pointer(cresult)) result := C.GoString(cresult) fmt.Println(result) } ``` 在这个例子,我们定义了一个C语言函数`reverse()`,它接受一个char*类型的字符串,并返回一个新的char*类型的字符串,这个字符串是原字符串的反转。我们在Go语言调用了这个函数,传递了一个Go语言字符串`s`,然后使用C.CString()函数将其转换为C语言的char*类型。C语言函数返回一个char*类型的字符串,我们使用C.GoString()函数将其转换为Go语言字符串类型。注意,我们需要在使用C.CString()函数时手动调用C.free()函数来释放分配的内存

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值