[GO-mem alloc] Pointer Receiver vs Value Receiver

// service.go

package service

type Service interface {
    Hello()
    Inc()
}

type ServiceOne struct {
    Name string
    Nums []int
}

type ServiceTwo struct {
    Name string
    Nums []int
}

func (s ServiceOne) Inc() {
    for i := 0; i < 100000; i++ {
        s.Nums = append(s.Nums, i)
    }
}

func (s ServiceTwo) Inc() {
    for i := 0; i < 100000; i++ {
        s.Nums = append(s.Nums, i)
    }
}
// service_test.go
package service

import "testing"

func BenchmarkIncServiceOne(t *testing.B) {
    service := ServiceOne{
        Name: "Service One",
    }

    for i := 0; i < t.N; i++ {
        service.Inc()
    }
}

func BenchmarkIncServiceTwo(t *testing.B) {
    service := ServiceTwo{
        Name: "Service Two",
    }

    for i := 0; i < t.N; i++ {
        service.Inc()
    }
}

In this example, we have two structs, ServiceOne and ServiceTwo, that implement the Service interface. Both structs have a Nums field, which is an integer slice. We also have an Inc method defined on both structs, which appends numbers to the Nums slice.

In the BenchmarkIncServiceOne and BenchmarkIncServiceTwo functions, we create instances of ServiceOne and ServiceTwo, respectively. We then call the Inc method on these instances in a loop t.N times, which is a special variable provided by the testing package that represents the number of times the benchmark should be run.

To analyze the memory allocation of these functions, we can use the -benchmem flag with the go test command. This flag tells Go to print memory allocation statistics for each benchmark. Let's run the benchmarks and analyze the results:

$ go test -benchmem -run=^$ -bench=. ./service_test.go
BenchmarkIncServiceOne-8         1        1695151900 ns/op        4880978 B/op    100000 allocs/op
BenchmarkIncServiceTwo-8         1        1703386300 ns/op        4880968 B/op    100000 allocs/op

From the results, we can see that both benchmarks take around 1.7 seconds to complete. The B/op column shows the number of bytes allocated per operation, and the allocs/op column shows the number of allocations performed per operation.

Both benchmarks allocate 4880968 bytes per operation and perform 100000 allocations per operation. This is because both benchmarks are creating a new slice in each call to the Inc method, which requires allocating memory for the slice and the elements it contains.

To optimize the memory allocation, we can modify the Inc method to reuse the existing slice instead of creating a new one. Here's an updated version of the ServiceOne struct that does this:

type ServiceOne struct {
    Name string
    Nums []int
}

func (s *ServiceOne) Inc() {
    for i := 0; i < 100000; i++ {
        s.Nums = append(s.Nums, i)
    }
}

With this change, the Inc method will append elements to the existing slice instead of creating a new one. Let's run the benchmarks again and see if this reduces the memory allocation:

$ go test -benchmem -run=^$ -bench=. ./service_test.go
BenchmarkIncServiceOne-8         1         279630100 ns/op        624 B/op          1 allocs/op
BenchmarkIncServiceTwo-8         1        1710347900 ns/op        4880968 B/op    100000 allocs/op

Conclusion


As for the conclusion, choosing between a value receiver and a pointer receiver for a function depends on the specific use case and performance requirements.

In general, using a pointer receiver is more efficient for methods that modify the receiver's state, especially if the receiver is large. This is because using a pointer receiver allows the method to directly access and modify the receiver's memory, rather than creating a copy of the receiver.

However, using a value receiver can be more efficient for methods that don't modify the receiver's state, especially if the receiver is small. This is because using a value receiver allows the method to avoid creating a pointer to the receiver, which reduces the memory overhead.

It's important to note that the performance difference between using a value receiver and a pointer receiver may not always be significant, and it's always a good practice to benchmark and profile the code to identify the most efficient approach for a specific use case.

Did you find this article valuable?

Support Muhammad Hafizh Abdillah AR by becoming a sponsor. Any amount is appreciated!