0

Given an exemplary

type container struct {
  arr int[]
}

I want to recursively manipulate its arr array field values.

I have two options, functionally:

  • a (pointer receiver) method on container, directly accessing its arr array
  • a function, receiving the arr slice as parameter

In any case, alternating start and end indices have to be passed as parameters to each recursive call.

Question:
Are there any known, theoretically beneficial or detrimental impacts, mainly with regards to performance¹ and memory footprint, by using one implementation over the other and considering that arr will hold between 10^6 and 10^9 elements?


¹ running (unprofessionally bench-marked) test implementations, performance does not seem to be significantly different

t.ry
  • 97
  • 5
  • _"unprofessionally bench-marked"_ Use go's testing tool which comes with built-in benchmarking capabilities. – icza May 04 '23 at 11:58
  • @icza gotcha, yet I am interested in general pitfalls for these two implementations - e.g. '*(tail) recursion can not be optimized for pointer receiver methods*' or '*accessing struct fields is generally slower (or vice versa)*' or '*call stack size is going to be an issue with this implementation*' – t.ry May 04 '23 at 12:07
  • 4
    The Go compiler performs lots of optimization and inlining which can vary from use case to use case, often giving surprising results (e.g. code being faster that logically should be slower). That's why I'm suggesting to benchmark _your_ code and see how they perform. – icza May 04 '23 at 12:12
  • In general, using a pointer receiver method to manipulate the arr array directly may have better performance than passing the slice as a parameter to a function. This is because passing a large slice as a parameter to a function requires copying the slice header and underlying array, which can be costly in terms of memory and time, especially if the slice is very large (in your case, between 10^6 and 10^9 elements). – techStud May 04 '23 at 19:03
  • On the other hand, using a function with a slice parameter can have some advantages, such as making the code more modular and easier to test. It also avoids the potential issue of concurrent access to the same data structure, which can occur when using a method with a pointer receiver. In terms of memory footprint, both approaches should be similar, as the underlying array of the slice will be shared between the struct and the function, and only the slice header and parameters need to be stored on the stack. – techStud May 04 '23 at 19:04
  • Overall, the choice between using a pointer receiver method or a function with a slice parameter may depend on the specific requirements and constraints of your use case, such as code modularity, concurrent access, and performance. – techStud May 04 '23 at 19:04
  • 1
    @techStud _"This is because passing a large slice as a parameter to a function requires copying the slice header and underlying array"_ - this is wrong. See [Are slices passed by value?](https://stackoverflow.com/questions/39993688/are-slices-passed-by-value/39993797#39993797) – icza May 04 '23 at 20:01

1 Answers1

2

I think that you can use both options, but I recommend to use pointer to array because it will protect you from accidental append to the slice which can lead to copying of the entire slice memory.

Also keep in mind that you cannot use array in such way:

func foo(sl []int) {
    sl[1] = 15
}

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    foo(arr)
}

Output:

cannot use arr (variable of type [5]int) as []int value in argument to foo

But you can convert array into slice without copying it:

package main

import (
    "fmt"
)

func foo(sl []int) {
    sl[1] = 55
}

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    sl := arr[:]

    fmt.Printf("arr=%v, sl=%v\n", arr, sl)

    arr[1] = 5
    fmt.Printf("arr=%v, sl=%v\n", arr, sl)

    sl[1] = 15
    fmt.Printf("arr=%v, sl=%v\n", arr, sl)

    foo(sl)
    fmt.Printf("arr=%v, sl=%v\n", arr, sl)
}

Output:

arr=[1 2 3 4 5], sl=[1 2 3 4 5]
arr=[1 5 3 4 5], sl=[1 5 3 4 5]
arr=[1 15 3 4 5], sl=[1 15 3 4 5]
arr=[1 55 3 4 5], sl=[1 55 3 4 5]
Eugene Mikhalev
  • 598
  • 3
  • 13
  • Thanks for the suggestion! And the warning, although I only ever need to switch elements, not change the size in any way. Looks like Go has no explicit tail recursion optimization whatsoever built in, and apparently won't have it for the time being. I went with the function way for now, not using a method on the struct - mainly because I was able to reuse it elsewhere. – t.ry Jul 05 '23 at 09:38