CockroachDB学习笔记——[译]Cgo的成本与复杂性
- 原文链接:https://www.cockroachlabs.com/blog/the-cost-and-complexity-of-cgo/
- 原作者:Tobias Schottdorf
- 原文日期:Dec 9, 2015
- 译:zifeiy
Cgo 是 Go 的一个非常重要的部分:
它是你调用任何任何非Go代码的窗口(或者更确切地说,任何由C语言绑定的东西)。
对于 CockroachDB 来说,
cgo 减轻了我们在处理连接 ROcksDB 的存储层(storage layer)时候的压力,
而 RocksDB 这个存储引擎目前在Go语言生态环境中没有良好的替代品,
至少在我们的任职范围内没有。
经过几次迭代之后,
我们发现处理这些外部库的正确方法——我们有相当多的外部库——是将它们处理后放在Go语言的依赖包(Go wrapper packages)中:
虽然Cgo为我们提供了便利,但是请远没有直接调用那么简单。
经验丰富的Cgo使用者(cgo-er)可能会知道这一点(并且可能更倾向于轻描淡写地在这篇文章的剩余部分上浏览),但是使用Cgo附带一些警告,我们将在下面讨论我们建议的解决方案。
调用的开销(Call Overhead)
调用 Cgo 带来的开销将比在Go语言内部的调用带来的开销大几个数量级。
这听起来很可怕,但实际上在很多应用中都不是问题。
然我们来观察一下这个 cgobench 包:
func BenchmarkCGO(b *testing.B) {
CallCgo(b.N) // call `C.(void f() {})` b.N times
}
// BenchmarkGo must be called with `-gcflags -l` to avoid inlining.
func BenchmarkGo(b *testing.B) {
CallGo(b.N) // call `func() {}` b.N times
}
$ go test -bench . -gcflags '-l' # disable inlining for fairness
BenchmarkCGO-8 10000000 171 ns/op
BenchmarkGo-8 2000000000 1.83 ns/op
换句话说,在这个(公认最小的)例子中,大约有100个因素参与其中。
大家不要因此疯狂。
按照绝对时间,171 ns通常是一个完全可以接受的成本,特别是如果你的C代码做了大量的工作。
然而,在我们的例子中,我们在一些测试中计时了数以万计的Cgo调用,
所以我们把一些代码推到C上以减少迭代次数。
我们的结论是调用开销并不重要,等价的C++和Go的实现在性能上是难以区分的。
然而,由于能够编写更高效的实现,我们仍然将一些操作移到C++上,并进行了fat改进。
手动ISH内存管理(Manual-ish Memory Management)
GO是一门垃圾回收语言,但C不是。
这意味着从C传递数据到Go不应该粗心地进行,并且拷贝常常是不可避免的,反之亦然。
(译者的理解:C语言中的数据不会自动释放,而Go语言中的数据会自动进行垃圾回收,所以在两者之间传输数据的时候可能会碰到数据被提前释放,或者该释放但是没释放的问题)
尤其是(在我们经常)非常频繁地处理字节字符串和接口的时候。
传统的对 C.CString 及 C.GoBytes 的使用会大大增加内存压力,
当然,复制数据也会明显消耗CPU性能。
在一些场景下,我们有办法避免这种类型的拷贝。
举个例子,在便利键的时候,我们使用如下办法:
func (r *rocksDBIterator) Key() []byte {
return C.GoBytes(unsafe.Pointer(r.key), s.len)
}
func (r *rocksDBIterator) Next() {
// The memory referenced by r.key stays valid until the next operation
// on the iterator.
r.key = C.DBNext(r.iter) // cgo call
}
如果我们想要做的是检查一个标准的当前键,我们知道底层内存在我们需要的时候是不会被释放掉的。
因此,这个(编写的)代码似乎是浪费的:
for ; iter.Valid(); iter.Next() {
if bytes.HasPrefix(iter.Key(), someKey) { // copy!
// ...
}
}
为了减轻所有这些拷贝的成本,我们添加(并使用)函数 Key() 的零拷贝(和不安全)版本:
// unsafeKey() returns the current key referenced by the iterator. The memory
// is invalid after the next operation on the iterator.
func (r *rocksDBIterator) unsafeKey() []byte {
// Go limits arrays to a length that will fit in a (signed) 32-bit
// integer. Fall back to copying if our slice is larger.
const maxLen = 0x7fffffff
if s.len > maxLen {
return C.GoBytes(unsafe.Pointer(r.key), s.len)
}
return (*[maxLen]byte)(unsafe.Pointer(s.data))[:s.len:s.len]
}
这看上去是一个更高效和安全的办法,但是在正确使用的时候,它涉及到更多需要考虑的地方。
我们正在创建一个由C分配的内存支持的切片。
我们需要小心,而我们的切片(或任何衍生切片)仍在使用中,而与此同时C内存没有释放。
(译者的理解:C内存没有释放,对切片的改动,不能引起内存的变化,那我们度数据是从内存中读的,
读到的还是原来的数据)
我们可以解决这个问题,因为这个问题出现在我们的底层代码中,但这肯定不是任何类型的面向公众的API的选择;
这将保证一些用户不会遵守对返回的字节切片的微妙契约,并且随机地体验空指针异常。
Cgoroutines != Goroutines
这可能是一个严重的问题,而当你想到它的时候是显而易见的,当你不这样做的时候,它会是一个惊喜。
考虑如下代码:
func main() {
for i := 0; i < 1000; i++ {
go func() {
time.Sleep(time.Second)
}()
}
time.Sleep(2*time.Second)
}
这个无聊的程序不会有多大效果。1000个 gouroutines 几乎免费地来了,分配给它们的“堆栈”只有几千字节。
如果我们把 cgo 带入这个编码游戏呢?
下面的代码是 cgobench 中一个示例的简化版本:
//#include <unistd.h>
import "C"
func main() {
for i := 0; i < 1000; i++ {
go func() {
C.sleep(1 /* seconds */)
}()
}
time.Sleep(2*time.Second)
}
“惊喜”是这段代码的效果(和上面那段)非常不同。
一个阻塞式的 cgo 调用会占用一个系统线程;
Go运行时库 不能像 goroutine 那样调度它们,而堆栈是一个真正的堆栈,其数量是兆字节的!
同样,如果你调用适当的有界并发的 cgo ,那就没什么大不了的。
但是如果你写的话,你可能已经习惯于不太考虑 Goroutines 了。
在关键请求路径中的阻塞cgo调用可能会给你带来成百上千的线程,
这些线程可能会 导致问题。
特别地,ulimit -r 或 debug.SetMaxThreads 可能会导致程序地快速终止。
或者,按照 Dave Cheney 地话来说,
- “过多的 cgo 使用中断 Go 轻量级并发的承诺。”
交叉编译(Cross Out Cross-Compilation)
使用 cgo ,你会丢失(或者更确切地说,你不会很好地体验到)交叉编译在 Go 1.5 或更高把呢不能中工作的易用性。
这并不奇怪(因为交叉编译与C依赖关系必然需要交叉编译C依赖),但是如果您在和Go自己的包或外部库之间进行选择,那么这可能是一个标准。
Dave Cheney在此处发表的一篇文章 通常是关于可用信息的最佳来源。
静态构建(Static Builds)
这种情况和交叉编译的情况类似,不过和交叉编译相比情况较好一点。
使用 cgo 构建静态二进制文件仍然是可能的,但需要进行一些调整。
在Go 1.5之前,最突出的例子是必须使用 netgo 构建标签来避免在 glibc 中链接DNS解析。
这已成为默认的处理方式,
但仍然存在一些微妙之处,
例如必须指定自定义 -installsuffix(以避免使用非静态生成中的缓存生成)、
将正确的标志传递给外部链接器(external linker,在本例中,-extldflags “-static”),
并用 -a 构建以执行一个完全的重建。
不是所有的这些都是必要的,但是你需要明白这一点:
它变得更加手动,并且所有的重建速度都变慢了。
对于所有感兴趣的人来说,
这是我与 cgo 的第一次约会(以及后来的)角斗 和
一个神秘的错误,
我们可以在未来的帖子中再次找到。
调试(Debugging)
调试你的代码会更困难(在使用 cgo 的情况下)。
驻留在C中的部分代码不容易通过Go的工具访问。
PProf、运行时统计(runtime statistics)、行号(line numbers)、
堆栈跟踪(stack traces) —— 当你越过边界时,所有的东西都会减弱。
GoRename 及其朋友可能 偶尔会将源代码中的标识符丢弃 ,
而这些标识符(原本应该)在转换后将转换为 cgo 生成的代码。
由于这些工具通常运行的很好,所以出现的一些损失总会让人感到不安。
但是,gdb仍然有效地工作着。
总结(Summary)
总而言之,cgo 是一个具有局限性的伟大工具。
我们最近开始把一些低级操作转移到C++,这给了一些令人印象深刻的加速。
其他的尝试并没有带来更多地性能提升。
性能地表现不是还令人满意吗?
CockroachDB学习笔记——[译]Cgo的成本与复杂性的更多相关文章
- CockroachDB学习笔记——[译]CockroachDB中的SQL:映射表中数据到键值存储
CockroachDB学习笔记--[译]CockroachDB中的SQL:映射表中数据到键值存储 原文标题:SQL in CockroachDB: Mapping Table Data to Key- ...
- CockroachDB学习笔记——[译]如何优化Go语言中的垃圾回收
原文链接:https://www.cockroachlabs.com/blog/how-to-optimize-garbage-collection-in-go/ 原作者:Jessica Edward ...
- CockroachDB学习笔记——[译]在CockroachDB中如何让在线模式更改成为可能
原文链接:https://www.cockroachlabs.com/blog/how-online-schema-changes-are-possible-in-cockroachdb/ 原作者: ...
- CockroachDB学习笔记——[译]为什么Go语言是CockroachDB的正确选择
原文链接:https://www.cockroachlabs.com/blog/why-go-was-the-right-choice-for-cockroachdb/ 原作者:Jessica Edw ...
- CockroachDB学习笔记——[译]The New Stack:遇见CockroachDB,一个弹性SQL数据库
原文链接:https://www.cockroachlabs.com/blog/the-new-stack-meet-cockroachdb-the-resilient-sql-database/ 原 ...
- CockroachDB学习笔记——[译]CockroachDB是如何进行分布式原子事务的
原文:How CockroachDB Does Distributed, Atomic Transactions 原文链接:https://www.cockroachlabs.com/blog/how ...
- CockroachDB学习笔记——[译]Scaling Raft
原文链接:https://www.cockroachlabs.com/blog/scaling-raft/ 原作者:Ben Darnell 原文日期:Jun 11, 2015 译:zifeiy 在Co ...
- CockroachDB学习笔记——[译]Hello World
原文链接:https://www.cockroachlabs.com/blog/hello-world/ 原作者:Spencer Kimball 原文日期:Jun 4, 2015 译:zifeiy 数 ...
- CockroachDB学习笔记——对此的选择
无意间了解到TiDB,然后知道了他是一款国产团队开源的NewSQL数据库, 看了一下官网,有很多中文的文档和技术分享挺不错的. 但是安装起来好像挺麻烦的说. 测试的硬件环境 也吓死我了,我只有一台笔记 ...
随机推荐
- maven常用命令参数
整理了一些maven常用命令参数,以便参考:参考了maven官网和网上其他一些maven追随者的文件,不在此一一列举,但表示感谢! mvn命令参数 mvn -v, --version 显示版本信息; ...
- script标签中的async、defer属性
Script标签是我们常用的引用js脚本的一种方式. 撸代码的时候,我们常常只写src属性,直接忽略其他属性. 最近发现了2个可以利用的属性:async.defer. 顾名思义async就是异步,在不 ...
- Robot Framework--修改log和报告的生成目录
1.修改log和报告的生成目录:-l F:\testreport\log -r F:\testreport\report -o F:\testreport\output -l:log -r:repor ...
- 5 webpack-dev-server的常用命令参数--open --port 3000 --contentBase src --hot
--open 自动打开浏览器 --port 3000 指定端口3000 --contentBase src 内容的根路径 --hot 热重载,热更新.打补丁,实现浏览器的无刷新
- 通过.frm表结构和.ibd文件恢复数据
整个恢复过程其实可以总结为下面几步: (1):恢复表结构 (2):复制出来创建表的sql语句 (3):恢复表数据(在恢复表数据的时候,首先需要解除当前创建的表与默认生成的.ibd文件间的关系,接着将要 ...
- easyUI-filebox图片上传和预览
转载自:https://blog.csdn.net/nvxiaq/article/details/77740516 备注: 1.如需上传多个图片可定义多个change_photo函数 在onChang ...
- sql 查询 between and 和 >= <= 比较
好久没有更新博客了,积累了很多问题没有得到解决,自己也在纠结有些东西需不需要花时间研究一下,认真想了想,不管怎么样,不能停止更新博客,继续保持一周至少一篇的习惯,不能放弃. 今天说的问题比较简单,就是 ...
- Vue项目中的文件/文件夹命名规范
Vue项目中的文件/文件夹命名规范 0.2262018.09.21 16:01:09字数 820阅读 6979 文件或文件夹的命名遵循以下原则: index.js 或者 index.vue,统一使用小 ...
- 2019.6.28 校内测试 T1 Jelly的难题1
这题面有点难理解,建议直接跳到题意解释那一部分(虽然我觉得解释的不大对,但按照解释来做确实能AC): 按照“题意解释”的思路来思考这个题,那么就十分的简单了: 1.首先要读入这个字符矩阵,可以用cin ...
- Python学习日记(七)——装饰器
1.必备知识 #### 一 #### def foo(): print 'foo' foo #表示是函数 foo() #表示执行foo函数 #### 二 #### def foo(): print ' ...