laurent
laurent

Reputation: 90776

Performance of function slice parameter vs global variable?

I've got the following function:

func checkFiles(path string, excludedPatterns []string) {
    // ...
}

I'm wondering, since excludedPatterns never changes, should I optimize it by making the var global (and not passing it to the function every time), or does Golang already handle this by passing them as copy-on-write?

Edit: I guess I could pass the slice as a pointer, but I'm still wondering about the copy-on-write behavior (if it exists) and whether, in general, I should worry about passing by value or by pointer.

Upvotes: 16

Views: 13437

Answers (3)

Teocci
Teocci

Reputation: 8895

I rewrote the benchmark so you can compare the results.

As you can see the ParameterPointer bench start to get ahead after 10 records. Which is very interesting.

package slices_bench

import (
    "testing"
)

var gslice = make([]string, 1000)

func global(s string) {
    for i := 0; i < 100; i++ { // Cycle to access slice may times
        _ = s
        _ = gslice // Access global-slice
    }
}

func param(s string, ss []string) {
    for i := 0; i < 100; i++ { // Cycle to access slice may times
        _ = s
        _ = ss // Access parameter-slice
    }
}

func paramPointer(s string, ss *[]string) {
    for i := 0; i < 100; i++ { // Cycle to access slice may times
        _ = s
        _ = ss // Access parameter-slice
    }
}

func BenchmarkPerformance(b *testing.B){
    fixture := []struct {
        desc    string
        records int
    }{
        {
            desc:    "1 record",
            records: 1,
        },
        {
            desc:    "10 records",
            records: 10,
        },
        {
            desc:    "100 records",
            records: 100,
        },
        {
            desc:    "1000 records",
            records: 1000,
        },
        {
            desc:    "10000 records",
            records: 10000,
        },
        {
            desc:    "100000 records",
            records: 100000,
        },
    }

    tests := []struct {
        desc string
        fn   func(b *testing.B, n int)
    }{
        {
            desc: "ParameterPointer",
            fn: func(b *testing.B, n int) {
                for j := 0; j < n; j++ {
                    paramPointer("hi", &gslice)
                }
            },
        },
        {
            desc: "Parameter",
            fn: func(b *testing.B, n int) {
                for j := 0; j < n; j++ {
                    param("hi", gslice)
                }
            },
        },
        {
            desc: "Global",
            fn: func(b *testing.B, n int) {
                for j := 0; j < n; j++ {
                    global("hi")
                }
            },
        },
    }

    for _, t := range tests {
        b.Run(t.desc, func(b *testing.B) {
            for _, f := range fixture {
                b.Run(f.desc, func(b *testing.B) {
                    b.ReportAllocs()
                    b.ResetTimer()
                    for i := 0; i < b.N; i++ {
                        t.fn(b, f.records)
                    }
                })
            }
        })
    }
}

Results:

goos: windows
goarch: amd64
pkg: benchs/slices-bench
cpu: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz
BenchmarkPerformance
BenchmarkPerformance/ParameterPointer
BenchmarkPerformance/ParameterPointer/1_record
BenchmarkPerformance/ParameterPointer/1_record-16           38661910            31.18 ns/op        0 B/op          0 allocs/op
BenchmarkPerformance/ParameterPointer/10_records
BenchmarkPerformance/ParameterPointer/10_records-16          4160023           288.4 ns/op         0 B/op          0 allocs/op
BenchmarkPerformance/ParameterPointer/100_records
BenchmarkPerformance/ParameterPointer/100_records-16          445131          2748 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/ParameterPointer/1000_records
BenchmarkPerformance/ParameterPointer/1000_records-16          43876         27380 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/ParameterPointer/10000_records
BenchmarkPerformance/ParameterPointer/10000_records-16          4441        273922 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/ParameterPointer/100000_records
BenchmarkPerformance/ParameterPointer/100000_records-16          439       2739282 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/Parameter
BenchmarkPerformance/Parameter/1_record
BenchmarkPerformance/Parameter/1_record-16                  39860619            30.79 ns/op        0 B/op          0 allocs/op
BenchmarkPerformance/Parameter/10_records
BenchmarkPerformance/Parameter/10_records-16                 4152728           288.6 ns/op         0 B/op          0 allocs/op
BenchmarkPerformance/Parameter/100_records
BenchmarkPerformance/Parameter/100_records-16                 445634          2757 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/Parameter/1000_records
BenchmarkPerformance/Parameter/1000_records-16                 43618         27496 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/Parameter/10000_records
BenchmarkPerformance/Parameter/10000_records-16                 4450        273960 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/Parameter/100000_records
BenchmarkPerformance/Parameter/100000_records-16                 435       2739053 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/Global
BenchmarkPerformance/Global/1_record
BenchmarkPerformance/Global/1_record-16                     38813095            30.97 ns/op        0 B/op          0 allocs/op
BenchmarkPerformance/Global/10_records
BenchmarkPerformance/Global/10_records-16                    4148433           288.4 ns/op         0 B/op          0 allocs/op
BenchmarkPerformance/Global/100_records
BenchmarkPerformance/Global/100_records-16                    429274          2758 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/Global/1000_records
BenchmarkPerformance/Global/1000_records-16                    43591         27412 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/Global/10000_records
BenchmarkPerformance/Global/10000_records-16                    4521        274420 ns/op           0 B/op          0 allocs/op
BenchmarkPerformance/Global/100000_records
BenchmarkPerformance/Global/100000_records-16                    436       2751042 ns/op           0 B/op          0 allocs/op

