Skip to content

High Memory Usage with ethtool.Stats Function in Go 1.24+ #104

@ritwikranjan

Description

@ritwikranjan

Description:

We have observed a significant increase in memory churn when using the ethtool.Stats function with Go version 1.24 and onwards. This is due to a change in the Go compiler's memory allocation strategy, where objects larger than 128KB are now allocated on the heap instead of the stack. Previously, this threshold was in the range of MBs.

As a result, every time the Stats function is called, large objects like gstrings and stats are allocated on the heap, leading to increased memory pressure and frequent garbage collection.

Steps to Reproduce:

  1. Use Go 1.24 or newer.
  2. Call the ethtool.Stats function in a program.

Observed Behavior:

  • A large amount of memory is allocated on the heap for gstrings and stats variables during each call to ethtool.Stats.
  • This memory is then left for the garbage collector to clean up, resulting in frequent garbage collection cycles and potential performance degradation.

Expected Behavior:

  • Memory allocation behavior should be optimized to avoid excessive heap allocations for large local variables like gstrings and stats.
  • A consistent and efficient allocation strategy should be implemented to minimize garbage collection overhead.

Reference:

This behavior is linked to changes in Go's escape analysis and memory allocation strategy introduced in Go 1.24. For more details, refer to the related Golang issue: golang/go#73536.

Analysis with pprof:

This issue can be easily analyzed using pprof. Below is a sample program that demonstrates the high memory churn:

package main

import (
	"fmt"
	"log"
	"net"
	"time"

	"net/http"
	_ "net/http/pprof"

	"github.com/safchain/ethtool"
)

func main() {
	go func() {
		log.Println(http.ListenAndServe("localhost:8000", nil))
	}()

	ifs, err := net.Interfaces()
	if err != nil {
		log.Fatal(err)
	}
	if len(ifs) == 0 {
		log.Fatal("No interfaces found")
	}

	ticker := time.NewTicker(10 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			eth, err := ethtool.NewEthtool()
			if err != nil {
				log.Fatalf("Error creating ethtool: %v", err)
			}
			for _, iface := range ifs {
				stats, err := eth.Stats(iface.Name)
				if err != nil {
					log.Printf("Error getting stats for %s: %v", iface.Name, err)
					continue
				}
				fmt.Printf("Stats for %s: %+v\n", iface.Name, stats)
			}
			eth.Close()
		}
	}
}

Using pprof to analyze the heap, we can observe a significant difference in the alloc_space profile between Go versions 1.23 and 1.24. This confirms the issue with increased heap allocations in Go 1.24.

Thank you for addressing this!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions