Jag
Jag

Reputation: 547

Why having a default clause in a goroutine's select makes it slower?

Referring to the following benchmarking test codes:

func BenchmarkRuneCountNoDefault(b *testing.B) {
    b.StopTimer()
    var strings []string
    numStrings := 10
    for n := 0; n < numStrings; n++{
        s := RandStringBytesMaskImprSrc(10)
        strings = append(strings, s)
    }
    jobs := make(chan string)
    results := make (chan int)

    for i := 0; i < runtime.NumCPU(); i++{
        go RuneCountNoDefault(jobs, results)
    }
    b.StartTimer()

    for n := 0; n < b.N; n++ {
        go func(){
            for n := 0; n < numStrings; n++{
                <-results
            }
            return
        }()

        for n := 0; n < numStrings; n++{
            jobs <- strings[n]
        }
    }

    close(jobs)
}

func RuneCountNoDefault(jobs chan string, results chan int){
    for{
        select{
        case j, ok := <-jobs:
            if ok{
                results <- utf8.RuneCountInString(j)
            } else {
                return
            }
        }
    }
}

func BenchmarkRuneCountWithDefault(b *testing.B) {
    b.StopTimer()
    var strings []string
    numStrings := 10
    for n := 0; n < numStrings; n++{
        s := RandStringBytesMaskImprSrc(10)
        strings = append(strings, s)
    }
    jobs := make(chan string)
    results := make (chan int)

    for i := 0; i < runtime.NumCPU(); i++{
        go RuneCountWithDefault(jobs, results)
    }
    b.StartTimer()

    for n := 0; n < b.N; n++ {
        go func(){
            for n := 0; n < numStrings; n++{
                <-results
            }
            return
        }()

        for n := 0; n < numStrings; n++{
            jobs <- strings[n]
        }
    }

    close(jobs)
}


func RuneCountWithDefault(jobs chan string, results chan int){
    for{
        select{
        case j, ok := <-jobs:
            if ok{
                results <- utf8.RuneCountInString(j)
            } else {
                return
            }
        default: //DIFFERENCE
        }
    }
}

//https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
    letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

var src = rand.NewSource(time.Now().UnixNano())

func RandStringBytesMaskImprSrc(n int) string {
    b := make([]byte, n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

When I benchmarked both the functions where one function, RuneCountNoDefault has no default clause in the select and the other, RuneCountWithDefault has a default clause, I'm getting the following benchmark:

BenchmarkRuneCountNoDefault-4             200000              8910 ns/op
BenchmarkRuneCountWithDefault-4                5         277798660 ns/op

Checking the cpuprofile generated by the tests, I noticed that the function with the default clause spends a lot of time in the following channel operations:

enter image description here

Why having a default clause in the goroutine's select makes it slower?

I'm using Go version 1.10 for windows/amd64

Upvotes: 3

Views: 113

Answers (1)

peterSO
peterSO

Reputation: 166795

The Go Programming Language Specification

Select statements

If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the "select" statement blocks until at least one of the communications can proceed.


Modifying your benchmark to count the number of proceed and default cases taken:

$ go test default_test.go -bench=.
goos: linux
goarch: amd64
BenchmarkRuneCountNoDefault-4         300000          4108 ns/op
BenchmarkRuneCountWithDefault-4           10     209890782 ns/op
--- BENCH: BenchmarkRuneCountWithDefault-4
    default_test.go:90: proceeds 114
    default_test.go:91: defaults 128343308
$ 

While other cases were unable to proceed, the default case was taken 128343308 times in 209422470, (209890782 - 114*4108), nanoseconds or 1.63 nanoseconds per default case. If you do something small a large number of times, it adds up.


default_test.go:

package main

import (
    "math/rand"
    "runtime"
    "sync/atomic"
    "testing"
    "time"
    "unicode/utf8"
)

func BenchmarkRuneCountNoDefault(b *testing.B) {
    b.StopTimer()
    var strings []string
    numStrings := 10
    for n := 0; n < numStrings; n++ {
        s := RandStringBytesMaskImprSrc(10)
        strings = append(strings, s)
    }
    jobs := make(chan string)
    results := make(chan int)

    for i := 0; i < runtime.NumCPU(); i++ {
        go RuneCountNoDefault(jobs, results)
    }
    b.StartTimer()

    for n := 0; n < b.N; n++ {
        go func() {
            for n := 0; n < numStrings; n++ {
                <-results
            }
            return
        }()

        for n := 0; n < numStrings; n++ {
            jobs <- strings[n]
        }
    }

    close(jobs)
}

func RuneCountNoDefault(jobs chan string, results chan int) {
    for {
        select {
        case j, ok := <-jobs:
            if ok {
                results <- utf8.RuneCountInString(j)
            } else {
                return
            }
        }
    }
}

var proceeds ,defaults uint64

func BenchmarkRuneCountWithDefault(b *testing.B) {
    b.StopTimer()
    var strings []string
    numStrings := 10
    for n := 0; n < numStrings; n++ {
        s := RandStringBytesMaskImprSrc(10)
        strings = append(strings, s)
    }
    jobs := make(chan string)
    results := make(chan int)

    for i := 0; i < runtime.NumCPU(); i++ {
        go RuneCountWithDefault(jobs, results)
    }
    b.StartTimer()

    for n := 0; n < b.N; n++ {
        go func() {
            for n := 0; n < numStrings; n++ {
                <-results
            }
            return
        }()

        for n := 0; n < numStrings; n++ {
            jobs <- strings[n]
        }
    }

    close(jobs)

    b.Log("proceeds", atomic.LoadUint64(&proceeds))
    b.Log("defaults", atomic.LoadUint64(&defaults))

}

func RuneCountWithDefault(jobs chan string, results chan int) {
    for {
        select {
        case j, ok := <-jobs:

            atomic.AddUint64(&proceeds, 1)

            if ok {
                results <- utf8.RuneCountInString(j)
            } else {
                return
            }
        default: //DIFFERENCE

            atomic.AddUint64(&defaults, 1)

        }
    }
}

//https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
    letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

var src = rand.NewSource(time.Now().UnixNano())

func RandStringBytesMaskImprSrc(n int) string {
    b := make([]byte, n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

Playground: https://play.golang.org/p/DLnAY0hovQG

Upvotes: 8

Related Questions