Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

process: reuse and preallocate memory #355

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

process: reuse and preallocate memory #355

wants to merge 8 commits into from

Conversation

florianl
Copy link
Contributor

No description provided.

@florianl florianl requested review from a team as code owners February 17, 2025 11:13
@rockdaboot
Copy link
Contributor

Can you add memory allocations for before and after?

@florianl
Copy link
Contributor Author

florianl commented Feb 18, 2025

Updated benchmark on 4b538b5:

$ benchstat old.txt new.txt 
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/ebpf-profiler/process
cpu: 12th Gen Intel(R) Core(TM) i7-12700H
               │   old.txt   │               new.txt               │
               │   sec/op    │   sec/op     vs base                │
GetMappings-20   37.14µ ± 1%   38.01µ ± 1%  +2.34% (p=0.000 n=10)

               │    old.txt    │               new.txt               │
               │     B/op      │     B/op      vs base               │
GetMappings-20   10.574Ki ± 0%   9.605Ki ± 0%  -9.16% (p=0.000 n=10)

               │  old.txt   │              new.txt               │
               │ allocs/op  │ allocs/op   vs base                │
GetMappings-20   28.00 ± 0%   24.00 ± 0%  -14.29% (p=0.000 n=10)

BenchmarkGetMappings
func BenchmarkGetMappings(b *testing.B) {
	pr := New(libpf.PID(os.Getpid()))
	b.ResetTimer()
	b.ReportAllocs()

	for i := 0; i < b.N; i++ {
		mappings, err := pr.GetMappings()
		if err != nil {
			b.Fatal(err)
		}
		if len(mappings) == 0 {
			b.Fatal("expected non-empty mappings")
		}
	}
}

@florianl
Copy link
Contributor Author

florianl commented Feb 18, 2025

Updated benchmark on 4b538b5:

$ benchstat old.txt new.txt 
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/ebpf-profiler/process
cpu: 12th Gen Intel(R) Core(TM) i7-12700H
                 │   old.txt   │              new.txt               │
                 │   sec/op    │   sec/op     vs base               │
ParseMappings-20   202.4µ ± 2%   204.4µ ± 5%  +0.97% (p=0.009 n=10)

                 │   old.txt    │               new.txt               │
                 │     B/op     │     B/op      vs base               │
ParseMappings-20   164.7Ki ± 0%   162.3Ki ± 0%  -1.48% (p=0.000 n=10)

                 │  old.txt   │              new.txt              │
                 │ allocs/op  │ allocs/op   vs base               │
ParseMappings-20   444.0 ± 0%   438.0 ± 0%  -1.35% (p=0.000 n=10)

BenchmarkParseMappings
func BenchmarkParseMappings(b *testing.B) {
    // Get sample data from current process
    mapsFile, err := os.Open(fmt.Sprintf("/proc/%d/maps", os.Getpid()))
    if err != nil {
        b.Fatal(err)
    }
    defer mapsFile.Close()
    
    // Read all content for reuse in benchmark
    content, err := io.ReadAll(mapsFile)
    if err != nil {
        b.Fatal(err)
    }

    b.ResetTimer()
    b.ReportAllocs()
    
    for i := 0; i < b.N; i++ {
        reader := bytes.NewReader(content)
        mappings, err := parseMappings(reader)
        if err != nil {
            b.Fatal(err)
        }
        if len(mappings) == 0 {
            b.Fatal("expected non-empty mappings")
        }
    }
}

Co-authored-by: Tim Rühsen <[email protected]>
florianl and others added 2 commits February 21, 2025 08:18
Co-authored-by: Christos Kalkanis <[email protected]>
Co-authored-by: Christos Kalkanis <[email protected]>
Comment on lines +90 to +93
// Reset memory and return it for reuse.
for j := 0; j < len(*scanBuf); j++ {
(*scanBuf)[j] = 0x0
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought... why clear the memory at all here? parseMapping() does not rely on the contents of the buffer retrieved from the pool. The scanner read data and only acts on the read data in the buffer. It doesn't matter if we fill the bufferswith 0 bytes or random bytes or leave it as is.

Suggested change
// Reset memory and return it for reuse.
for j := 0; j < len(*scanBuf); j++ {
(*scanBuf)[j] = 0x0
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scanner, that uses this buffer, uses the default split function ScanLines. By not clearing the buffer, we might introduce subtle bugs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might introduce subtle bugs

Possibly yes. Can you add a test case that triggers such a bug. That makes it obvious to if someone ever touches that code or wonders if it is required.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I can't see how bufio.Scanner.Scan() uses ScanLines().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we don't set a specific Split function, the buffer uses the default https://pkg.go.dev/bufio#NewScanner.

Copy link
Member

@christos68k christos68k Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're not sure, I'd rather we clear the buffer than introduce a subtle bug. The performance implications here should be minimal to non-existent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no indication of subtle bugs here (the std library code is pretty clear). If you think there are, please add a test case to catch it. The split function only operates on the bytes that have been read by Read(), so whatever bytes we set to 0, they will be overwritten before being acted upon. I don't think we should introduce code because "there could be bugs inside the std library", that isn't fact-driven development.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reverted the applied suggestion, as I should not have applied in the first place. And I also reasked for review.
I have reverted the applied suggestion as resetting a buffer before reusing is something I will ask in a review, until it is shown that the operation can be done correctly with every state of the buffer. And this proposed change does not show that it will work as supposed with every state of the shared buffer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still didn't see any argument why or what exactly can fail. Without an explicit statement it is not possible to create a test case. Can't we just go with the facts instead of having some vague "fear"?

Co-authored-by: Tim Rühsen <[email protected]>
@florianl florianl requested a review from rockdaboot February 24, 2025 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants