| <!--{ | |
| "Title": "A Guide to the Go Garbage Collector", | |
| "Path": "/doc/gc-guide", | |
| "Breadcrumb": true | |
| }--> | |
| <!-- | |
| NOTE: In this document and others in this directory, the convention is to | |
| set fixed-width phrases with non-fixed-width spaces, as in | |
| <code>hello</code> <code>world</code>. | |
| Do not send CLs removing the interior tags from such phrases. | |
| --> | |
| <style> | |
| .gc-guide-graph { | |
| display: inline-block; | |
| position: relative; | |
| width: 100%; | |
| vertical-align: top; | |
| overflow: hidden; | |
| } | |
| .gc-guide-graph-controls { | |
| display: flex; | |
| flex-direction: row; | |
| flex-wrap: wrap; | |
| justify-content: space-around; | |
| align-items: center; | |
| width: 100%; | |
| } | |
| .gc-guide-graph-controls div { | |
| display: flex; | |
| flex-direction: row; | |
| flex-wrap: nowrap; | |
| align-items: center; | |
| padding-left: 5px; | |
| padding-right: 5px; | |
| height: 24px; | |
| } | |
| .gc-guide-counter { | |
| display: block; | |
| overflow-x: hidden; /* Prevent automatic resizing, which makes the input jittery. */ | |
| width: 10em; /* Never contains more than 10 characters. */ | |
| } | |
| .gc-guide-equation { | |
| display: block; | |
| text-align: center; | |
| } | |
| .gc-guide-note { | |
| margin-left: 3em; | |
| } | |
| </style> | |
| <h2 id="Introduction">Introduction</h2> | |
| <p> | |
| This guide is intended to aid advanced Go users in better understanding their | |
| application costs by providing insights into the Go garbage collector. | |
| It also provides guidance on how Go users may use these insights to improve | |
| their applications' resource utilization. | |
| It does not assume any knowledge of garbage collection, but does assume | |
| familiarity with the Go programming language. | |
| </p> | |
| <p> | |
| The Go language takes responsibility for arranging the storage of Go values; | |
| in most cases, a Go developer need not care about where these values are stored, | |
| or why, if at all. | |
| In practice, however, these values often need to be stored in computer | |
| <b>physical memory</b> and physical memory is a finite resource. | |
| Because it is finite, memory must be managed carefully and recycled in order to | |
| avoid running out of it while executing a Go program. | |
| It's the job of a Go implementation to allocate and recycle memory as needed. | |
| </p> | |
| <p> | |
| Another term for automatically recycling memory is <b>garbage collection</b>. | |
| At a high level, a garbage <i>collector</i> (or GC, for short) is a system that | |
| recycles memory on behalf of the application by identifying which parts of memory | |
| are no longer needed. | |
| The Go standard toolchain provides a runtime library that ships with every | |
| application, and this runtime library includes a garbage collector. | |
| </p> | |
| <p> | |
| Note that the existence of a garbage collector as described by this guide | |
| is not guaranteed by the <a href="/ref/spec">Go specification</a>, only that | |
| the underlying storage for Go values is managed by the language itself. | |
| This omission is intentional and enables the use of radically different | |
| memory management techniques. | |
| </p> | |
| <p> | |
| Therefore, this guide is about a specific implementation of the Go programming | |
| language and <i>may not apply to other implementations</i>. | |
| Specifically, this following guide applies to the standard toolchain (the | |
| <code>gc</code> Go compiler and tools). | |
| Gccgo and Gollvm both use a very similar GC implementation so many of the | |
| same concepts apply, but details may vary. | |
| </p> | |
| <p> | |
| Furthermore, this is a living document and will change over time to best | |
| reflect the latest release of Go. | |
| This document currently describes the garbage collector as of Go 1.19. | |
| </p> | |
| <h3 id="Where_Go_Values_Live">Where Go Values Live</h3> | |
| <p> | |
| Before we dive into the GC, let's first discuss the memory that doesn't need to | |
| be managed by the GC. | |
| </p> | |
| <p> | |
| For instance, non-pointer Go values stored in local variables will likely not be | |
| managed by the Go GC at all, and Go will instead arrange for memory to be | |
| allocated that's tied to the | |
| <a href="/ref/spec#Declarations_and_scope">lexical scope</a> in | |
| which it's created. | |
| In general, this is more efficient than relying on the GC, because the Go | |
| compiler is able to predetermine when that memory may be freed and emit | |
| machine instructions that clean up. | |
| Typically, we refer to allocating memory for Go values this way as | |
| "stack allocation," because the space is stored on the goroutine stack. | |
| </p> | |
| <p> | |
| Go values whose memory cannot be allocated this way, because the Go compiler | |
| cannot determine its lifetime, are said to <i>escape to the heap</i>. | |
| "The heap" can be thought of as a catch-all for memory allocation, for when Go | |
| values need to be placed <i>somewhere</i>. | |
| The act of allocating memory on the heap is typically referred to as "dynamic | |
| memory allocation" because both the compiler and the runtime can make very few | |
| assumptions as to how this memory is used and when it can be cleaned up. | |
| That's where a GC comes in: it's a system that specifically identifies and | |
| cleans up dynamic memory allocations. | |
| </p> | |
| <p> | |
| There are many reasons why a Go value might need to escape to the heap. | |
| One reason could be that its size is dynamically determined. | |
| Consider for instance the backing array of a slice whose initial size is | |
| determined by a variable, rather than a constant. | |
| Note that escaping to the heap must also be transitive: if a reference to a | |
| Go value is written into another Go value that has already been determined to | |
| escape, that value must also escape. | |
| </p> | |
| <p> | |
| Whether a Go value escapes or not is a function of the context in which it is | |
| used and the Go compiler's escape analysis algorithm. | |
| It would be fragile and difficult to try to enumerate precisely when values | |
| escape: the algorithm itself is fairly sophisticated and changes between Go | |
| releases. | |
| For more details on how to identify which values escape and which do not, see | |
| the section on | |
| <a href="#Eliminating_heap_allocations">eliminating heap allocations</a>. | |
| </p> | |
| <h3 id="Tracing_Garbage_Collection">Tracing Garbage Collection</h3> | |
| <p> | |
| Garbage collection may refer to many different methods of automatically | |
| recycling memory; for example, reference counting. | |
| In the context of this document, garbage collection refers to <b>tracing</b> | |
| garbage collection, which identifies in-use, so-called <b>live</b>, objects by | |
| following pointers transitively. | |
| </p> | |
| <p> | |
| Let's define these terms more rigorously. | |
| </p> | |
| <ul> | |
| <li> | |
| <p> | |
| <b>Object</b>—An object is a dynamically allocated piece of memory | |
| that contains one or more Go values. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b>Pointer</b>—A memory address that references any value within an | |
| object. | |
| This naturally includes Go values of the form <code>*T</code>, but also includes | |
| parts of built-in Go values. | |
| Strings, slices, channels, maps, and interface values all contain memory | |
| addresses that the GC must trace. | |
| </p> | |
| </li> | |
| </ul> | |
| <p> | |
| Together, objects and pointers to other objects form the <b>object graph</b>. | |
| To identify live memory, the GC walks the object graph starting at the | |
| program's <b>roots</b>, pointers that identify objects that are definitely | |
| in-use by the program. | |
| Two examples of roots are local variables and global variables. | |
| The process of walking the object graph is referred to as <b>scanning</b>. | |
| Another phrase you might see in the Go documentation is whether an object is | |
| <b>reachable</b>, which just means that the object can be discovered by the | |
| scanning process. | |
| Note also that, <a href="#Finalizers_cleanups_and_weak_pointers">with one | |
| exception</a>, once memory becomes unreachable, it stays unreachable. | |
| </p> | |
| <p> | |
| This basic algorithm is common to all tracing GCs. | |
| Where tracing GCs differ is what they do once they discover memory is live. | |
| Go's GC uses the mark-sweep technique, which means that in order to keep track | |
| of its progress, the GC also <b>marks</b> the values it encounters as live. | |
| Once tracing is complete, the GC then walks over all memory in the heap and | |
| makes all memory that is <i>not</i> marked available for allocation. | |
| This process is called <b>sweeping</b>. | |
| </p> | |
| <p> | |
| One alternative technique you may be familiar with is to actually <i>move</i> | |
| the objects to a new part of memory and leave behind a forwarding pointer that | |
| is later used to update all the application's pointers. | |
| We call a GC that moves objects in this way a <b>moving</b> GC; Go has a | |
| <b>non-moving</b> GC. | |
| </p> | |
| <h2 id="The_GC_cycle">The GC cycle</h2> | |
| <p> | |
| Because the Go GC is a mark-sweep GC, it broadly operates in two phases: the | |
| mark phase, and the sweep phase. | |
| While this statement might seem tautological, it contains an important insight: | |
| it's not possible to release memory back to be allocated until <i>all</i> memory | |
| has been traced, because there may still be an un-scanned pointer keeping | |
| an object alive. | |
| As a result, the act of sweeping must be entirely separated from the act of | |
| marking. | |
| Furthermore, the GC may also not be active at all, when there's no GC-related | |
| work to do. | |
| The GC continuously rotates through these three phases of sweeping, off, and | |
| marking in what's known as the <b>GC cycle</b>. | |
| For the purposes of this document, consider the GC cycle starting with sweeping, | |
| turning off, then marking. | |
| </p> | |
| <p> | |
| The next few sections will focus on building intuition for the costs of the | |
| GC to aid users in tweaking GC parameters for their own benefit. | |
| </p> | |
| <h3 id="Understanding_costs">Understanding costs</h3> | |
| <p> | |
| The GC is inherently a complex piece of software built on even more complex | |
| systems. | |
| It's easy to become mired in detail when trying to understand the GC and | |
| tweak its behavior. | |
| This section is intended to provide a framework for reasoning about the cost | |
| of the Go GC and its tuning parameters. | |
| </p> | |
| <p> | |
| To begin with, consider this model of GC cost based on three simple axioms. | |
| </p> | |
| <ol> | |
| <li> | |
| <p> | |
| The GC involves only two resources: physical memory, and CPU time. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| The GC's memory costs consist of live heap memory, new heap memory | |
| allocated before the mark phase, and space for metadata that, even | |
| if proportional to the previous costs, are small in comparison. | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| GC memory cost for cycle N = live heap from cycle N-1 + new heap | |
| </i> | |
| </p> | |
| <p> | |
| Live heap memory is memory that was determined to be live by the | |
| previous GC cycle, while new heap memory is any memory allocated in the | |
| current cycle, which may or may not be live by the end. | |
| How much memory is live at any given point in time is a property of the | |
| program, and not something the GC can directly control. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| The GC's CPU costs are modeled as a fixed cost per cycle, and a | |
| marginal cost that scales proportionally with the size of the live | |
| heap. | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| GC CPU time for cycle N = Fixed CPU time cost per cycle + average CPU time cost per byte * live heap memory found in cycle N | |
| </i> | |
| </p> | |
| <p> | |
| The fixed CPU time cost per cycle includes things that happen a constant number | |
| of times each cycle, like initializing data structures for the next GC cycle. | |
| This cost is typically small, and is included just for completeness. | |
| </p> | |
| <p> | |
| Most of the CPU cost of the GC is marking and scanning, which is captured by | |
| the marginal cost. | |
| The average cost of marking and scanning depends on the GC implementation, but | |
| also on the behavior of the program. | |
| For example, more pointers means more GC work, because at minimum the GC needs | |
| to visit all the pointers in the program. | |
| Structures like linked lists and trees are also more difficult for the GC to | |
| walk in parallel, increasing the average cost per byte. | |
| </p> | |
| <p> | |
| This model ignores sweeping costs, which are proportional to total heap memory, | |
| including memory that is dead (it must be made available for allocation). | |
| For Go's current GC implementation, sweeping is so much faster than marking and | |
| scanning that the cost is negligible in comparison. | |
| </p> | |
| </li> | |
| </ol> | |
| <p> | |
| This model is simple but effective: it accurately categorizes the dominant | |
| costs of the GC. | |
| It also tells us that the <i>total CPU cost</i> of the garbage collector depends on | |
| the total number of GC cycles in a given time frame. | |
| Finally, embedded in this model is a fundamental time/space trade-off for the GC. | |
| </p> | |
| <p> | |
| To see why, let's explore a constrained but useful scenario: the | |
| <b>steady state</b>. | |
| The steady state of an application, from the GC's perspective, is defined by the | |
| following properties: | |
| </p> | |
| <ul> | |
| <li> | |
| <p> | |
| The rate at which the application allocates new memory (in bytes per | |
| second) is constant. | |
| </p> | |
| <p> | |
| This means that, from the GC's perspective, the application's workload | |
| looks approximately the same over time. | |
| For example, for a web service, this would be a constant request rate | |
| with, on average, the same kinds of requests being made, with the average | |
| lifetime of each request staying roughly constant. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| The marginal costs of the GC are constant. | |
| </p> | |
| <p> | |
| This means that statistics of the object graph, such as the distribution | |
| of object sizes, the number of pointers, and the average depth of data | |
| structures, remain the same from cycle to cycle. | |
| </p> | |
| </li> | |
| </ul> | |
| <p> | |
| Let's work through an example. | |
| Assume some application is operating in a steady-state, allocating 10 MiB/s, | |
| while the GC can scan memory at a rate of 100 MiB/cpu-second (this is made up). | |
| The steady state makes no assumptions about the size of the live heap, | |
| but for simplicity, let's say this application's live heap is always 10 MiB. | |
| Let's also assume, again, for simplicity, that the fixed GC costs are zero. | |
| Let's play around with the GC cycle period. | |
| </p> | |
| <p> | |
| Suppose each GC cycle happens after exactly 1 cpu-second. | |
| Then, by the end of each GC cycle our example application will have allocated | |
| 10 MiB of additional memory, resulting in a 20 MiB total heap size. | |
| And with every GC cycle, the GC will spend 0.1 cpu-seconds scanning the | |
| 10 MiB live heap, resulting in a 10% CPU overhead. | |
| Recall that the GC only needs to walk the live heap, not the whole heap. | |
| (Note: a constant live heap does not mean that all newly allocated memory is | |
| dead. | |
| It means that, after the GC runs, <i>some mix</i> of old and new heap memory | |
| dies, and only that the end result is 10 MiB found live each cycle.) | |
| </p> | |
| <p> | |
| Now suppose each GC cycle happens less often, once every 2 cpu-seconds. | |
| Then, our example application, in the steady state, will have a 30 MiB total | |
| heap size on each GC cycle, since it'll allocate 20 MiB in that time. | |
| But with every GC cycle, the GC will <i>still only need 0.1 cpu-seconds</i> | |
| to scan the 10 MiB of live memory. | |
| Again, we're assuming the live heap size stays the same, regardless of how | |
| much memory is allocated. | |
| So this means that our GC overhead just went down, from 10% to 5%, at the | |
| cost of 50% more memory being used. | |
| </p> | |
| <p> | |
| This change in overheads is the fundamental time/space trade-off mentioned | |
| earlier. | |
| And <b>GC frequency</b> is at the center of this trade-off: | |
| if we execute the GC more frequently, then we use less memory, and vice versa. | |
| But how often does the GC actually execute? | |
| In Go, deciding when the GC should start is the main parameter which the user | |
| has control over. | |
| </p> | |
| <h3 id="GOGC">GOGC</h3> | |
| <p> | |
| At a high level, GOGC determines the trade-off between GC CPU and memory. | |
| </p> | |
| <p> | |
| It works by determining the target heap size after each GC cycle, a target value | |
| for the total heap size in the next cycle. | |
| The GC's goal is to finish a collection cycle before the total heap size | |
| exceeds the target heap size. | |
| Total heap size is defined as the live heap size at the end of the previous | |
| cycle, plus any new heap memory allocated by the application since the previous | |
| cycle. | |
| Meanwhile, target heap memory is defined as: | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| Target heap memory = Live heap + (Live heap + GC roots) * GOGC / 100 | |
| </i> | |
| </p> | |
| <p> | |
| As an example, consider a Go program with a live heap size of 8 MiB, 1 MiB | |
| of goroutine stacks, and 1 MiB of pointers in global variables. | |
| Then, with a GOGC value of 100, the amount of new memory that will be allocated | |
| before the next GC runs will be 10 MiB, or 100% of the 10 MiB of work, for a | |
| total heap footprint of 18 MiB. | |
| With a GOGC value of 50, then it'll be 50%, or 5 MiB. | |
| With a GOGC value of 200, it'll be 200%, or 20 MiB. | |
| </p> | |
| <p class="gc-guide-note"> | |
| Note: GOGC includes the root set only as of Go 1.18. | |
| Previously, it would only count the live heap. | |
| Often, the amount of memory in goroutine stacks is quite small and the live | |
| heap size dominates all other sources of GC work, but in cases where programs | |
| had hundreds of thousands of goroutines, the GC was making poor judgements. | |
| </p> | |
| <p> | |
| The heap target controls GC frequency: the bigger the target, the longer the GC | |
| can wait to start another mark phase and vice versa. | |
| While the precise formula is useful for making estimates, it's best to think of | |
| GOGC in terms of its fundamental purpose: a parameter that picks a point in the | |
| GC CPU and memory trade-off. | |
| The key takeaway is that <b>doubling GOGC will double heap memory overheads and | |
| roughly halve GC CPU cost</b>, and vice versa. | |
| (To see a full explanation as to why, see the | |
| <a href="#Additional_notes_on_GOGC">appendix</a>.) | |
| </p> | |
| <p class="gc-guide-note"> | |
| Note: the target heap size is just a target, and there are several reasons why | |
| the GC cycle might not finish right at that target. | |
| For one, a large enough heap allocation can simply exceed the target. | |
| However, other reasons appear in GC implementations that go beyond the | |
| <a href="#Understanding_costs">GC model</a> this guide has been using thus far. | |
| For some more detail, see the <a href="#Latency">latency section</a>, but the | |
| complete details may be found in the <a href="#Additional_resources">additional | |
| resources</a>. | |
| </p> | |
| <p> | |
| GOGC may be configured through either the <code>GOGC</code> environment | |
| variable (which all Go programs recognize), or through the | |
| <a href="https://pkg.go.dev/runtime/debug#SetGCPercent"><code>SetGCPercent</code></a> | |
| API in the <code>runtime/debug</code> package. | |
| </p> | |
| <p> | |
| Note that GOGC may also be used to turn off the GC entirely (provided the | |
| <a href="#Memory_limit">memory limit</a> does not apply) by setting | |
| <code>GOGC=off</code> or calling <code>SetGCPercent(-1)</code>. | |
| Conceptually, this setting is equivalent to setting GOGC to a value of | |
| infinity, as the amount of new memory before a GC is triggered is unbounded. | |
| </p> | |
| <p> | |
| To better understand everything we've discussed so far, try out the interactive | |
| visualization below that is built on the | |
| <a href="#Understanding_costs">GC cost model</a> discussed earlier. | |
| This visualization depicts the execution of some program whose non-GC work takes | |
| 10 seconds of CPU time to complete. | |
| In the first second it performs some initialization step (growing its live heap) | |
| before settling into a steady state. | |
| The application allocates 200 MiB in total, with 20 MiB live at a time. | |
| It assumes that the only relevant GC work to complete comes from the live heap, | |
| and that (unrealistically) the application uses no additional memory. | |
| </p> | |
| <p> | |
| Use the slider to adjust the value of GOGC to see how the application responds | |
| in terms of total duration and GC overhead. | |
| Each GC cycle ends while the new heap drops to zero. | |
| The time taken while the new heap drops to zero is the combined time for the | |
| mark phase for cycle N, and the sweep phase for the cycle N+1. | |
| Note that this visualization (and all the visualizations in this guide) assume | |
| the application is paused while the GC executes, so GC CPU costs are fully | |
| represented by the time it takes for new heap memory to drop to zero. | |
| This is only to make visualization simpler; the same intuition still applies. | |
| The X axis shifts to always show the full CPU-time duration of the program. | |
| Notice that additional CPU time used by the GC increases the overall duration. | |
| </p> | |
| <div class="gc-guide-graph" data-workload='[ | |
| {"duration": 1.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 1.00, "oldDeathRate": 0.00}, | |
| {"duration": 9.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.00} | |
| ]' data-config='{ | |
| "fixedCost": 0.04, | |
| "otherMem": 0, | |
| "GOGC": "graph1-gogc", | |
| "memoryLimit": 100000 | |
| }'></div> | |
| <div class="gc-guide-graph-controls"> | |
| <div> | |
| GOGC | |
| <input type="range" min="0" max="10" step="0.005" value="6.64" id="graph1-gogc"> | |
| <div class="gc-guide-counter" id="graph1-gogc-display"></div> | |
| </div> | |
| </div> | |
| <p> | |
| Notice that the GC always incurs some CPU and peak memory overhead. | |
| As GOGC increases, CPU overhead decreases, but peak memory increases | |
| proportionally to the live heap size. | |
| As GOGC decreases, the peak memory requirement decreases at the expense of | |
| additional CPU overhead. | |
| </p> | |
| <p class="gc-guide-note"> | |
| Note: the graph displays CPU time, not wall-clock time to complete the program. | |
| If the program runs on 1 CPU and fully utilizes its resources, then these are | |
| equivalent. | |
| A real-world program likely runs on a multi-core system and does not 100% | |
| utilize the CPUs at all times. | |
| In these cases the wall-time impact of the GC will be lower. | |
| </p> | |
| <p class="gc-guide-note"> | |
| Note: the Go GC has a minimum total heap size of 4 MiB, so if the GOGC-set | |
| target is ever below that, it gets rounded up. | |
| The visualization reflects this detail. | |
| </p> | |
| <p> | |
| Here's another example that's a little bit more dynamic and realistic. | |
| Once again, the application takes 10 CPU-seconds to complete without the GC, but | |
| the steady state allocation rate increases dramatically half-way through, and | |
| the live heap size shifts around a bit in the first phase. | |
| This example demonstrates how the steady state might look when the live heap | |
| size is actually changing, and how a higher allocation rate leads to more | |
| frequent GC cycles. | |
| </p> | |
| <div class="gc-guide-graph" data-workload='[ | |
| {"duration": 1.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 1.00, "oldDeathRate": 0.00}, | |
| {"duration": 1.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.50}, | |
| {"duration": 1.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.50, "oldDeathRate": 0.00}, | |
| {"duration": 1.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.50}, | |
| {"duration": 1.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.50, "oldDeathRate": 0.00}, | |
| {"duration": 5.0, "allocRate": 200, "scanRate": 1024, "newSurvivalRate": 0.02, "oldDeathRate": 1.00} | |
| ]' data-config='{ | |
| "fixedCost": 0.04, | |
| "otherMem": 0, | |
| "GOGC": "graph2-gogc", | |
| "memoryLimit": 100000 | |
| }'></div> | |
| <div class="gc-guide-graph-controls"> | |
| <div> | |
| GOGC | |
| <input type="range" min="0" max="10" step="0.005" value="6.64" id="graph2-gogc"> | |
| <div class="gc-guide-counter" id="graph2-gogc-display"></div> | |
| </div> | |
| </div> | |
| <h3 id="Memory_limit">Memory limit</h3> | |
| <p> | |
| Until Go 1.19, GOGC was the sole parameter that could be used to modify the GC's | |
| behavior. | |
| While it works great as a way to set a trade-off, it doesn't take into account | |
| that available memory is finite. | |
| Consider what happens when there's a transient spike in the live heap size: | |
| because the GC will pick a total heap size proportional to that live heap size, | |
| GOGC must be configured such for the <i>peak</i> live heap size, even if in the | |
| usual case a higher GOGC value provides a better trade-off. | |
| </p> | |
| <p> | |
| The visualization below demonstrates this transient heap spike situation. | |
| </p> | |
| <div class="gc-guide-graph" data-workload='[ | |
| {"duration": 1.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 1.00, "oldDeathRate": 0.00}, | |
| {"duration": 4.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.00}, | |
| {"duration": 0.5, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 1.00, "oldDeathRate": 0.00}, | |
| {"duration": 0.5, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.00}, | |
| {"duration": 0.5, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.50}, | |
| {"duration": 3.5, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.00} | |
| ]' data-config='{ | |
| "fixedCost": 0.04, | |
| "otherMem": 0, | |
| "GOGC": "graph3-gogc", | |
| "memoryLimit": 100000 | |
| }'></div> | |
| <div class="gc-guide-graph-controls"> | |
| <div> | |
| GOGC | |
| <input type="range" min="0" max="10" step="0.005" value="6.64" id="graph3-gogc"> | |
| <div class="gc-guide-counter" id="graph3-gogc-display"></div> | |
| </div> | |
| </div> | |
| <p> | |
| If the example workload is running in a container with a bit over 60 MiB of | |
| memory available, then GOGC can't be increased beyond 100, even though the rest | |
| of the GC cycles have the available memory to make use of that extra memory. | |
| Furthermore, in some applications, these transient peaks can be rare and hard to | |
| predict, leading to occasional, unavoidable, and potentially costly | |
| out-of-memory conditions. | |
| </p> | |
| <p> | |
| That's why in the 1.19 release, Go added support for setting a runtime memory | |
| limit. | |
| The memory limit may be configured either via the <code>GOMEMLIMIT</code> | |
| environment variable which all Go programs recognize, or through the | |
| <code>SetMemoryLimit</code> function available in the <code>runtime/debug</code> | |
| package. | |
| </p> | |
| <p> | |
| This memory limit sets a maximum on the <i>total amount of memory that the Go | |
| runtime can use</i>. | |
| The specific set of memory included is defined in terms of | |
| <a href="https://pkg.go.dev/runtime#MemStats"><code>runtime.MemStats</code></a> | |
| as the expression | |
| </p> | |
| <p> | |
| <code>Sys</code> <code>-</code> <code>HeapReleased</code> | |
| </p> | |
| <p> | |
| or equivalently in terms of the | |
| <a href="https://pkg.go.dev/runtime/metrics"><code>runtime/metrics</code></a> | |
| package, | |
| </p> | |
| <p> | |
| <code>/memory/classes/total:bytes</code> <code>-</code> <code>/memory/classes/heap/released:bytes</code> | |
| </p> | |
| <p> | |
| Because the Go GC has explicit control over how much heap memory it uses, it | |
| sets the total heap size based on this memory limit and how much other memory | |
| the Go runtime uses. | |
| </p> | |
| <p> | |
| The visualization below depicts the same single-phase steady state workload from | |
| the GOGC section, but this time with an extra 10 MiB of overhead from the Go | |
| runtime and with an adjustable memory limit. | |
| Try shifting around both GOGC and the memory limit and see what happens. | |
| </p> | |
| <div class="gc-guide-graph" data-workload='[ | |
| {"duration": 1.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 1.00, "oldDeathRate": 0}, | |
| {"duration": 9.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0} | |
| ]' data-config='{ | |
| "fixedCost": 0.04, | |
| "otherMem": 10, | |
| "GOGC": "graph4-gogc", | |
| "memoryLimit": "graph4-memlimit" | |
| }'></div> | |
| <div class="gc-guide-graph-controls"> | |
| <div> | |
| GOGC | |
| <input type="range" min="0" max="10" step="0.005" value="6.64" id="graph4-gogc"> | |
| <div class="gc-guide-counter" id="graph4-gogc-display"></div> | |
| </div> | |
| <div> | |
| Memory Limit | |
| <input type="range" min="1" max="100" step="0.5" value="100" id="graph4-memlimit"> | |
| <div class="gc-guide-counter" id="graph4-memlimit-display"></div> | |
| </div> | |
| </div> | |
| <p> | |
| Notice that when the memory limit is lowered below the peak memory that's | |
| determined by GOGC (42 MiB for a GOGC of 100), the GC runs more frequently to | |
| keep the peak memory within the limit. | |
| </p> | |
| <p> | |
| Returning to our previous example of the transient heap spike, by setting a | |
| memory limit and turning up GOGC, we can get the best of both worlds: no memory | |
| limit breach, and better resource economy. | |
| Try out the interactive visualization below. | |
| </p> | |
| <div class="gc-guide-graph" data-workload='[ | |
| {"duration": 1.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 1.00, "oldDeathRate": 0.00}, | |
| {"duration": 4.0, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.00}, | |
| {"duration": 0.5, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 1.00, "oldDeathRate": 0.00}, | |
| {"duration": 0.5, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.00}, | |
| {"duration": 0.5, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.50}, | |
| {"duration": 3.5, "allocRate": 20, "scanRate": 1024, "newSurvivalRate": 0.00, "oldDeathRate": 0.00} | |
| ]' data-config='{ | |
| "fixedCost": 0.04, | |
| "otherMem": 0, | |
| "GOGC": "graph5-gogc", | |
| "memoryLimit": "graph5-memlimit" | |
| }'></div> | |
| <div class="gc-guide-graph-controls"> | |
| <div> | |
| GOGC | |
| <input type="range" min="0" max="10" step="0.005" value="6.64" id="graph5-gogc"> | |
| <div class="gc-guide-counter" id="graph5-gogc-display"></div> | |
| </div> | |
| <div> | |
| Memory Limit | |
| <input type="range" min="1" max="100" step="0.5" value="100" id="graph5-memlimit"> | |
| <div class="gc-guide-counter" id="graph5-memlimit-display"></div> | |
| </div> | |
| </div> | |
| <p> | |
| Notice that with some values of GOGC and the memory limit, peak memory use | |
| stops at whatever the memory limit is, but that the rest of the program's | |
| execution still obeys the total heap size rule set by GOGC. | |
| </p> | |
| <p> | |
| This observation leads to another interesting detail: even when GOGC is set to | |
| off, the memory limit is still respected! | |
| In fact, this particular configuration represents a <i>maximization of resource | |
| economy</i> because it sets the minimum GC frequency required to maintain some | |
| memory limit. | |
| In this case, <i>all</i> of the program's execution has the heap size rise to | |
| meet the memory limit. | |
| </p> | |
| <p> | |
| Now, while the memory limit is clearly a powerful tool, <b>the use of | |
| a memory limit does not come without a cost</b>, and certainly doesn't | |
| invalidate the utility of GOGC. | |
| </p> | |
| <p> | |
| Consider what happens when the live heap grows large enough to bring total | |
| memory use close to the memory limit. | |
| In the steady state visualization above, try turning GOGC off and then slowly | |
| lowering the memory limit further and further to see what happens. | |
| Notice that the total time the application takes will start to grow in an | |
| unbounded manner as the GC is constantly executing to maintain an impossible | |
| memory limit. | |
| </p> | |
| <p> | |
| This situation, where the program fails to make reasonable progress due to | |
| constant GC cycles, is called <b>thrashing</b>. | |
| It's particularly dangerous because it effectively stalls the program. | |
| Even worse, it can happen for exactly the same situation we were trying to | |
| avoid with GOGC: a large enough transient heap spike can cause a program to | |
| stall indefinitely! | |
| Try reducing the memory limit (around 30 MiB or lower) in the transient heap | |
| spike visualization and notice how the worst behavior specifically starts with | |
| the heap spike. | |
| </p> | |
| <p> | |
| In many cases, an indefinite stall is worse than an out-of-memory condition, | |
| which tends to result in a much faster failure. | |
| </p> | |
| <p> | |
| For this reason, the memory limit is defined to be <b>soft</b>. | |
| The Go runtime makes no guarantees that it will maintain this memory limit | |
| under all circumstances; it only promises some reasonable amount of effort. | |
| This relaxation of the memory limit is critical to avoiding thrashing behavior, | |
| because it gives the GC a way out: let memory use surpass the limit to avoid | |
| spending too much time in the GC. | |
| </p> | |
| <p> | |
| How this works internally is the GC sets an upper limit on the amount | |
| of CPU time it can use over some time window (with some hysteresis for very | |
| short transient spikes in CPU use). | |
| This limit is currently set at roughly 50%, with a <code>2 * GOMAXPROCS</code> | |
| CPU-second window. | |
| The consequence of limiting GC CPU time is that the GC's work is delayed, | |
| meanwhile the Go program may continue allocating new heap memory, even beyond | |
| the memory limit. | |
| </p> | |
| <p> | |
| The intuition behind the 50% GC CPU limit is based on the worst-case impact | |
| on a program with ample available memory. | |
| In the case of a misconfiguration of the memory limit, where it is set too | |
| low mistakenly, the program will slow down at most by 2x, because the GC | |
| can't take more than 50% of its CPU time away. | |
| </p> | |
| <p class="gc-guide-note"> | |
| Note: the visualizations on this page do not simulate the GC CPU limit. | |
| </p> | |
| <h4 id="Suggested_uses">Suggested uses</h4> | |
| <p> | |
| While the memory limit is a powerful tool, and the Go runtime takes steps to | |
| mitigate the worst behaviors from misuse, it's still important to use it | |
| thoughtfully. | |
| Below is a collection of tidbits of advice about where the memory limit is most | |
| useful and applicable, and where it might cause more harm than good. | |
| </p> | |
| <ul> | |
| <li> | |
| <p> | |
| <b>Do</b> take advantage of the memory limit when the execution | |
| environment of your Go program is entirely within your control, and | |
| the Go program is the only program with access to some set of resources | |
| (i.e. some kind of memory reservation, like a container memory limit). | |
| </p> | |
| <p> | |
| A good example is the deployment of a web service into containers with | |
| a fixed amount of available memory. | |
| </p> | |
| <p> | |
| <b>In this case, a good rule of thumb is to leave an additional 5-10% | |
| of headroom to account for memory sources the Go runtime is unaware of. | |
| </b> | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b>Do</b> feel free to adjust the memory limit in real time to adapt to | |
| changing conditions. | |
| </p> | |
| <p> | |
| A good example is a cgo program where C libraries temporarily need to | |
| use substantially more memory. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b>Don't</b> set GOGC to off with a memory limit if the Go program | |
| might share some of its limited memory with other programs, and those | |
| programs are generally decoupled from the Go program. | |
| Instead, keep the memory limit since it may help to curb undesirable | |
| transient behavior, but set GOGC to some smaller, reasonable value for | |
| the average case. | |
| </p> | |
| <p> | |
| While it may be tempting to try and "reserve" memory for co-tenant | |
| programs, unless the programs are fully synchronized (e.g. the Go | |
| program calls some subprocess and blocks while its callee executes), | |
| the result will be less reliable as inevitably both programs will | |
| need more memory. | |
| Letting the Go program use less memory when it doesn't need it will | |
| generate a more reliable result overall. | |
| This advice also applies to overcommit situations, where the sum of | |
| memory limits of containers running on one machine may exceed the | |
| actual physical memory available to the machine. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b>Don't</b> use the memory limit when deploying to an execution | |
| environment you don't control, especially when your program's memory | |
| use is proportional to its inputs. | |
| </p> | |
| <p> | |
| A good example is a CLI tool or a desktop application. | |
| Baking a memory limit into the program when it's unclear what kind of | |
| inputs it might be fed, or how much memory might be available on the | |
| system can lead to confusing crashes and poor performance. | |
| Plus, an advanced end-user can always set a memory limit if they wish. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b>Don't</b> set a memory limit to avoid out-of-memory conditions when | |
| a program is already close to its environment's memory limits. | |
| </p> | |
| <p> | |
| This effectively replaces an out-of-memory risk with a risk of | |
| severe application slowdown, which is often not a favorable trade, | |
| even with the efforts Go makes to mitigate thrashing. | |
| In such a case, it would be much more effective to either increase the | |
| environment's memory limits (and <i>then</i> potentially set a memory | |
| limit) or decrease GOGC (which provides a much cleaner trade-off than | |
| thrashing-mitigation does). | |
| </p> | |
| </li> | |
| </ul> | |
| <h3 id="Latency">Latency</h3> | |
| <p> | |
| The visualizations in this document have modeled the application as paused while | |
| the GC is executing. | |
| GC implementations do exist that behave this way, and they're referred to as | |
| "stop-the-world" GCs. | |
| </p> | |
| <p> | |
| The Go GC, however, is not fully stop-the-world and does most of its work | |
| concurrently with the application. | |
| This is primarily to reduce application <i>latencies</i>. | |
| Specifically, the end-to-end duration of a single unit of computation (e.g. a | |
| web request). | |
| Thus far, this document mainly considered application <i>throughput</i> (e.g. | |
| web requests handled per second). | |
| Note that each example in the <a href="#The_GC_cycle">GC cycle</a> section | |
| focused on the total CPU duration of an executing program. | |
| However, such a duration is far less meaningful for say, a web service. | |
| While throughput is still important for a web service (i.e. queries per second), | |
| often the latency of each individual request matters even more. | |
| </p> | |
| <p> | |
| In terms of latency, a stop-the-world GC may require a considerable length of | |
| time to execute both its mark and sweep phases, during which the application, | |
| and in the context of a web service, any in-flight request, is unable to make | |
| further progress. | |
| Instead, the Go GC avoids making the length of any global application pauses | |
| proportional to the size of the heap, and that the core tracing algorithm is | |
| performed while the application is actively executing. | |
| (The pauses are more strongly proportional to GOMAXPROCS algorithmically, but | |
| most commonly are dominated by the time it takes to stop running goroutines.) | |
| Collecting concurrently is not without cost: in practice it often leads to a | |
| design with lower throughput than an equivalent stop-the-world garbage | |
| collector. | |
| However, it's important to note that <i>lower latency does not inherently mean | |
| lower throughput</i>, and the performance of the Go garbage collector has | |
| steadily improved over time, in both latency and throughput. | |
| </p> | |
| <p> | |
| The concurrent nature of Go's current GC does not invalidate anything discussed | |
| in this document so far: none of the statements relied on this design choice. | |
| GC frequency is still the primary way the GC trades off between CPU | |
| time and memory for throughput, and in fact, it also takes on this role for | |
| latency. | |
| This is because most of the costs for the GC are incurred while the mark phase | |
| is active. | |
| </p> | |
| <p> | |
| The key takeaway then, is that <b>reducing GC frequency may also lead to latency | |
| improvements</b>. | |
| This applies not only to reductions in GC frequency from modifying tuning | |
| parameters, like increasing GOGC and/or the memory limit, but also applies to | |
| the optimizations described in the | |
| <a href="#Optimization_guide">optimization guide</a>. | |
| </p> | |
| <p> | |
| However, latency is often more complex to understand than throughput, because it | |
| is a product of the moment-to-moment execution of the program and not just an | |
| aggregation of costs. | |
| As a result, the connection between latency and GC frequency is less direct. | |
| Below is a list of possible sources of latency for those inclined to dig | |
| deeper. | |
| </p> | |
| <ol> | |
| <li> | |
| Brief stop-the-world pauses when the GC transitions between the mark | |
| and sweep phases, | |
| </li> | |
| <li> | |
| Scheduling delays because the GC takes 25% of CPU resources when in the | |
| mark phase, | |
| </li> | |
| <li> | |
| User goroutines assisting the GC in response to a high allocation rate, | |
| </li> | |
| <li> | |
| Pointer writes requiring additional work while the GC is in the mark | |
| phase, and | |
| </li> | |
| <li> | |
| Running goroutines must be suspended for their roots to be scanned. | |
| </li> | |
| </ol> | |
| <p> | |
| These latency sources are visible in | |
| <a href="/doc/diagnostics#execution-tracer">execution traces</a>, except for | |
| pointer writes requiring additional work. | |
| </p> | |
| <h3 id="Finalizers_cleanups_and_weak_pointers">Finalizers, cleanups, and weak pointers</h2> | |
| <p> | |
| Garbage collection provides the illusion of infinite memory using only finite | |
| memory. | |
| Memory is allocated but never explicitly freed, which enables simpler APIs and | |
| concurrent algorithms compared to bare-bones manual memory management. | |
| (Some languages with manually managed memory use alternative approaches such | |
| as "smart pointers" and compile-time ownership tracking to ensure that objects | |
| are freed, but these features are deeply embedded into the API design | |
| conventions in these languages.) | |
| </p> | |
| <p> | |
| Only the live objects—those reachable from a global variable or a | |
| computation in some goroutine—can affect the behavior of the program. | |
| Any time after an object becomes unreachable ("dead"), it may be safely | |
| recycled by the GC. | |
| This allows for a wide variety of GC designs, such as the tracing design used | |
| by Go today. | |
| The death of an object is not an observable event at the language level. | |
| </p> | |
| <p> | |
| However, Go's runtime library provides three features that break that illusion: | |
| <a href="/pkg/runtime#AddCleanup">cleanups</a>, | |
| <a href="/pkg/weak#Pointer">weak pointers</a>, and | |
| <a href="/pkg/runtime#SetFinalizer">finalizers</a>. | |
| Each of these features provides some way to observe and react to object death, | |
| and in the case of finalizers, even reverse it. | |
| This of course complicates Go programs and adds an additional burden to the GC | |
| implementation. | |
| Nonetheless, these features exist because they are useful in a variety of | |
| circumstances, and Go programs use them and benefit from them all the time. | |
| </p> | |
| <p> | |
| For the details of each feature, refer to its package documentation | |
| (<a href="/pkg/runtime#AddCleanup">runtime.AddCleanup</a>, | |
| <a href="/pkg/weak#Pointer">weak.Pointer</a>, | |
| <a href="/pkg/runtime#SetFinalizer">runtime.SetFinalizer</a>). | |
| Below is some general advice for using these features, outlines of common | |
| issues you can run into with each feature, and advice for testing uses of these | |
| features. | |
| </p> | |
| <h4 id="General_advice">General advice</h4> | |
| <ul> | |
| <li> | |
| <p> | |
| <b> | |
| Write unit tests. | |
| </b> | |
| </p> | |
| <p> | |
| The exact timing of cleanups, weak pointers, and finalizers can be difficult | |
| to predict, and it's easy to convince yourself that everything works, even | |
| after many consecutive executions. | |
| But it's also easy to make subtle mistakes. | |
| <a href="#Testing_object_death">Writing tests</a> for them can be tricky, but | |
| given that they're so subtle to use, testing is even more important usual. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b> | |
| Avoid using these features directly in typical Go code. | |
| </b> | |
| </p> | |
| <p> | |
| These are low-level features with subtle restrictions and behaviors. | |
| For instance, there's no guarantee cleanups or finalizers will be run | |
| at program exit, or at all for that matter. | |
| The long comments in their API documentation should be seen as a warning. | |
| The vast majority of Go code does not benefit from using these features | |
| directly, only indirectly. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b> | |
| Encapsulate the use of these mechanisms within a package. | |
| </b> | |
| </p> | |
| <p> | |
| Where possible, do not allow the use of these mechanisms to leak into | |
| the public API of your package; provide interfaces that make it hard or | |
| impossible for users to misuse them. | |
| For example, instead of asking the user to set up a cleanup on some | |
| C-allocated memory to free it, write a wrapper package and hide that | |
| detail inside. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b> | |
| Restrict access to objects that have finalizers, cleanups, and weak | |
| pointers to the package that created and applied them. | |
| </b> | |
| </p> | |
| <p> | |
| This is related to the previous point, but is worth calling out | |
| explicitly, since it's a very powerful pattern for using these | |
| features in a less error-prone way. | |
| For example, the <a href="/pkg/unique">unique package</a> uses | |
| weak pointers under the hood, but completely encasulates the objects | |
| that are weakly pointed-to. | |
| Those values can never be mutated by the rest of the application, | |
| it can only be copied through the | |
| <a href="/pkg/unique#Handle.Value">Value method</a>, preserving | |
| the illusion of infinite memory for package users. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b> | |
| Prefer cleaning up non-memory resources deterministically when possible, | |
| with finalizers and cleanups as a fallback. | |
| </b> | |
| </p> | |
| <p> | |
| Cleanups and finalizers are a good fit for memory resources such as | |
| memory allocated externally, like from C, or references to an | |
| <code>mmap</code> mapping. | |
| Memory allocated by C's malloc must eventually be freed by C's free. | |
| A finalizer that calls <code>free</code>, attached to a wrapper object | |
| for the C memory, is a reasonable way to ensure that C memory is | |
| eventually reclaimed as a consequence of garbage collection. | |
| </p> | |
| <p> | |
| However, non-memory resources, like file descriptors, tend to be subject to | |
| system limits that the Go runtime is generally unaware of. | |
| In addition, the timing of the garbage collector in a given Go program | |
| is usually something a package author has little control over (for instance, | |
| how often the GC runs is controlled by <a href="#GOGC">GOGC</a>, which can | |
| be set by operators to a variety of different values in practice). | |
| These two facts conspire to make cleanups and finalizers a bad fit to use | |
| as the only mechanism for releasing non-memory resources. | |
| </p> | |
| <p> | |
| If you're a package author exposing an API that wraps some non-memory | |
| resource, consider providing an explicit API for releasing the resource | |
| deterministically (through a <code>Close</code> method, or something similar), | |
| rather than relying on the garbage collector through cleanups or finalizers. | |
| Instead, prefer to use cleanups and finalizers as a best-effort handler for | |
| programmer mistakes, either by cleaning up the resource anyway like | |
| <a href="/pkg/os#File">os.File</a> does, or by reporting the failure to | |
| deterministically clean up back to the user. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b> | |
| Prefer cleanups to finalizers. | |
| </b> | |
| </p> | |
| <p> | |
| Historically, finalizers were added to simplify the interface between Go code | |
| and C code and to clean up non-memory resources. | |
| The intended use was to apply them to wrapper objects that owned C memory or | |
| some other non-memory resource, so that the resource could be released once | |
| Go code was done using it. | |
| These reasons at least partially explain why finalizers are narrowly scoped, | |
| why any given object can only have one finalizer, and why that finalizer must | |
| be attached to the first byte of the object only. | |
| This limitation already stifles some use-cases. | |
| For example, any package that wishes to internally cache some information about | |
| an object passed to it cannot clean up that information once the object is gone. | |
| </p> | |
| <p> | |
| But worse than that, finalizers are inefficient and error-prone due to the fact | |
| that they <a href="https://en.wikipedia.org/wiki/Object_resurrection">resurrect | |
| the object</a> they're attached to, so that it can be passed to the finalizer | |
| function (and even continue to live beyond that, too). | |
| This simple fact means that if the object is part of a reference cycle it can | |
| never be freed, and the memory backing the object cannot be reused until at | |
| least until the following garbage collection cycle. | |
| </p> | |
| <p> | |
| Because finalizers resurrect objects, though, they do have a better-defined | |
| execution order than cleanups. | |
| For this reason, finalizers are still potentially (but rarely) useful for | |
| cleaning up structures that have complex destruction ordering requirements. | |
| </p> | |
| <p> | |
| But for all other uses in Go 1.24 and beyond, we recommend you use cleanups | |
| because they are more flexible, less error-prone, and more efficient than | |
| finalizers. | |
| </p> | |
| </li> | |
| </ul> | |
| <h4 id="Common_cleanup_issues">Common cleanup issues</h4> | |
| <ul> | |
| <li> | |
| <p> | |
| Objects with attached cleanups must not be reachable from the cleanup | |
| function (for example, through a captured local variable). | |
| This will prevent the object from being reclaimed and the cleanup from | |
| ever running. | |
| </p> | |
| </li> | |
| <pre> | |
| f := new(myFile) | |
| f.fd = syscall.Open(...) | |
| runtime.AddCleanup(f, func(fd int) { | |
| syscall.Close(f.fd) // Mistake: We reference f, so this cleanup won't run! | |
| }, f.fd) | |
| </pre> | |
| <li> | |
| <p> | |
| Objects with attached cleanups must not be reachable from the argument | |
| to the cleanup function. | |
| This will prevent the object from being reclaimed and the cleanup from | |
| ever running. | |
| </p> | |
| </li> | |
| <pre> | |
| f := new(myFile) | |
| f.fd = syscall.Open(...) | |
| runtime.AddCleanup(f, func(f *myFile) { | |
| syscall.Close(f.fd) | |
| }, f) // Mistake: We reference f, so this cleanup wouldn't ever run. This specific case also panics. | |
| </pre> | |
| <li> | |
| <p> | |
| Finalizers have a well-defined execution order, but cleanups do not. | |
| Cleanups can also run concurrently with one another. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| Long running cleanups should create a goroutine to avoid | |
| blocking the execution of other cleanups. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <code>runtime.GC</code> will not wait until cleanups for unreachable | |
| objects are executed, only until they are all queued. | |
| </p> | |
| </li> | |
| </ul> | |
| <h4 id="Common_weak_pointer_issues">Common weak pointer issues</h4> | |
| <ul> | |
| <li> | |
| <p> | |
| Weak pointers can begin returning <code>nil</code> from their | |
| <code>Value</code> method at unexpected times. | |
| Always guard the call to <code>Value</code> with a <code>nil</code> | |
| check and have a backup plan. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| When weak pointers are used as map keys, they do not affect the | |
| reachability of map values. | |
| Therefore, if a weak pointer map key points to an object that is also | |
| reachable from the map value, that object will still be considered | |
| reachable. | |
| </p> | |
| </li> | |
| </ul> | |
| <h4 id="Common_finalizer_issues">Common finalizer issues</h4> | |
| <ul> | |
| <li> | |
| <p> | |
| Objects with attached finalizers must not be reachable from themselves | |
| by any path (in other words, they cannot be in a reference cycle). | |
| This will prevent the object from being reclaimed and the finalizer from | |
| ever running. | |
| </p> | |
| </li> | |
| <pre> | |
| f := new(myCycle) | |
| f.self = f // Mistake: f is reachable from f, so this finalizer would never run. | |
| runtime.SetFinalizer(f, func(f *myCycle) { | |
| ... | |
| }) | |
| </pre> | |
| <li> | |
| <p> | |
| Objects with attached finalizers must not be reachable from the finalizer | |
| function (for example, through a captured local variable). | |
| This will prevent the object from being reclaimed and the finalizer from | |
| ever running. | |
| </p> | |
| </li> | |
| <pre> | |
| f := new(myFile) | |
| f.fd = syscall.Open(...) | |
| runtime.SetFinalizer(f, func(_ *myFile) { | |
| syscall.Close(f.fd) // Mistake: We reference the outer f, so this cleanup won't run! | |
| }) | |
| </pre> | |
| <li> | |
| <p> | |
| Reference chains of objects with attached finalizers (say, in a linked list) | |
| take, at minimum, as many GC cycles as there are objects in the chain | |
| to clean them all up. | |
| Keep finalizers shallow! | |
| </p> | |
| </li> | |
| <pre> | |
| // Mistake: reclaiming this linked list will take at least 10 GC cycles. | |
| node := new(linkedListNode) | |
| for range 10 { | |
| tmp := new(linkedListNode) | |
| tmp.next = node | |
| node = tmp | |
| runtime.SetFinalizer(node, func(node *linkedListNode) { | |
| ... | |
| }) | |
| } | |
| </pre> | |
| <li> | |
| <p> | |
| Avoid placing finalizers on objects returned at package boundaries. | |
| This makes it possible for users of your package to call | |
| <code>runtime.SetFinalizer</code> to mutate the finalizer on the object | |
| you return, which can be an unexpected behavior that users of your package | |
| may end up relying on. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| Long running finalizers should create a new goroutine | |
| to avoid blocking the execution of other finalizers. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <code>runtime.GC</code> will not wait until finalizers for unreachable | |
| objects are executed, only until they are all queued. | |
| </p> | |
| </li> | |
| </ul> | |
| <h4 id="Testing_object_death">Testing object death</h4> | |
| <p> | |
| When using these features, it can sometimes be tricky to write tests for code that | |
| uses them. | |
| Here are some tips for writing robust tests for code that uses these features. | |
| </p> | |
| <ul> | |
| <li> | |
| Avoid running such tests in parallel with other tests. | |
| It helps a lot to increase determinism as much as possible and to have | |
| a good handle on the state of the world at any given time. | |
| </li> | |
| <li> | |
| Use <code>runtime.GC</code> to establish a baseline upon entering the | |
| test. | |
| Use <code>runtime.GC</code> to force weak pointers to <code>nil</code>, | |
| and to queue up cleanups and finalizers to run. | |
| </li> | |
| <li> | |
| <p> | |
| <code>runtime.GC</code> does not wait for cleanups and finalizers to run, | |
| it only queues them. | |
| </p> | |
| <p> | |
| To write the most robust tests possible, inject a way to block on a cleanup | |
| or finalizer from your test (for example, pass an optional channel to the | |
| cleanup and/or finalizer from the test, and write to the channel once it | |
| has finished executing). | |
| If this is too hard or impossible, an alternative is to spin on a particular | |
| post-cleanup state to be true. | |
| For example, the <code>os</code> tests call <code>runtime.Gosched</code> in | |
| a loop that checks whether a file has been closed, once it becomes | |
| unreachable. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| If writing tests for using finalizers, and you have a chain of objects | |
| that use finalizers, you will need at minimum the length of the deepest | |
| chain the test can create of <code>runtime.GC</code> calls to ensure | |
| all the finalizers run. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| Test in race mode to discover races between concurrent cleanups, and | |
| between cleanup and finalizer code and the rest of the codebase. | |
| </p> | |
| </li> | |
| </ul> | |
| <!-- TODO: Add a short section about non-steady-state behavior. --> | |
| <h3 id="Additional_resources">Additional resources</h3> | |
| <p> | |
| While the information presented above is accurate, it lacks the detail to | |
| fully understand costs and trade-offs in the Go GC's design. | |
| For more information, see the following additional resources. | |
| </p> | |
| <ul> | |
| <li> | |
| <a href="https://gchandbook.org/">The GC Handbook</a>—An | |
| excellent general resource and reference on garbage collector design. | |
| </li> | |
| <li> | |
| <a href="https://google.github.io/tcmalloc/design.html">TCMalloc</a>—Design | |
| document for the C/C++ memory allocator TCMalloc, which the Go memory allocator is based on. | |
| </li> | |
| <li> | |
| <a href="/blog/go15gc">Go 1.5 GC announcement</a>—The | |
| blog post announcing the Go 1.5 concurrent GC, which describes the algorithm in more detail. | |
| </li> | |
| <li> | |
| <a href="/blog/ismmkeynote">Getting to Go</a>—An | |
| in-depth presentation about the evolution of Go's GC design up to 2018. | |
| </li> | |
| <li> | |
| <a href="https://docs.google.com/document/d/1wmjrocXIWTr1JxU-3EQBI6BK6KgtiFArkG47XK73xIQ/edit">Go 1.5 concurrent GC pacing</a>—Design | |
| document for determining when to start a concurrent mark phase. | |
| </li> | |
| <li> | |
| <a href="/issue/30333">Smarter scavenging</a>—Design | |
| document for revising the way the Go runtime returns memory to the operating system. | |
| </li> | |
| <li> | |
| <a href="/issue/35112">Scalable page allocator</a>—Design | |
| document for revising the way the Go runtime manages memory it gets from the operating system. | |
| </li> | |
| <li> | |
| <a href="/issue/44167">GC pacer redesign (Go 1.18)</a>—Design | |
| document for revising the algorithm to determine when to start a concurrent mark phase. | |
| </li> | |
| <li> | |
| <a href="/issue/48409">Soft memory limit (Go 1.19)</a>—Design | |
| document for the soft memory limit. | |
| </li> | |
| </ul> | |
| <h2 id="A_note_about_virtual_memory">A note about virtual memory</h2> | |
| <p> | |
| This guide has largely focused on the physical memory use of the GC, but a | |
| question that comes up regularly is what exactly that means and how it compares | |
| to virtual memory (typically presented in programs like <code>top</code> as | |
| "VSS"). | |
| </p> | |
| <p> | |
| Physical memory is memory housed in the actual physical RAM chip in most | |
| computers. | |
| <a href="https://en.wikipedia.org/wiki/Virtual_memory">Virtual memory</a> is an | |
| abstraction over physical memory provided by the operating system to isolate | |
| programs from one another. | |
| It's also typically acceptable for programs to reserve virtual address space | |
| that doesn't map to any physical addresses at all. | |
| </p> | |
| <p> | |
| <b> | |
| Because virtual memory is just a mapping maintained by the operating system, | |
| it is typically very cheap to make large virtual memory reservations that don't | |
| map to physical memory. | |
| </b> | |
| </p> | |
| <p> | |
| The Go runtime generally relies upon this view of the cost of virtual memory in | |
| a few ways: | |
| </p> | |
| <ul> | |
| <li> | |
| <p> | |
| The Go runtime never deletes virtual memory that it maps. | |
| Instead, it uses special operations that most operating systems | |
| provide to explicitly release any physical memory resources | |
| associated with some virtual memory range. | |
| </p> | |
| <p> | |
| This technique is used explicitly to manage the | |
| <a href="#Memory_limit">memory limit</a> and return memory to the | |
| operating system that the Go runtime no longer needs. | |
| The Go runtime also releases memory it no longer needs continuously | |
| in the background. | |
| See <a href="#Additional_resources">the additional resources</a> for | |
| more information. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| On 32-bit platforms, the Go runtime reserves between 128 MiB and 512 MiB | |
| of address space up-front for the heap to limit fragmentation issues. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| The Go runtime uses large virtual memory address space reservations | |
| in the implementation of several internal data structures. | |
| On 64-bit platforms, these typically have a minimum virtual memory | |
| footprint of about 700 MiB. | |
| On 32-bit platforms, their footprint is negligible. | |
| </p> | |
| </li> | |
| </ul> | |
| <p> | |
| As a result, virtual memory metrics such as "VSS" in <code>top</code> are | |
| typically not very useful in understanding a Go program's memory footprint. | |
| Instead, focus on "RSS" and similar measurements, which more directly reflect | |
| physical memory usage. | |
| </p> | |
| <h2 id="Optimization_guide">Optimization guide</h2> | |
| <h3 id="Identifying_costs">Identifying costs</h3> | |
| <p> | |
| Before trying to optimize how your Go application interacts with the GC, it's | |
| important to first identify that the GC is a major cost in the first place. | |
| </p> | |
| </p> | |
| The Go ecosystem provides a number of tools for identifying costs and optimizing | |
| Go applications. | |
| For a brief overview of these tools, see the | |
| <a href="/doc/diagnostics">guide on diagnostics</a>. | |
| Here, we'll focus on a subset of these tools and a reasonable order to apply | |
| them in in order to understand GC impact and behavior. | |
| </p> | |
| <ol> | |
| <li> | |
| <p> | |
| <b>CPU profiles</b> | |
| </p> | |
| <p> | |
| A good place to start is with | |
| <a href="https://pkg.go.dev/runtime/pprof#hdr-Profiling_a_Go_program">CPU profiling</a>. | |
| CPU profiling provides an overview of where CPU time is spent, though to the | |
| untrained eye it may be difficult to identify the magnitude of the role the | |
| GC plays in a particular application. | |
| Luckily, understanding how the GC fits in mostly boils down to knowing what | |
| different functions in the `runtime` package mean. | |
| Below is a useful subset of these functions for interpreting CPU profiles. | |
| </p> | |
| <p> | |
| Note that the functions listed below are not leaf functions, so they may not show | |
| up in the default the <code>pprof</code> tool provides with the <code>top</code> | |
| command. | |
| Instead, use the <code>top -cum</code> command or use the <code>list</code> | |
| command on these functions directly and focus on the cumulative percent column. | |
| </p> | |
| </li> | |
| <ul> | |
| <li> | |
| <p> | |
| <b><code>runtime.gcBgMarkWorker</code></b>: Entrypoint to the | |
| background mark worker goroutines. | |
| Time spent here scales with GC frequency and the complexity and | |
| size of the object graph. | |
| It represents a baseline for how much time the application spends | |
| marking and scanning. | |
| </p> | |
| <p> | |
| Note that within these goroutines, you will find calls to | |
| <code>runtime.gcDrainMarkWorkerDedicated</code>, | |
| <code>runtime.gcDrainMarkWorkerFractional</code>, and | |
| <code>runtime.gcDrainMarkWorkerIdle</code>, | |
| which indicate worker type. | |
| In a largely idle Go application, the Go GC is going to use up | |
| additional (idle) CPU resources to get its job done faster, which | |
| is indicated with the <code>runtime.gcDrainMarkWorkerIdle</code> | |
| symbol. | |
| As a result, time here may represent a large fraction of CPU | |
| samples, which the Go GC believes are free. | |
| If the application becomes more active, CPU time in idle workers | |
| will drop. | |
| One common reason this can happen is if an application runs entirely | |
| in one goroutine but <code>GOMAXPROCS</code> is >1. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b><code>runtime.mallocgc</code></b>: Entrypoint to the memory | |
| allocator for heap memory. | |
| A large amount of cumulative time spent here (>15%) | |
| typically indicates a lot of memory being allocated. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b><code>runtime.gcAssistAlloc</code></b>: Function goroutines | |
| enter to yield some of their time to assist the GC with scanning | |
| and marking. | |
| A large amount of cumulative time spent here (>5%) indicates | |
| that the application is likely out-pacing the GC with respect to how | |
| fast it's allocating. | |
| It indicates a particularly high degree of impact from the GC, | |
| and also represents time the application spend marking and scanning. | |
| Note that this is included in the <code>runtime.mallocgc</code> | |
| call tree, so it will inflate that as well. | |
| </p> | |
| </li> | |
| </ul> | |
| <li> | |
| <p> | |
| <b>Execution traces</b> | |
| </p> | |
| <p> | |
| While CPU profiles are great for identifying where time is spent in | |
| aggregate, they're less useful for indicating performance costs that | |
| are more subtle, rare, or related to latency specifically. | |
| Execution traces</a> on | |
| the other hand provide a rich and deep view into a short window of | |
| a Go program's execution. | |
| They contain a variety of events related to the Go GC and specific | |
| execution paths can be directly observed, along with how the application | |
| might interact with the Go GC. | |
| All the GC events tracked are conveniently labeled as such in the trace | |
| viewer. | |
| </p> | |
| <p> | |
| See the <a href="https://pkg.go.dev/runtime/trace">documentation for the | |
| <code>runtime/trace</code></a> package for how to get started with | |
| execution traces. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| <b>GC traces</b> | |
| </p> | |
| <p> | |
| When all else fails, the Go GC provides a few different specific traces | |
| that provide much deeper insights into GC behavior. | |
| These traces are always printed directly to STDERR, one line per GC cycle, | |
| and are configured through the <code>GODEBUG</code> environment variable | |
| that all Go programs recognize. | |
| They're mostly useful for debugging the Go GC itself since they require | |
| some familiarity with the specifics of the GC's implementation, but | |
| nonetheless can occasionally be useful to gain a better understanding of | |
| GC behavior. | |
| </p> | |
| <p> | |
| The core GC trace is enabled by setting <code>GODEBUG=gctrace=1</code>. | |
| The output produced by this trace is documented in the | |
| <a href="https://pkg.go.dev/runtime#hdr-Environment_Variables">environment | |
| variables section in the documentation for the <code>runtime</code> | |
| package.</a> | |
| </p> | |
| <p> | |
| A supplementary GC trace called the "pacer trace" provides even deeper | |
| insights and is enabled by setting <code>GODEBUG=gcpacertrace=1</code>. | |
| Interpreting this output requires an understanding of the GC's "pacer" | |
| (see <a href="#Additional_resources">additional resources</a>), which is | |
| outside the scope of this guide. | |
| </p> | |
| </li> | |
| </ol> | |
| <h3 id="Eliminating_heap_allocations">Eliminating heap allocations</h3> | |
| <p> | |
| One way to reduce costs from the GC is to have the GC manage fewer values to begin | |
| with. | |
| The techniques described below can produce some of the largest improvements in | |
| performance, because as the <a href="#GOGC">GOGC section</a> demonstrated, the | |
| allocation rate of a Go program is a major factor in GC frequency, the key | |
| cost metric used by this guide. | |
| </p> | |
| <h4 id="Heap_profiling">Heap profiling</h4> | |
| <p> | |
| After <a href="#Identifying_costs">identifying that the GC is a source of | |
| significant costs</a>, the next step in eliminating heap allocations is to | |
| find out where most of them are coming from. | |
| For this purpose, memory profiles (really, heap memory profiles) are very | |
| useful. | |
| Check out the <a href="https://pkg.go.dev/runtime/pprof#hdr-Profiling_a_Go_program"> | |
| documentation</a> for how to get started with them. | |
| </p> | |
| <p> | |
| Memory profiles describe where in the program heap allocations come from, | |
| identifying them by the stack trace at the point they were allocated. | |
| Each memory profile can break down memory in four ways. | |
| </p> | |
| <ul> | |
| <li><code>inuse_objects</code>—Breaks down the number of objects that | |
| are live.</li> | |
| <li><code>inuse_space</code>—Breaks down live objects by how much | |
| memory they use in bytes.</li> | |
| <li><code>alloc_objects</code>—Breaks down the number of objects | |
| that have been allocated since the Go program began executing.</li> | |
| <li><code>alloc_space</code>—Breaks down the total amount of memory | |
| allocated since the Go program began executing.</li> | |
| </ul> | |
| <p> | |
| Switching between these different views of heap memory may be done with either | |
| the <code>-sample_index</code> flag to the <code>pprof</code> tool, or via the | |
| <code>sample_index</code> option when the tool is used interactively. | |
| </p> | |
| <p class="gc-guide-note"> | |
| Note: memory profiles by default only sample a subset of heap objects so they | |
| will not contain information about every single heap allocation. | |
| However, this is sufficient to find hot-spots. | |
| To change the sampling rate, see | |
| <a href="https://pkg.go.dev/runtime#pkg-variables"><code>runtime.MemProfileRate</code></a>. | |
| </p> | |
| <p> | |
| For the purposes of reducing GC costs, <code>alloc_space</code> is typically the | |
| most useful view as it directly corresponds to the allocation rate. | |
| This view will indicate allocation hot spots that would provide the most benefit. | |
| </p> | |
| <h4 id="Escape_analysis">Escape analysis</h4> | |
| <p> | |
| Once candidate heap allocation sites have been identified with the help of | |
| <a href="#Heap_profiling">heap profiles</a>, how can they be eliminated? | |
| The key is to leverage the Go compiler's escape analysis to have the Go compiler | |
| find alternative, and more efficient storage for this memory, for example | |
| in the goroutine stack. | |
| Luckily, the Go compiler has the ability to describe why it decides to escape | |
| a Go value to the heap. | |
| With that knowledge, it becomes a matter of reorganizing your source code to | |
| change the outcome of the analysis (which is often the hardest part, but outside | |
| the scope of this guide). | |
| </p> | |
| <p> | |
| As for how to access the information from the Go compiler's escape analysis, the | |
| simplest way is through a debug flag supported by the Go compiler that describes | |
| all optimizations it applied or did not apply to some package in a text format. | |
| This includes whether or not values escape. | |
| Try the following command, where <code>[package]</code> is some Go package path. | |
| </p> | |
| <pre> | |
| $ go build -gcflags=-m=3 [package] | |
| </pre> | |
| <p> | |
| This information can also be visualized as an overlay in | |
| an LSP-capable editor; it is exposed as a code action. | |
| For example, in VS Code, invoke the "Source Action... > Show compiler | |
| optimization details" command to enable diagnostics for the current package. | |
| (You can also run the "Go: Toggle compiler optimization details" | |
| command.) | |
| Use this configuration setting to control which annotations are displayed: | |
| </p> | |
| <ol> | |
| <li> | |
| Enable the overlay for escape analysis by | |
| <a href="https://github.com/golang/vscode-go/wiki/settings#uidiagnosticannotations"> | |
| setting <code>ui.diagnostic.annotations</code> to include <code>escape</code> | |
| </a>. | |
| </li> | |
| </ol> | |
| <p> | |
| Finally, the Go compiler provides this information in a machine-readable (JSON) | |
| format that may be used to build additional custom tooling. | |
| For more information on that, see the | |
| <a href="https://cs.opensource.google/go/go/+/master:src/cmd/compile/internal/logopt/log_opts.go;l=25;drc=351e0f4083779d8ac91c05afebded42a302a6893"> | |
| documentation in the source Go code</a>. | |
| <p> | |
| <h3 id="Implementation-specific_optimizations">Implementation-specific optimizations</h3> | |
| <p> | |
| The Go GC is sensitive to the demographics of live memory, because a complex | |
| graph of objects and pointers both limits parallelism and generates more work | |
| for the GC. | |
| As a result, the GC contains a few optimizations for specific common structures. | |
| The most directly useful ones for performance optimization are listed below. | |
| </p> | |
| <p class="gc-guide-note"> | |
| Note: Applying the optimizations below may reduce the readability of your code | |
| by obscuring intent, and may fail to hold up across Go releases. | |
| Prefer to apply these optimizations only in the places they matter most. | |
| Such places may be identified by using the tools listed in the | |
| <a href="#Identifying_costs">section on identifying costs</a>. | |
| </p> | |
| <ul> | |
| <li> | |
| <p> | |
| Pointer-free values are segregated from other values. | |
| </p> | |
| <p> | |
| As a result, it may be advantageous to eliminate pointers from data | |
| structures that do not strictly need them, as this reduces the cache | |
| pressure the GC exerts on the program. | |
| As a result, data structures that rely on indices over pointer values, | |
| while less well-typed, may perform better. | |
| This is only worth doing if it's clear that the object graph is complex | |
| and the GC is spending a lot of time marking and scanning. | |
| </p> | |
| </li> | |
| <li> | |
| <p> | |
| The GC will stop scanning values at the last pointer in the value. | |
| </p> | |
| <p> | |
| As a result, it may be advantageous to group pointer fields in | |
| struct-typed values at the beginning of the value. | |
| This is only worth doing if it's clear the application spends a lot of its | |
| time marking and scanning. | |
| (In theory the compiler can do this automatically, but it is not yet | |
| implemented, and struct fields are arranged as written in the source | |
| code.) | |
| </p> | |
| </li> | |
| </ul> | |
| <p> | |
| Furthermore, the GC must interact with nearly every pointer it sees, so using | |
| indices into an slice, for example, instead of pointers, can aid in reducing GC | |
| costs. | |
| </p> | |
| <h3 id="Linux_transparent_huge_pages">Linux transparent huge pages (THP)</h3> | |
| <p> | |
| When a program accesses memory, the CPU needs to translate the | |
| <a href="#A_note_about_virtual_memory">virtual memory</a> addresses it uses | |
| into physical memory addresses that refer to the data it was trying to access. | |
| To do this, the CPU consults the "page table," a data structure that represents | |
| the mapping from virtual to physical memory, managed by the operating system. | |
| Each entry in the page table represents an indivisible block of physical memory | |
| called a page, hence the name. | |
| </p> | |
| <p> | |
| Transparent huge pages (THP) is a Linux feature that transparently replaces pages of | |
| physical memory backing contiguous virtual memory regions with bigger blocks of memory | |
| called huge pages. | |
| By using bigger blocks, fewer page table entries are needed to represent the same memory | |
| region, improving page table lookup times. | |
| However, bigger blocks mean more waste if only a small part of the huge page is used | |
| by the system. | |
| </p> | |
| <p> | |
| When running Go programs in production, enabling transparent huge pages on Linux | |
| can improve throughput and latency at the cost of additional memory use. | |
| Applications with small heaps tend not to benefit from THP and may end up using a | |
| substantial amount of additional memory (as high as 50%). | |
| However, applications with big heaps (1 GiB or more) tend to benefit quite a bit | |
| (up to 10% throughput) without very much additional memory overhead (1-2% or less). | |
| Being aware of your THP settings in either case can be helpful, and experimentation | |
| is always recommended. | |
| </p> | |
| <p> | |
| One can enable or disable transparent huge pages in a Linux environment by modifying | |
| <code>/sys/kernel/mm/transparent_hugepage/enabled</code>. | |
| See the | |
| <a href="https://www.kernel.org/doc/html/next/admin-guide/mm/transhuge.html">official | |
| Linux admin guide</a> for more details. | |
| If you choose to have your Linux production environment enable transparent huge pages, | |
| we recommend the following additional settings for Go programs. | |
| </p> | |
| <ul> | |
| <li> | |
| <p> | |
| Set <code>/sys/kernel/mm/transparent_hugepage/defrag</code> | |
| to <code>defer</code> or <code>defer+madvise</code>. | |
| <br /> | |
| <br /> | |
| This setting controls how aggressively a Linux kernel coalesces regular | |
| pages into huge pages. | |
| <code>defer</code> tells the kernel to coalesce huge pages lazily | |
| and in the background. | |
| A more aggressive setting can induce stalls in memory constrained systems | |
| and can often hurt application latencies. | |
| <code>defer+madvise</code> is like <code>defer</code>, but is friendlier | |
| to other applications on the system that request huge pages explicitly and | |
| require them for performance. | |
| </p> | |
| </li> | |
| <li> | |
| <p id="Linux_THP_max_ptes_none_workaround"> | |
| Set <code>/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none</code> | |
| to <code>0</code>. | |
| <br /> | |
| <br /> | |
| This setting controls how many additional pages the Linux kernel daemon | |
| can allocate when trying to allocate a huge page. | |
| The default setting is maximally aggressive, and can often | |
| <a href="https://bugzilla.kernel.org/show_bug.cgi?id=93111">undo work the Go | |
| runtime does to return memory to the OS</a>. | |
| Before Go 1.21, the Go runtime tried to mitigate the negative effects of the | |
| default setting, but it came with a CPU cost. | |
| With Go 1.21+ and Linux 6.2+, the Go runtime no longer mutates huge page | |
| state. | |
| <br /> | |
| <br /> | |
| If you experience an increase in memory usage when upgrading to Go 1.21.1 or | |
| later, try applying this setting; it will likely resolve your issue. | |
| As an additional workaround, you can call | |
| <a href="/pkg/golang.org/x/sys/unix#Prctl">the <code>Prctl</code> | |
| function</a> with <code>PR_SET_THP_DISABLE</code> to disable huge pages at | |
| the process level, or you can set <code>GODEBUG=disablethp=1</code> (to be | |
| added in Go 1.21.6 and Go 1.22) to disable huge pages for heap memory. | |
| Note that the <code>GODEBUG</code> setting may be removed in a future release. | |
| </p> | |
| </li> | |
| </ul> | |
| <h2 id="Appendix">Appendix</h2> | |
| <h3 id="Additional_notes_on_GOGC">Additional notes on GOGC</h3> | |
| <p> | |
| The <a href="#GOGC">GOGC section</a> claimed that doubling GOGC doubles heap | |
| memory overheads and halves GC CPU costs. | |
| To see why, let's break it down mathematically. | |
| </p> | |
| <p> | |
| Firstly, the heap target sets a target for the total heap size. | |
| This target, however, mainly influences the new heap memory, because the live | |
| heap is fundamental to the application. | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| Target heap memory = Live heap + (Live heap + GC roots) * GOGC / 100 | |
| </i> | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| Total heap memory = Live heap + New heap memory | |
| </i> | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| ⇒ | |
| </i> | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| New heap memory = (Live heap + GC roots) * GOGC / 100 | |
| </i> | |
| </p> | |
| <p> | |
| From this we can see that doubling GOGC would also double the amount of new heap | |
| memory that application will allocate each cycle, which captures heap memory | |
| overheads. | |
| Note that <i>Live heap + GC roots</i> is an approximation of the amount of | |
| memory the GC needs to scan. | |
| </p> | |
| <p> | |
| Next, let's look at GC CPU cost. | |
| Total cost can be broken down as the cost per cycle, times GC frequency over | |
| some time period T. | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| Total GC CPU cost = (GC CPU cost per cycle) * (GC frequency) * T | |
| </i> | |
| </p> | |
| <p> | |
| GC CPU cost per cycle can be derived from the | |
| <a href="#Understanding_costs">GC model</a>: | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| GC CPU cost per cycle = (Live heap + GC roots) * (Cost per byte) + Fixed cost | |
| </i> | |
| </p> | |
| <p> | |
| Note that sweep phase costs are ignored here as mark and scan costs dominate. | |
| </p> | |
| <p> | |
| The steady state is defined by a constant allocation rate and a constant cost | |
| per byte, so in the steady state we can derive a GC frequency from this new heap | |
| memory: | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| GC frequency = (Allocation rate) / (New heap memory) = (Allocation rate) / ((Live heap + GC roots) * GOGC / 100) | |
| </i> | |
| </p> | |
| <p> | |
| Putting this together, we get the full equation for the total cost: | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| Total GC CPU cost = (Allocation rate) / ((Live heap + GC roots) * GOGC / 100) * ((Live heap + GC roots) * (Cost per byte) + Fixed cost) * T | |
| </i> | |
| </p> | |
| <p> | |
| For a sufficiently large heap (which represents most cases), the marginal costs | |
| of a GC cycle dominate the fixed costs. | |
| This allows for a significant simplification of the total GC CPU cost formula. | |
| </p> | |
| <p class="gc-guide-equation"> | |
| <i> | |
| Total GC CPU cost = (Allocation rate) / (GOGC / 100) * (Cost per byte) * T | |
| </i> | |
| </p> | |
| <p> | |
| From this simplified formula, we can see that if we double GOGC, we halve total | |
| GC CPU cost. | |
| (Note that the visualizations in this guide do simulate fixed costs, so the GC | |
| CPU overheads reported by them will not exactly halve when GOGC doubles.) | |
| Furthermore, GC CPU costs are largely determined by allocation rate and the | |
| cost per byte to scan memory. | |
| For more information on how to reduce these costs specifically, see the | |
| <a href="#Optimization_guide">optimization guide</a>. | |
| </p> | |
| <p class="gc-guide-note"> | |
| Note: there exists a discrepancy between the size of the live heap, and the | |
| amount of that memory the GC actually needs to scan: the same size live heap but | |
| with a different structure will result in a different CPU cost, but the same | |
| memory cost, resulting a different trade-off. | |
| This is why the structure of the heap is part of the definition of the | |
| steady state. | |
| The heap target should arguably only include the scannable live heap as a closer | |
| approximation of memory the GC needs to scan, but this leads to degenerate | |
| behavior when there's a very small amount of scannable live heap but the live | |
| heap is otherwise large. | |
| </p> | |
| <script src="/js/d3.js"></script> | |
| <script async src="/doc/gc-guide.js"></script> |