Upvotes: 2

IvanD
IvanD

Reputation: 2933

Piggybacking on @icza's excellent answer, there is another way to pass a slice as parameter: a pointer to a slice.

When you need to modify the underlying slice, the global variable slice works, but passing it as a parameter does not work, you are effectively working with a copy. To mitigate that, one can actually pass the slice as a pointer.

Interesting enough, it's actually faster than accessing a global variable:

package main

import (
    "testing"
)

var gslice = make([]string, 1000000)

func global(s string) {
    for i := 0; i < 100; i++ { // Cycle to access slice may times
        _ = s
        _ = gslice // Access global-slice
    }
}

func param(s string, ss []string) {
    for i := 0; i < 100; i++ { // Cycle to access slice may times
        _ = s
        _ = ss // Access parameter-slice
    }
}

func paramPointer(s string, ss *[]string) {
    for i := 0; i < 100; i++ { // Cycle to access slice may times
        _ = s
        _ = ss // Access parameter-slice
    }
}

func BenchmarkParameter(b *testing.B) {
    for i := 0; i < b.N; i++ {
        param("hi", gslice)
    }
}

func BenchmarkParameterPointer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        paramPointer("hi", &gslice)
    }
}

func BenchmarkGlobal(b *testing.B) {
    for i := 0; i < b.N; i++ {
        global("hi")
    }
}

Results:

goos: darwin
goarch: amd64
pkg: untitled
BenchmarkParameter-8            24437403                48.2 ns/op
BenchmarkParameterPointer-8     27483115                40.3 ns/op
BenchmarkGlobal-8               25631470                46.0 ns/op

Upvotes: 3

icza
icza

Reputation: 417662

Judging from the name of your function, performance can't be that critical to even consider moving parameters to global variables just to save time/space required to pass them as parameters (IO operations like checking files are much-much slower than calling functions and passing values to them).

Slices in Go are just small descriptors, something like a struct with a pointer to a backing array and 2 ints, a length and capacity. No matter how big the backing array is, passing slices are always efficient and you shouldn't even consider passing a pointer to them, unless you want to modify the slice header of course.

Parameters in Go are always passed by value, and a copy of the value being passed is made. If you pass a pointer, then the pointer value will be copied and passed. When a slice is passed, the slice value (which is a small descriptor) will be copied and passed - which will point to the same backing array (which will not be copied).

Also if you need to access the slice multiple times in the function, a parameter is usually an extra gain as compilers can make further optimization / caching, while if it is a global variable, more care has to be taken.

More about slices and their internals: Go Slices: usage and internals

And if you want exact numbers on performance, benchmark!

Here comes a little benchmarking code which shows no difference between the 2 solutions (passing slice as argument or accessing a global slice). Save it into a file like slices_test.go and run it with go test -bench .

package main

import (
    "testing"
)

var gslice = make([]string, 1000)

func global(s string) {
    for i := 0; i < 100; i++ { // Cycle to access slice may times
        _ = s
        _ = gslice // Access global-slice
    }
}

func param(s string, ss []string) {
    for i := 0; i < 100; i++ { // Cycle to access slice may times
        _ = s
        _ = ss // Access parameter-slice
    }
}

func BenchmarkParameter(b *testing.B) {
    for i := 0; i < b.N; i++ {
        param("hi", gslice)
    }
}

func BenchmarkGlobal(b *testing.B) {
    for i := 0; i < b.N; i++ {
        global("hi")
    }
}

Example output:

testing: warning: no tests to run
PASS
BenchmarkParameter-2    30000000                55.4 ns/op
BenchmarkGlobal-2       30000000                55.1 ns/op
ok      _/V_/workspace/IczaGo/src/play  3.569s

Upvotes: 22

Related Questions