英文:
Go: Performance Hit of Waiting for Multiple Channels
问题
我今天发现了一件让我有点困惑的事情,我想向社区请教一下,看看我是否遗漏了什么,或者只是设计得不好。
使用情况:我有一个输入通道,我希望一个Go协程等待该通道上的值。如果上下文被取消,就退出。可选地,如果等待一定时间而没有接收到输入,则运行回调函数。
我从这样的代码开始:
func myRoutine(ctx context.Context, c <-chan int, callbackInterval *time.Duration, callback func() error) {
var timeoutChan <-chan time.Time
var timer *time.Timer
if callbackInterval != nil {
// 如果设置了回调间隔,创建一个定时器
timer = time.NewTimer(*callbackInterval)
timeoutChan = timer.C
} else {
// 如果没有设置回调间隔,创建一个永远不会提供值的通道
timeoutChan = make(<-chan time.Time, 0)
}
for {
select {
// 处理上下文取消
case <-ctx.Done():
return
// 处理超时
case <-timeoutChan:
callback()
// 处理通道中的值
case v, ok := <-c:
if !ok {
// 通道已关闭,退出
return
}
// 处理v
fmt.Println(v)
}
// 重置超时定时器(如果有的话)
if timer != nil {
if !timer.Stop() {
// 参见timer.Stop()的文档,了解为什么需要这样做
<-timer.C
}
// 重置定时器
timer.Reset(*callbackInterval)
timeoutChan = timer.C
}
}
}
这种设计看起来很好,因为我认为在select
中没有办法有一个条件性的case
(我认为使用reflect
可能是可能的,但通常非常慢),所以我没有使用两个不同的select
(一个带有定时器,一个不带),也没有使用if
来选择它们之间的情况,而是在一个select
中使用了一个永远不会提供值的定时器通道。保持DRY(Don't Repeat Yourself)原则。
但是后来我开始担心这样做会对性能产生影响。当我们不使用定时器时,在select
中是否会因为有这个额外的通道(代替定时器通道)而减慢应用程序的速度?
因此,我决定进行一些测试来进行比较。
package main
import (
"context"
"fmt"
"reflect"
"time"
)
func prepareChan() chan int {
var count int = 10000000
c := make(chan int, count)
for i := 0; i < count; i++ {
c <- i
}
close(c)
return c
}
func oneChan() int64 {
c := prepareChan()
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("1 Chan - Standard: %dms\n", ms)
return ms
}
func twoChan() int64 {
c := prepareChan()
neverchan1 := make(chan struct{}, 0)
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
case <-neverchan1:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("2 Chan - Standard: %dms\n", ms)
return ms
}
func threeChan() int64 {
c := prepareChan()
neverchan1 := make(chan struct{}, 0)
neverchan2 := make(chan struct{}, 0)
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
case <-neverchan1:
break
case <-neverchan2:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("3 Chan - Standard: %dms\n", ms)
return ms
}
func fourChan() int64 {
c := prepareChan()
neverchan1 := make(chan struct{}, 0)
neverchan2 := make(chan struct{}, 0)
neverchan3 := make(chan struct{}, 0)
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
case <-neverchan1:
break
case <-neverchan2:
break
case <-neverchan3:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("4 Chan - Standard: %dms\n", ms)
return ms
}
func oneChanReflect() int64 {
c := reflect.ValueOf(prepareChan())
branches := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
}
start := time.Now()
for {
_, _, recvOK := reflect.Select(branches)
if !recvOK {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("1 Chan - Reflect: %dms\n", ms)
return ms
}
func twoChanReflect() int64 {
c := reflect.ValueOf(prepareChan())
neverchan1 := reflect.ValueOf(make(chan struct{}, 0))
branches := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan1, Send: reflect.Value{}},
}
start := time.Now()
for {
_, _, recvOK := reflect.Select(branches)
if !recvOK {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("2 Chan - Reflect: %dms\n", ms)
return ms
}
func threeChanReflect() int64 {
c := reflect.ValueOf(prepareChan())
neverchan1 := reflect.ValueOf(make(chan struct{}, 0))
neverchan2 := reflect.ValueOf(make(chan struct{}, 0))
branches := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan1, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan2, Send: reflect.Value{}},
}
start := time.Now()
for {
_, _, recvOK := reflect.Select(branches)
if !recvOK {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("3 Chan - Reflect: %dms\n", ms)
return ms
}
func fourChanReflect() int64 {
c := reflect.ValueOf(prepareChan())
neverchan1 := reflect.ValueOf(make(chan struct{}, 0))
neverchan2 := reflect.ValueOf(make(chan struct{}, 0))
neverchan3 := reflect.ValueOf(make(chan struct{}, 0))
branches := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan1, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan2, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan3, Send: reflect.Value{}},
}
start := time.Now()
for {
_, _, recvOK := reflect.Select(branches)
if !recvOK {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("4 Chan - Reflect: %dms\n", ms)
return ms
}
func main() {
oneChan()
oneChanReflect()
twoChan()
twoChanReflect()
threeChan()
threeChanReflect()
fourChan()
fourChanReflect()
}
测试结果如下:
1 Chan - Standard: 169ms
1 Chan - Reflect: 1017ms
2 Chan - Standard: 460ms
2 Chan - Reflect: 1593ms
3 Chan - Standard: 682ms
3 Chan - Reflect: 2041ms
4 Chan - Standard: 950ms
4 Chan - Reflect: 2423ms
它与通道的数量成线性关系。事后看来,我想这是有道理的,因为它必须对每个通道进行快速轮询以查看是否有值。如预期的那样,使用reflect
要慢得多。
无论如何,我的问题是:
-
这个结果是否让其他人感到惊讶?我本来期望它会使用基于中断的设计,无论
select
中有多少个通道,它都能保持相同的性能,因为它不需要轮询每个通道。 -
鉴于我尝试解决的原始问题(在
select
中有一个“可选”情况),最好/首选的设计是什么?答案是否只是使用两个不同的select
,一个带有定时器,一个不带?当我有2或3个条件/可选定时器用于不同的情况时,这会变得非常混乱。
编辑:
@Brits建议使用nil通道来表示“永远不返回值”,而不是使用初始化的通道,即使用var neverchan1 chan struct{}
代替neverchan1 := make(chan struct{}, 0)
。以下是新的性能结果:
1 Chan - Standard: 221ms
1 Chan - Reflect: 1639ms
2 Chan - Standard: 362ms
2 Chan - Reflect: 2544ms
3 Chan - Standard: 376ms
3 Chan - Reflect: 3359ms
4 Chan - Standard: 394ms
4 Chan - Reflect: 4123ms
仍然有影响,尤其是从一个通道到两个通道,但是在第二个通道之后,性能影响要比使用初始化的通道小得多。
不过,我仍然想知道这是否是最佳解决方案...
英文:
I discovered something today that threw me for a bit of a loop, and I wanted to run it by the community to see if I'm missing something, or perhaps just designing things poorly.
Use case: I have an input channel, and I want a Go routine to wait for values on that channel. If the context is canceled, exit out. Optionally, also run a callback if it's been waiting a certain amount of time for an input without receiving one.
I started with code like this:
func myRoutine(ctx context.Context, c <-chan int, callbackInterval *time.Duration, callback func() error) {
var timeoutChan <-chan time.Time
var timer *time.Timer
if callbackInterval != nil {
// If we have a callback interval set, create a timer for it
timer = time.NewTimer(*callbackInterval)
timeoutChan = timer.C
} else {
// If we don't have a callback interval set, create
// a channel that will never provide a value.
timeoutChan = make(<-chan time.Time, 0)
}
for {
select {
// Handle context cancellation
case <-ctx.Done():
return
// Handle timeouts
case <-timeoutChan:
callback()
// Handle a value in the channel
case v, ok := <-c:
if !ok {
// Channel is closed, exit out
return
}
// Do something with v
fmt.Println(v)
}
// Reset the timeout timer, if there is one
if timer != nil {
if !timer.Stop() {
// See documentation for timer.Stop() for why this is needed
<-timer.C
}
// Reset the timer
timer.Reset(*callbackInterval)
timeoutChan = timer.C
}
}
}
This design seemed nice because there's no way (as far as I can tell) to have a conditional case
in a select
(I think it's possible with reflect
, but that's usually super slow), so instead of having two different selects
(one with the timer when a timer is needed, one without) and an if
to select between them, I just did one select
where the timer channel never provides a value if a timer isn't desired. Keep it DRY.
But then I started wondering about the performance impacts of this. When we're not using a timer, would it slow down the application to have this extra channel in the select
that never gets a value (in place of the timer channel)?
So, I decided to do some testing to compare.
package main
import (
"context"
"fmt"
"reflect"
"time"
)
func prepareChan() chan int {
var count int = 10000000
c := make(chan int, count)
for i := 0; i < count; i++ {
c <- i
}
close(c)
return c
}
func oneChan() int64 {
c := prepareChan()
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("1 Chan - Standard: %dms\n", ms)
return ms
}
func twoChan() int64 {
c := prepareChan()
neverchan1 := make(chan struct{}, 0)
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
case <-neverchan1:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("2 Chan - Standard: %dms\n", ms)
return ms
}
func threeChan() int64 {
c := prepareChan()
neverchan1 := make(chan struct{}, 0)
neverchan2 := make(chan struct{}, 0)
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
case <-neverchan1:
break
case <-neverchan2:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("3 Chan - Standard: %dms\n", ms)
return ms
}
func fourChan() int64 {
c := prepareChan()
neverchan1 := make(chan struct{}, 0)
neverchan2 := make(chan struct{}, 0)
neverchan3 := make(chan struct{}, 0)
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
case <-neverchan1:
break
case <-neverchan2:
break
case <-neverchan3:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("4 Chan - Standard: %dms\n", ms)
return ms
}
func oneChanReflect() int64 {
c := reflect.ValueOf(prepareChan())
branches := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
}
start := time.Now()
for {
_, _, recvOK := reflect.Select(branches)
if !recvOK {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("1 Chan - Reflect: %dms\n", ms)
return ms
}
func twoChanReflect() int64 {
c := reflect.ValueOf(prepareChan())
neverchan1 := reflect.ValueOf(make(chan struct{}, 0))
branches := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan1, Send: reflect.Value{}},
}
start := time.Now()
for {
_, _, recvOK := reflect.Select(branches)
if !recvOK {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("2 Chan - Reflect: %dms\n", ms)
return ms
}
func threeChanReflect() int64 {
c := reflect.ValueOf(prepareChan())
neverchan1 := reflect.ValueOf(make(chan struct{}, 0))
neverchan2 := reflect.ValueOf(make(chan struct{}, 0))
branches := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan1, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan2, Send: reflect.Value{}},
}
start := time.Now()
for {
_, _, recvOK := reflect.Select(branches)
if !recvOK {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("3 Chan - Reflect: %dms\n", ms)
return ms
}
func fourChanReflect() int64 {
c := reflect.ValueOf(prepareChan())
neverchan1 := reflect.ValueOf(make(chan struct{}, 0))
neverchan2 := reflect.ValueOf(make(chan struct{}, 0))
neverchan3 := reflect.ValueOf(make(chan struct{}, 0))
branches := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan1, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan2, Send: reflect.Value{}},
{Dir: reflect.SelectRecv, Chan: neverchan3, Send: reflect.Value{}},
}
start := time.Now()
for {
_, _, recvOK := reflect.Select(branches)
if !recvOK {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("4 Chan - Reflect: %dms\n", ms)
return ms
}
func main() {
oneChan()
oneChanReflect()
twoChan()
twoChanReflect()
threeChan()
threeChanReflect()
fourChan()
fourChanReflect()
}
And the results:
1 Chan - Standard: 169ms
1 Chan - Reflect: 1017ms
2 Chan - Standard: 460ms
2 Chan - Reflect: 1593ms
3 Chan - Standard: 682ms
3 Chan - Reflect: 2041ms
4 Chan - Standard: 950ms
4 Chan - Reflect: 2423ms
It scales linearly with the number of channels. In hindsight, I suppose this makes sense, as it must be doing a fast loop to poll each channel to see if it has a value? As expected, using reflect
is far slower.
In any case, my questions are:
-
Does this result surprise anyone else? I would have expected it would use a interrupt-based design that would allow it to maintain the same performance regardless of the number of channels in the
select
, as it wouldn't need to poll each channel. -
Given the original problem that I was trying to solve (an "optional" case in a
select
), what would be the best/preferred design? Is the answer just to have two differentselect
s, one with the timer and one without? That gets awfully messy when I have 2 or 3 conditional/optional timers for various things.
EDIT:
@Brits suggested using a nil channel for "never returning a value" instead of an initialized channel, i.e. using var neverchan1 chan struct{}
instead of neverchan1 := make(chan struct{}, 0)
. Here are the new performance results:
1 Chan - Standard: 221ms
1 Chan - Reflect: 1639ms
2 Chan - Standard: 362ms
2 Chan - Reflect: 2544ms
3 Chan - Standard: 376ms
3 Chan - Reflect: 3359ms
4 Chan - Standard: 394ms
4 Chan - Reflect: 4123ms
There's still an effect, most noticeably from one channel in the select
to two, but after the second one the performance impact is much smaller than with an initialized channel.
Still wondering if this is the best possible solution though...
答案1
得分: 1
根据评论,使用一个“从不提供值的通道”与select
的替代方法是使用一个nil
通道(“永远不准备好通信”)。根据以下示例,将neverchan1 := make(chan struct{}, 0)
替换为var neverchan1 chan struct{}
(或neverchan1 := chan struct{}(nil)
):
func twoChan() int64 {
c := prepareChan()
var neverchan1 chan struct{} // was neverchan1 := make(chan struct{}, 0)
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
case <-neverchan1:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("2 Chan - Standard: %dms\n", ms)
return ms
}
这样可以显著缩小差距(使用4个通道版本,差距更大 - 我的机器比你的慢一点):
4 Chan - Standard: 1281ms
4 Chan - Nil: 394ms
这是最好的解决方案吗?
不是的;但这可能涉及一些汇编代码!你可以尝试一些可能改进的方法(这里有一些非常粗略的示例),但它们的有效性将取决于一系列因素(实际情况与人为测试用例的性能通常有很大差异)。
此时,我会问:“优化这个函数对整个应用程序有什么影响?”除非节省几个纳秒会产生实质性的差异(即提高利润!),否则我建议在此处停止,直到你:
- 确认存在需要解决的问题
- 可以在实际场景中对代码进行性能分析(此外,总的来说,我认为“可读性胜过速度”)。
英文:
As per the comments an alternative to using select
with a channel that "channel never provides a value" is to use a nil
channel ("never ready for communication"). Replacing neverchan1 := make(chan struct{}, 0)
with var neverchan1 chan struct{}
(or neverchan1 := chan struct{}(nil)
) as per the following example:
func twoChan() int64 {
c := prepareChan()
var neverchan1 chan struct{} // was neverchan1 := make(chan struct{}, 0)
foundVal := true
start := time.Now()
for {
select {
case _, foundVal = <-c:
break
case <-neverchan1:
break
}
if !foundVal {
break
}
}
ms := time.Since(start).Milliseconds()
fmt.Printf("2 Chan - Standard: %dms\n", ms)
return ms
}
This significantly narrows the gap (using 4 channel version as the difference is greater - my machine is a bit slower than yours):
4 Chan - Standard: 1281ms
4 Chan - Nil: 394ms
> is the best possible solution
No; but that would probably involve some assembler! There are a number things you could do that may improve on this (here are a few very rough examples); however their effectiveness is going to depend on a range of factors (real life vs contrived test case performance often differs significantly).
At this point I would be asking "what impact is optimising this function going to have on the overall application?"; unless saving a few ns
will make a material difference (i.e. improve profit!) I'd suggest stopping at this point until you:
- Have confirmed that there is an issue to address
- Can profile the code in a realistic scenario (also, in general, I feel that "readability beats speed").
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论