Go语言越来越受欢迎。 确信会议越来越多,例如
GolangConf ,并且该语言
是十项收入最高的技术之一。 因此,谈论其特定问题(例如性能)已经很有意义。 除了所有编译语言的常见问题外,Go都有自己的。 它们与优化器,堆栈,类型系统和多任务模型相关联。 解决这些问题的方法和解决方法有时非常具体。
丹尼尔·波多尔斯基 (
Daniel Podolsky )虽然是Go的传教士,但在他身上也遇到了很多奇怪的事情。 收集并
测试所有奇怪且最重要的是有趣的东西,然后在HighLoad ++中进行讨论。 该报告的文字记录将包括数字,图形,代码示例,分析器结果,以不同语言对相同算法的性能进行比较-以及其他所有内容,因此我们讨厌“优化”一词。 笔录中不会有任何启示-它们是从如此简单的语言中获得的-一切都可以在报纸上阅读。
关于演讲者。 Daniil Podolsky :26年的经验,有20年的运营经验,包括该小组的负责人,还有5年的Go编程经验。
Kirill Danshin :
Gramework的创建者,维护者,快速HTTP,Black Go-mage。
该报告是由丹尼尔·波多尔斯基(Daniil Podolsky)和基里尔·丹辛(Kirill Danshin)共同编写的,但丹尼尔(Daniel)提出了报告,而西里尔(Cyril)在精神上提供了帮助。语言构造
我们有
direct
的性能标准。 这是一个使变量递增且不再执行任何操作的函数。
每个操作的功能结果为
1.46 ns 。 这是最小的选择。 每次操作快于1.5 ns,可能无法正常工作。
推迟我们如何爱他
许多人知道并喜欢使用延迟语言构造。 我们经常像这样使用它。
func BenchmarkDefer(b *testing.B) { for i := 0; i < bN; i++ { incDefer() } } func incDefer() { defer incDirect() }
但是您不能那样使用它! 每个操作每次延迟吃掉40 ns。
// BenchmarkDirect-4 2000000000 1.46 / // defer BenchmarkDefer-4 30000000 40.70 /
我以为是因为内联? 也许内联是如此之快?
Direct是内联的,而defer函数不能内联。 因此,编译了一个没有内联的单独的测试函数。
func BenchmarkDirectNoInline(b *testing.B) { for i := 0; i < bN; i++ { incDirectNoInline() } }
一切都没有改变,延迟花了相同的40 ns。 推迟亲爱的,但不是灾难性的。
如果函数花费的时间少于100 ns,则可以不延迟进行操作。
但是,如果该功能花费的时间超过一微秒,则完全一样-您可以使用defer。
通过引用传递参数
考虑一个流行的神话。
func BenchmarkDirectByPointer(b *testing.B) { for i := 0; i < bN; i++ { incDirectByPointer(&testInt64) } } func incDirectByPointer(n *int64) { *n++ }
一切都没有改变-没有什么值得。
// BenchmarkDirectByPointer-4 2000000000 1.47 / BenchmarkDeferByPointer-4 30000000 43.90 /
每次延迟除了3 ns之外,但这会因波动而被撇销。
匿名函数
有时,新手会问:“匿名函数贵吗?”
func BenchmarkDirectAnonymous(b *testing.B) { for i := 0; i < bN; i++ { func() { testInt64++ }() } }
匿名函数并不昂贵,它需要40.4 ns。
介面
有一个实现它的接口和结构。
type testTypeInterface interface { Inc() } type testTypeStruct struct { n int64 } func (s *testTypeStruct) Inc() { s.n++ }
使用增量方法有三个选项。 直接来自Struct:
var testStruct = testTypeStruct{}
。
在相应的具体接口中:
var testInterface testTypeInterface = &testStruct
。
使用运行时接口转换:
var testInterfaceEmpty interface{} = &testStruct
。
下面是运行时界面的直接转换和使用。
func BenchmarkInterface(b *testing.B) { for i := 0; i < bN; i++ { testInterface.Inc() } } func BenchmarkInterfaceRuntime(b *testing.B) { for i := 0; i < bN; i++ { testInterfaceEmpty.(testTypeInterface).Inc() } }
因此,该接口不花费任何费用。
// BenchmarkStruct-4 2000000000 1.44 / BenchmarkInterface-4 2000000000 1.88 / BenchmarkInterfaceRuntime-4 200000000 9.23 /
运行时界面转换是值得的,但并不昂贵-您无需明确拒绝。 但是,请尽可能尝试不使用它。
神话:- 取消引用-取消引用指针-免费。
- 匿名功能是免费的。
- 接口是免费的。
- 运行时界面转换-不是免费的。
切换,地图和切片
每个Go的新手都会问,如果用map替换switch会发生什么。 会更快吗?
开关有不同的尺寸。 我测试了三种尺寸:小号10例,中号100例,大号1000例。 在实际的生产代码中找到了1000个案例的开关。 当然,没有人会用手书写它们。 这是自动生成的代码,通常是类型开关。 在两种类型上进行了测试:int和string。 看来结果会更加清楚。
一点开关。 最快的选择是实际开关。 紧随其后的是slice,其中相应的整数索引包含对该函数的引用。 Map在int或string上都不是领导者。
打开字符串比int慢得多。 如果您可以不切换到字符串而是切换到int,则可以这样做。
中间开关。 开关本身仍然可以控制int,但是slice已经超过了它。 地图仍然不好。 但是在字符串键上,映射比切换要快-符合预期。
大开关。 一千个案例表明,在“按字符串切换”提名中,地图无条件获胜。 从理论上讲,slice赢了,但实际上,我建议您在此处使用相同的开关。 即使考虑到映射具有带特殊哈希函数的整数键,映射仍然很慢。 通常,此功能不执行任何操作。 int本身具有int的哈希值。
结论 映射仅在数量较大而不是整数条件下更好。 我确信在int以外的任何条件下,它的行为都将与string相同。 当条件为整数时,Slice总是会转向。 如果您想以2 ns的速度“加速”程序,请使用它。
程序间交互
主题很复杂,我进行了许多测试,并将介绍最具启发性的测试。 我们知道以下
机构间互动的方式 。
- 原子的 这些是适用性有限的方法-您可以替换指针或使用int。
- 自Java以来,互斥量已被广泛使用。
- 频道是GO特有的。
- 缓冲通道-缓冲通道。
当然,我在争夺一种资源的大量goroutines上进行了测试。 但是他为自己选择了三个作为指示:一点-100,中等-1000和很多-10000。
负载曲线不同 。 有时所有gorutin都想写一个变量,但这很少见。 通常,毕竟有些写,有些读。 在大多数读者(占90%的阅读者)中,有90%是写作。
这是使用的代码,因此为通道提供服务的goroutine可以提供对变量的读取和写入操作。
go func() { for { select { case n, ok := <-cw: if !ok { wgc.Done() return } testInt64 += n case cr <- testInt64: } } }()
如果一条消息通过我们编写的渠道到达我们,我们将执行该消息。 如果通道关闭,则完成goroutin。 在任何时候,我们都准备好写入其他goroutine用于读取的通道。
这是一个goroutine的数据。 通道测试是在两个goroutine上执行的:一个过程处理Channel,另一个过程写入该Channel。 这些选项已经过测试。
- 直接写入变量。
- 互斥锁获取日志,写入变量并释放日志。
- 原子通过原子写入变量。 它不是免费的,但仍然比Mutex的一种garutin便宜得多。
使用少量的goroutine,Atomic是一种有效且快速的同步方式,这不足为奇。 Direct不在这里,因为我们需要同步,而同步却没有提供。 但是,原子当然有缺陷。
接下来是Mutex。 我期望Channel可以和Mutex一样快,但是不可以。
通道比Mutex贵一个数量级。
而且,Channel和缓冲Channel的价格大致相同。 还有Channel,缓冲区永远不会溢出。 它比缓冲区溢出的便宜一个数量级。 仅当Channel中的缓冲区未满时,它的成本在数量级上与Mutex差不多。 这是我从测试中获得的期望。
在任何负载配置文件上都重复了此图以及其成本分配情况-在MostlyRead和MostlyWrite上均如此。 此外,完整的MostlyRead Channel的费用与未完成的费用相同。 而MostlyWrite的缓冲通道(其中缓冲区未满)的成本与其余通道相同。 我无法说出为什么-我尚未研究此问题。
传递参数
如何更快地传递参数-通过引用还是通过值? 让我们来看看。
我进行了如下检查-将嵌套类型设为1到10。
type TP001 struct { I001 int64 } type TV002 struct { I001 int64 S001 TV001 I002 int64 S002 TV001 }
第十个嵌套类型将具有10个int64字段,而上一个嵌套的嵌套类型也将为10。
然后他编写了创建嵌套类型的函数。
func NewTP001() *TP001 { return &TP001{ I001: rand.Int63(), } } func NewTV002() TV002 { return TV002{ I001: rand.Int63(), S001: NewTV001(), I002: rand.Int63(), S002: NewTV001(), } }
为了进行测试,我使用了三种类型的选项:带有嵌套2的小号,带有嵌套3的中号,带有嵌套5的大号。我不得不在晚上进行带有嵌套10的非常大的测试,但是图片与5完全相同。
在函数中,按值传递的速度至少是按引用传递的速度的两倍 。 这是由于以下事实:按值传递不会加载转义分析。 因此,我们分配的变量在堆栈上。 对于运行时和垃圾收集器而言,它便宜得多。 虽然他可能没有时间连接。 这些测试持续了几秒钟-垃圾收集器可能仍处于睡眠状态。
黑魔法
你知道这个程序会输出什么吗?
package main type A struct { a, b int32 } func main() { a := new(A) aa = 0 ab = 1 z := (*(*int64)(unsafe.Pointer(a))) fmt.Println(z) }
程序的结果取决于执行程序的体系结构。 在小端(例如AMD64)上,程序将显示
。 在大端上,一个。 结果是不同的,因为在小字节序上,此单位出现在数字的中间,在大字节序上-在结尾。
世界上仍然有处理器进行字节序交换,例如Power PC。 在推断不安全技巧的作用之前,有必要在启动时找出计算机上配置了哪些字节序。 例如,如果您编写了将在某些IBM多处理器服务器上执行的Go代码。
我引用此代码来解释为什么我考虑所有不安全的黑魔法。 您不需要使用它。 但是西里尔认为这是必要的。 这就是为什么。
有一个功能与GOB相同的功能-Go Binary Marshaller。 这是编码器,但不安全。
func encodeMut(data []uint64) (res []byte) { sz := len(data) * 8 dh := (*header)(unsafe.Pointer(&data)) rh := &header{ data: dh.data, len: sz, cap: sz, } res = *(*[]byte)(unsafe.Pointer(&rh)) return }
实际上,它需要一块内存并从中绘制一个字节数组。
这甚至不是命令-这是两个命令。 因此,西里尔·丹辛(Cyril Danshin)在编写高性能代码时,会毫不犹豫地进入程序的胆量并使之不安全。
我们将在10月7日的GolangConf上讨论Go的更多特定功能,该会议是为那些使用Go进行专业开发的人以及认为使用该语言作为替代语言的人的会议。 Daniil Podolsky只是计划委员会的成员,如果您想对本文进行辩论或揭示相关问题,请提交报告申请 。
对于其他所有方面,关于高性能,当然是HighLoad ++ 。 我们也在那里接受申请。 订阅新闻时事,并及时了解我们所有针对Web开发人员的会议的新闻。