Почему каналы в Go более эффективны для параллельных вычислений, чем общая память?

Пример взят из книги "100 Ошибок Go"

Не могу понять почему использование каналов настолько выгоднее чем простое добавление массива? Они же должны нести расходы не только на создание, но и на операционную деятельность, т.к. за ними лежит структра.

Небольшие пояснения:

Count и Count3 работают со структурой типа Result

Count2 работает со структурой типа Result2


type Input struct {
    a int64
    b int64
}

type Result struct {
    sumA int64
    sumB int64
}

//[56]byte используется для расположение sumA и SumB в разных сегментах кеша
type Result2 struct {
    sumA int64
    _    [56]byte
    sumB int64
}

// 1048576 - 2^20 для наглядности
func CreateInput() []Input {
    input := make([]Input, 0, 1048576)

    for i := int64(0); i < 1048576; i++ {
        input = append(input, Input{
            a: i,
            b: i,
        })
    }

    return input
}

func Count(inputs []Input) Result {
    wg := sync.WaitGroup{}
    wg.Add(2)

    result := Result{}

    go func() {
        for i := 0; i < len(inputs); i++ {
            result.sumA += inputs[i].a
        }

        wg.Done()
    }()

    go func() {
        for i := 0; i < len(inputs); i++ {
            result.sumB += inputs[i].b
        }

        wg.Done()
    }()

    wg.Wait()

    return result
}

func Count2(inputs []Input) Result2 {
    wg := sync.WaitGroup{}
    wg.Add(2)

    result := Result2{}

    go func() {
        for i := 0; i < len(inputs); i++ {
            result.sumA += inputs[i].a
        }

        wg.Done()
    }()

    go func() {
        for i := 0; i < len(inputs); i++ {
            result.sumB += inputs[i].b
        }

        wg.Done()
    }()

    wg.Wait()

    return result
}

func Count3(inputs []Input) Result {
    var result Result
    sumAChan := make(chan int64)
    sumBChan := make(chan int64)

    go func() {
        var sumA int64
        for i := 0; i < len(inputs); i++ {
            sumA += inputs[i].a
        }
        sumAChan <- sumA
    }()

    go func() {
        var sumB int64
        for i := 0; i < len(inputs); i++ {
            sumB += inputs[i].b
        }
        sumBChan <- sumB
    }()

    result.sumA = <-sumAChan
    result.sumB = <-sumBChan

    return result
}

Update: результат сильно зависит от параметра -coverprofile

Count2 и Count3 очень близки, но почему -coverprofile оказывает такую сильную разницу, только на Count2? без него средние результаты такие:

          │  stats.txt   │
          │    sec/op    │
Count-12    18.24m ±  2%
Count2-12   677.0µ ±  7%
Count3-12   689.8µ ± 12%
geomean     2.042m

          │  stats.txt  │
          │    B/op     │
Count-12    162.5 ± 19%
Count2-12   192.0 ±  6%
Count3-12   288.0 ±  0%
geomean     207.9

          │ stats.txt  │
          │ allocs/op  │
Count-12    4.000 ± 0%
Count2-12   4.000 ± 0%
Count3-12   4.000 ± 0%
geomean     4.000

Вот получившиеся результаты по бенчмаркам при 1048576 элементах c -coverprofile

cpu: AMD Ryzen 5 5600G with Radeon Graphics         
BenchmarkCount-12             49      25359128 ns/op         237 B/op          4 allocs/op
BenchmarkCount2-12           295       3661261 ns/op         300 B/op          4 allocs/op
BenchmarkCount3-12          1946        644967 ns/op         288 B/op          4 allocs/op

Вот получившиеся результаты по бенчмаркам без -coverprofile

BenchmarkCount-12             67          18467245 ns/op             209 B/op          4 allocs/op
BenchmarkCount2-12          1833            597416 ns/op             211 B/op          4 allocs/op
BenchmarkCount3-12          1938            734245 ns/op             288 B/op          4 allocs/op

код бенчмарка

var res test.Result
var res2 test.Result2
var res3 test.Result

// BenchmarkResult placeholder.
func BenchmarkCount(b *testing.B) {
    var sum test.Result
    b.StopTimer()

    input := test.CreateInput()

    b.StartTimer()

    for i := 0; i < b.N; i++ {
        sum = test.Count(input)
    }

    res = sum
}

func BenchmarkCount2(b *testing.B) {
    var sum test.Result2
    b.StopTimer()

    input := test.CreateInput()

    b.StartTimer()

    for i := 0; i < b.N; i++ {
        sum = test.Count2(input)
    }

    res2 = sum
}

func BenchmarkCount3(b *testing.B) {
    var sum test.Result
    b.StopTimer()

    input := test.CreateInput()

    b.StartTimer()

    for i := 0; i < b.N; i++ {
        sum = test.Count3(input)
    }

    res3 = sum
}

команда для запуска бенчмарка, запускал через графический интерфейс в vscode

go test -benchmem -run=^$ -coverprofile=/tmp/vscode-govayH6A/go-code-cover -bench . alice/effective/internal/test

Ответы (1 шт):

Автор решения: lol9 kawaii

На изначальный вопрос, ответ я получил. В данном случае разница между каналами и расположением в разных сегментах кэша практически отсутствует. Влияние на результаты бенчмарков оказывает флаг -coverprofile

Вот получившиеся результаты по бенчмаркам при 1048576 элементах c -coverprofile

cpu: AMD Ryzen 5 5600G with Radeon Graphics         
BenchmarkCount-12             49      25359128 ns/op         237 B/op          4 allocs/op
BenchmarkCount2-12           295       3661261 ns/op         300 B/op          4 allocs/op
BenchmarkCount3-12          1946        644967 ns/op         288 B/op          4 allocs/op

Вот получившиеся результаты по бенчмаркам без -coverprofile

BenchmarkCount-12             67          18467245 ns/op             209 B/op          4 allocs/op
BenchmarkCount2-12          1833            597416 ns/op             211 B/op          4 allocs/op
BenchmarkCount3-12          1938            734245 ns/op             288 B/op          4 allocs/op
→ Ссылка