diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..c490669 --- /dev/null +++ b/config.yml @@ -0,0 +1,130 @@ +baseURL: "https://d1ngd0.github.io/blog/" +title: Paul Montag +paginate: 5 +theme: PaperMod + +enableRobotsTXT: true +buildDrafts: false +buildFuture: false +buildExpired: false + +# googleAnalytics: UA-123-45 + +# minify: + # disableXML: true + # minifyOutput: true + +params: +# env: production # to enable google analytics, opengraph, twitter-cards and schema. + title: Paul Montag + description: "Tech blog covering software engineering, and observability" + keywords: [Blog, Go, Golang, Software, Observability, Software Engineering] + author: Paul Montag + # author: ["Me", "You"] # multiple authors + # images: [""] + DateFormat: "January 2, 2006" + defaultTheme: auto # dark, light + disableThemeToggle: false + + ShowReadingTime: true + ShowShareButtons: true + ShowPostNavLinks: true + ShowBreadCrumbs: true + ShowCodeCopyButtons: false + ShowWordCount: true + ShowRssButtonInSectionTermList: true + UseHugoToc: true + disableSpecial1stPost: false + disableScrollToTop: false + comments: false + hidemeta: false + hideSummary: false + showtoc: false + tocopen: false + + #assets: + ## disableHLJS: true # to disable highlight.js + ## disableFingerprinting: true + #favicon: "" + #favicon16x16: "" + #favicon32x32: "" + #apple_touch_icon: "" + #safari_pinned_tab: "" + + #label: + #text: "Home" + #icon: /apple-touch-icon.png + #iconHeight: 35 + + # profile-mode + profileMode: + enabled: true # needs to be explicitly set + title: Paul Montag + subtitle: "Software Engineer" + imageUrl: "img/me.jpg" + #imageTitle: my image + buttons: + - name: Posts + url: posts + #- name: Tags + #url: tags + + # home-info mode + homeInfoParams: + Title: "Hi there \U0001F44B" + Content: Welcome to my blog + + # socialIcons: + # - name: x + # url: "https://x.com/" + # - name: stackoverflow + # url: "https://stackoverflow.com" + # - name: github + # url: "https://github.com/" + + # analytics: + # google: + # SiteVerificationTag: "XYZabc" + # bing: + # SiteVerificationTag: "XYZabc" + # yandex: + # SiteVerificationTag: "XYZabc" + + cover: + hidden: true # hide everywhere but not in structured data + hiddenInList: true # hide on list pages and home + hiddenInSingle: true # hide on single page + + editPost: + URL: "https://github.com/d1ngd0/blog/content" + Text: "Suggest Changes" # edit text + appendFilePath: true # to append file path to Edit link + + # for search + # https://fusejs.io/api/options.html + #fuseOpts: + #isCaseSensitive: false + #shouldSort: true + #location: 0 + #distance: 1000 + #threshold: 0.4 + #minMatchCharLength: 0 + #limit: 10 # refer: https://www.fusejs.io/api/methods.html#search + #keys: ["title", "permalink", "summary", "content"] +menu: + main: + - identifier: about + name: about + url: /about/ + weight: 10 + +# Read: https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#using-hugos-syntax-highlighter-chroma +pygmentsUseClasses: true +markup: + highlight: + noClasses: false + # anchorLineNos: true + # codeFences: true + # guessSyntax: true + # lineNos: true + # style: monokai diff --git a/content/archives.md b/content/archives.md new file mode 100644 index 0000000..5628e34 --- /dev/null +++ b/content/archives.md @@ -0,0 +1,6 @@ +--- +title: "Archive" +layout: "archives" +url: "/archives/" +summary: archives +--- diff --git a/content/posts/go_heap_allocation.md b/content/posts/go_heap_allocation.md new file mode 100644 index 0000000..2922526 --- /dev/null +++ b/content/posts/go_heap_allocation.md @@ -0,0 +1,203 @@ ++++ +title = 'Go Heap Allocations' +date = 2024-01-24T21:45:13-06:00 +draft = false ++++ + +Recently I found myself caring about performance. Usually, with go, you don't have to care if something is on the stack or the heap. Since Go is a garbage collected language all those details are taken care of for you. Worring about these things is a pre-optimization, and the community is very happy to tell you so. However, there is the rare event performance takes precidence, in which case the favor of garbage collection turns into a frustrating scavenger hunt. + +Lets write a simple benchmark so we can start looking for allocations. + +main_test.go +```go +type Person struct { + name string + age int +} + +func BenchmarkNew(b *testing.B) { + for i := 0; i < b.N; i++ { + e := Person{ + name: "eric", + age: 12, + } + + _, err := json.Marshal(e) + if err != nil { + b.FailNow() + } + } +} +``` + +We want to run our benchmarks and capture memory statistics. To do this we can use the `-memprofile` flag for `go test`. This will generate a memory profile which will help find where our allocations are occuring. We can also get a count of allocations to the heap during each loop in our benchmark by passing the `-benchmem` flag. + +``` +❯ go test -run='^$' -bench=BenchmarkNew -memprofile=mem.out -benchmem . +goos: linux +goarch: amd64 +pkg: github.com/d1ngd0/go-play +cpu: AMD FX(tm)-8320 Eight-Core Processor +BenchmarkNew-8 3484684 443.3 ns/op 32 B/op 2 allocs/op +PASS +ok github.com/d1ngd0/go-play 1.906s +``` + +So our code has 2 memory allocations per run. We can use the `go tool pprof` command to look through our memory profile and figure out where exactly the allocations are occuring. + +``` +❯ go tool pprof mem.out +File: go-play.test +Type: alloc_space +Time: Jan 24, 2024 at 10:17pm (CST) +Entering interactive mode (type "help" for commands, "o" for options) +(pprof) +``` + +Running the command above will place you in a prompt with commands to help you explore the data. The best place to start is with `top`, which shows you the top bytes allocated. + +``` +(pprof) top +Showing nodes accounting for 138.50MB, 100% of 138.50MB total + flat flat% sum% cum cum% + 101.50MB 73.29% 73.29% 138.50MB 100% github.com/d1ngd0/go-play.BenchmarkNew + 37MB 26.71% 100% 37MB 26.71% encoding/json.Marshal + 0 0% 100% 138.50MB 100% testing.(*B).launch + 0 0% 100% 138.50MB 100% testing.(*B).runN +``` + +We can then view the **exact** location by using the `list` command. + +``` +(pprof) list encoding/json.Marshal +Total: 138.50MB +ROUTINE ======================== encoding/json.Marshal in /usr/local/go/src/encoding/json/encode.go + 37MB 37MB (flat, cum) 26.71% of Total + . . 158:func Marshal(v any) ([]byte, error) { + . . 159: e := newEncodeState() + . . 160: defer encodeStatePool.Put(e) + . . 161: + . . 162: err := e.marshal(v, encOpts{escapeHTML: true}) + . . 163: if err != nil { + . . 164: return nil, err + . . 165: } + 37MB 37MB 166: buf := append([]byte(nil), e.Bytes()...) + . . 167: + . . 168: return buf, nil + . . 169:} + . . 170: + . . 171:// MarshalIndent is like Marshal but applies Indent to format the output. +``` + +Now we see that `buf := append([]byte(nil), e.Bytes()...)` is the line causing the heap allocation. This makes sense, as the code here is effectively copying the bytes from one slice into a new one. + +One of the issues here is we are tracking heap allocations by bytes allocated,not total number of allocations. Each allocation has overhead, as our program has to find a space large enough to hold our value. Disproportionate bytes needed for each allocation to the heap may throw off our numbers, and make a small performance issue look like a huge one. + +For instance lets add a big heap allocation to our benchmark. + +main_test.go +```go +func BenchmarkNew(b *testing.B) { + for i := 0; i < b.N; i++ { + // make a big heap allocation + v := make([]byte, 100000) + // make sure we use v so it doesn't optimise out + _ = v + + e := Person{ + name: "eric", + age: 12, + } + + // cause more heap allocations + for x := 0; x < 1000; x++ { + _, err := json.Marshal(e) + if err != nil { + b.FailNow() + } + } + } +} +``` + +We now see a staggering number of allocations, since we have increased the number of times we run json.Marshal + +``` +❯ go test -run='^$' -bench=BenchmarkNew -memprofile=mem.out -benchmem . +goos: linux +goarch: amd64 +pkg: github.com/d1ngd0/go-play +cpu: AMD FX(tm)-8320 Eight-Core Processor +BenchmarkNew-8 163 8571304 ns/op 138551 B/op 2001 allocs/op +PASS +ok github.com/d1ngd0/go-play 2.138s +``` + +After looking at the allocations in `BenchmarkNew` we see this + +``` +(pprof) list BenchmarkNew +Total: 32.34MB +ROUTINE ======================== github.com/d1ngd0/go-play.BenchmarkNew in /home/paul/Projects/go-play/main_test.go + 30.37MB 32.24MB (flat, cum) 99.69% of Total + . . 13:func BenchmarkNew(b *testing.B) { + . . 14: for i := 0; i < b.N; i++ { + . . 15: // make a big heap allocation + 24.78MB 24.78MB 16: v := make([]byte, 100000) + . . 17: // make sure we use v so it doesn't optimise out + . . 18: _ = v + . . 19: + . . 20: e := Person{ + . . 21: name: "eric", + . . 22: age: 12, + . . 23: } + . . 24: + . . 25: // cause more heap allocations + . . 26: for x := 0; x < 1000; x++ { + 5.58MB 7.46MB 27: _, err := json.Marshal(e) + . . 28: if err != nil { + . . 29: b.FailNow() + . . 30: } + . . 31: } + . . 32: } +``` + +You can see line 16 has a much larger allocation in bytes, but each run should cause a single allocation. We are running 1000 allocations in the for loop in BenchmemNew and another 1000 inside `json.Marshal`. This will have a much larger performance impact. Though the way we are measuring things would make us look at line 16 first. Let's run our test again to get a count of allocations instead. + +First when we run our command we will set the flag `-memprofilerate=1`. This will count **every** allocation that occurs, though it does this at a massive performance cost. Since we only care to count allocations, we don't really care how long it takes to run. + +``` +❯ go test -run='^$' -bench=BenchmarkNew -memprofile=mem.out -memprofilerate=1 -benchmem . +``` + +Now we can run `pprof` again, but this time we will pass in the `-alloc_objects` flag. + +``` +❯ go tool pprof -alloc_objects mem.out +(pprof) list BenchmarkNew +Total: 396519 +ROUTINE ======================== github.com/d1ngd0/go-play.BenchmarkNew in /home/paul/Projects/go-play/main_test.go + 264264 396342 (flat, cum) 100% of Total + . . 13:func BenchmarkNew(b *testing.B) { + . . 14: for i := 0; i < b.N; i++ { + . . 15: // make a big heap allocation + 264 264 16: v := make([]byte, 100000) + . . 17: // make sure we use v so it doesn't optimise out + . . 18: _ = v + . . 19: + . . 20: e := Person{ + . . 21: name: "eric", + . . 22: age: 12, + . . 23: } + . . 24: + . . 25: // cause more heap allocations + . . 26: for x := 0; x < 1000; x++ { + 264000 396078 27: _, err := json.Marshal(e) + . . 28: if err != nil { + . . 29: b.FailNow() + . . 30: } + . . 31: } + . . 32: } +``` + +Now we can see where the allocations are really occuring. diff --git a/hugo.toml b/hugo.toml deleted file mode 100644 index 4196c60..0000000 --- a/hugo.toml +++ /dev/null @@ -1,5 +0,0 @@ -baseURL = 'https://pages.github.com/d1ngd0/blog/' -languageCode = 'en-us' -title = 'Paul Montag Blog' - -theme = [ 'PaperMod' ] diff --git a/static/img/me.jpg b/static/img/me.jpg new file mode 100644 index 0000000..6ba2216 Binary files /dev/null and b/static/img/me.jpg differ