| ### Proposal: Add support for Persistent Memory in Go |
| |
| Authors: Jerrin Shaji George, Mohit Verma, Rajesh Venkatasubramanian, Pratap Subrahmanyam |
| |
| Last updated: January 20, 2021 |
| |
| Discussion at https://golang.org/issue/43810. |
| |
| ## Abstract |
| |
| Persistent memory is a new memory technology that allows byte-addressability at |
| DRAM-like access speed and provides disk-like persistence. Operating systems |
| such as Linux and Windows server already support persistent memory and the |
| hardware is available commercially in servers. More details on this technology |
| can be found at [pmem.io](https://pmem.io). |
| |
| This is a proposal to add native support for programming persistent memory in |
| Go. A detailed design of our approach to add this support is described in our |
| 2020 USENIX ATC paper [go-pmem](https://www.usenix.org/system/files/atc20-george.pdf). |
| An implementation of the above design based on Go 1.15 release is available |
| [here](http://github.com/jerrinsg/go-pmem). |
| |
| ## Background |
| |
| Persistent Memory is a new type of random-access memory that offers persistence |
| and byte-level addressability at DRAM-like access speed. Operating systems |
| provide the capability to mmap this memory to an application's virtual address |
| space. Applications can then use this mmap'd region just like memory. Durable |
| data updates made to persistent memory can be retrieved by an application even |
| after a crash/restart. |
| |
| Applications using persistent memory benefit in a number of ways. Since durable |
| data updates made to persistent memory is non-volatile, applications no longer |
| need to marshal data between DRAM and storage devices. A significant portion |
| of application code that used to do this heavy-lifting can now be retired. |
| Another big advantage is a significant reduction in application startup times on |
| restart. This is because applications no longer need to transform their at-rest |
| data into an in-memory representation. For example, commercial applications like |
| SAP HANA report a [12x improvement](https://cloud.google.com/blog/topics/partners/available-first-on-google-cloud-intel-optane-dc-persistent-memory) |
| in startup times using persistent memory. |
| |
| This proposal is to provide first-class native support for Persistent memory in |
| Go. Our design modifies Go 1.15 to introduce a garbage collected persistent |
| heap. We also instrument the Go compiler to introduce semantics that enables |
| transactional updates to persistent-memory datastructures. We call our modified |
| Go suite as *go-pmem*. A Redis database developed with using go-pmem offers more |
| than 5x throughput compared to Redis running on NVMe SSD. |
| |
| ## Proposal |
| |
| We propose adding native support for programming persistent memory in Go. This |
| requires making the following features available in Go: |
| |
| 1. Support persistent memory allocations |
| 2. Garbage collection of persistent memory heap objects |
| 3. Support modifying persistent memory datastructures in a crash-consistent |
| manner |
| 4. Enable applications to recover following a crash/restart |
| 5. Provide applications a mechanism to retrieve back durably stored data in |
| persistent memory |
| |
| To support these features, we extended the Go runtime and added a new SSA pass |
| in our implementation as discussed below. |
| |
| ## Rationale |
| |
| There exists libraries such as Intel [PMDK](https://pmem.io/pmdk/) that provides |
| C and C++ developers support for persistent memory programming. Other |
| programming languages such as Java and Python are exploring ways to enable |
| efficient access to persistent memory. E.g., |
| * Java - https://bugs.openjdk.java.net/browse/JDK-8207851 |
| * Python - https://pynvm.readthedocs.io/en/v0.3.1/ |
| |
| But no language provide a native persistent memory programming support. We |
| believe this is an impediment to widespread adoption to this technology. This |
| proposal attempts to remedy this problem by making Go the first language to |
| completely support persistent memory. |
| |
| ### Why language change? |
| |
| The C libraries expose a programming model significantly different (and complex) |
| than existing programming models. In particular, memory management becomes |
| difficult with libraries. A missed "free" call can lead to memory leaks and |
| persistent memory leaks become permanent and do not vanish after application |
| restarts. In a language with a managed runtime such as Go, providing visibility |
| to its garbage collector into a memory region managed by a library becomes very |
| difficult. |
| Identifying and instrumenting stores to persistent memory data to provide |
| transactional semantics also requires programming language change. |
| In our implementation experience, the Go runtime and compiler was easily |
| amenable to add these capabilities. |
| |
| ## Compatibility |
| |
| Our current changes preserve the Go 1.x future compatibility promise. It does |
| not break compatibility for programs not using any persistent memory features |
| exposed by go-pmem. |
| |
| Having said that, we acknowledge a few downsides with our current design: |
| |
| 1. We store memory allocator metadata in persistent memory. When a program |
| restarts, we use these metadata to recreate the program state of the memory |
| allocator and garbage collector. As with any persistent data, we need to |
| maintain the data layout of this metadata. Any changes to Go memory allocator's |
| datastructure layout can break backward compatibility with our persistent |
| metadata. This can be fixed by developing an offline tool which can do this |
| data format conversion or by embedding this capability in go-pmem. |
| |
| 2. We currently add three new Go keywords : pnew, pmake and txn. pnew, pmake are |
| persistent memory allocation APIs and txn is used to demarcate transactional |
| updates to data structures. We have explored a few ways to avoid making these |
| language changes as described below. |
| |
| a) pnew/pmake |
| |
| The availability of generics support in a future version of Go can help us avoid |
| introducing these memory allocation functions. They can instead be functions |
| exported by a Go package. |
| |
| ``` |
| func Pnew[T any](_ T) *T { |
| ptr := runtime.pnew(T) |
| return ptr |
| } |
| |
| func Pmake[T any](_ T, len, cap int) []T { |
| slc := runtime.pmake([]T, len, cap) |
| return slc |
| } |
| ``` |
| |
| `runtime.pnew` and `runtime.pmake` would be special functions that can take a |
| type as arguments. They then behave very similar to the `new()` and `make()` |
| APIs but allocate objects in the persistent memory heap. |
| |
| b) txn |
| |
| An alternative approach would be to define a new Go pragma that identifies a |
| transactional block of code. It could have the following syntax: |
| |
| ``` |
| //go:transactional |
| { |
| // transactional data updates |
| } |
| ``` |
| |
| Another alternative approach can be to use closures with the help of a few |
| runtime and compiler changes. For example, something like this can work: |
| |
| ``` |
| runtime.Txn() foo() |
| ``` |
| |
| Internally, this would be similar to how Go compiler instruments stores when |
| mrace/msan flag is passed while compiling. In this case, writes inside |
| function foo() will be instrumented and foo() will be executed transactionally. |
| |
| See this playground [code](https://go2goplay.golang.org/p/WRUTZ9dr5W3) for a |
| complete code listing with our proposed alternatives. |
| |
| ## Implementation |
| |
| Our implementation is based on a fork of Go source code version Go 1.15. Our |
| implementation adds three new keywords to Go: pnew, pmake and txn. pnew and |
| pmake are persistent memory allocation APIs and txn is used to demarcate a |
| block of transaction data update to persistent memory. |
| |
| 1. pnew - `func pnew(Type) *Type` |
| |
| Just like `new`, `pnew` creates a zero-value object of the `Type` argument in |
| persistent memory and returns a pointer to this object. |
| |
| |
| 2. pmake - `func pmake(t Type, size ...IntType) Type` |
| |
| The `pmake` API is used to create a slice in persistent memory. The semantics of |
| `pmake` is exactly the same as `make` in Go. We don't yet support creating maps |
| and channels in persistent memory. |
| |
| 3. txn |
| |
| ``` |
| txn() { |
| // transaction data updates |
| } |
| ``` |
| |
| Our code changes to Go can be broken down into two parts - runtime changes and |
| compiler-SSA changes. |
| |
| ### Runtime changes |
| |
| We extend the Go runtime to support persistent memory allocations. The garbage |
| collector now works across both the persistent and volatile heaps. The `mspan` |
| datastructure has one additional data member `memtype` to distinguish between |
| persistent and volatile spans. We also extend various memory allocator |
| datastructures in mcache, mcentral, and mheap to store metadata related to |
| persistent memory and volatile memory separately. The garbage collector now |
| understands these different span types and puts back garbage collected spans |
| in the appropriate datastructures depending on its `memtype`. |
| |
| Persistent memory is managed in arenas that are a multiple of 64MB. Each |
| persistent memory arena has in its header section certain metadata that |
| facilitates heap recovery in case of application crash or restart. Two kinds of |
| metadata are stored: |
| * GC heap type bits - Garbage collector heap type bits set for any object in |
| this arena is copied as such to the metadata section to be restored on a |
| subsequent run of this application |
| * Span table - Captures metadata about each span in this arena that lets the |
| heap recovery code recreates these spans in the next run. |
| |
| We added the following APIs in the runtime package to manage persistent memory: |
| |
| 1 `func PmemInit(fname string) (unsafe.Pointer, error)` |
| |
| Used to initialize persistent memory. It takes the path to a persistent memory |
| file as input. It returns the application root pointer and an error value. |
| |
| 2 `func SetRoot(addr unsafe.Pointer) (err Error)` |
| |
| Used to set the application root pointer. All application data in persistent |
| memory hangs off this root pointer. |
| |
| 3 `func GetRoot() (addr unsafe.Pointer)` |
| |
| Returns the root pointer set using SetRoot(). |
| |
| 4 `func InPmem(addr unsafe.Pointer) bool` |
| |
| Returns whether `addr` points to data in persistent memory or not. |
| |
| 5. `func PersistRange(addr unsafe.Pointer, len uintptr)` |
| |
| Flushes all the cachelines in the address range (addr, addr+len) to ensure |
| any data updates to this memory range is persistently stored. |
| |
| ### Compiler-SSA changes |
| |
| 1. We change the parser to recognize three new language tokens - `pnew`, |
| `pmake`, and `txn`. |
| |
| 2. We add a new SSA pass to instrument all stores to persistent memory. Because |
| data in persistent memory survives crashes, updates to data in persistent memory |
| have to be transactional. |
| |
| 3. The Go AST and SSA was modified so that users can now demarcate a block of |
| Go code as transactional by encapsulating them within a `txn()` block. |
| - To do this, we add a new keyword to Go called `txn`. |
| - A new SSA pass would then look for stores(`OpStore`/`OpMove`/`OpZero`) to |
| persistent memory locations within this `txn()` block, and store the old |
| data at this location in an [undo Log](https://github.com/vmware/go-pmem-transaction/blob/master/transaction/undoTx.go). |
| This would be done before making the actual memory update. |
| |
| |
| ### go-pmem packages |
| |
| We have developed two packages that makes it easier to use go-pmem to write |
| persistent memory applications. |
| |
| 1. [pmem](https://github.com/vmware/go-pmem-transaction/tree/master/pmem) package |
| |
| It provides a simple `Init(fname string) bool` API that applications can use to |
| initialize persistent memory. It returns if this is a first-time initialization |
| or not. In case it is not the first-time initialization, any incomplete |
| transactions are reverted as well. |
| |
| pmem package also provides named objects where names can be associated with |
| objects in persistent memory. Users can create and retrieve these objects using |
| string names. |
| |
| 2. [transaction](https://github.com/vmware/go-pmem-transaction/tree/master/transaction) package |
| |
| Transaction package provides the implementation of undo logging that is used |
| by go-pmem to enable crash-consistent data updates. |
| |
| |
| ### Example Code |
| |
| Below is a simple linked list application written using go-pmem |
| |
| ``` |
| // A simple linked list application. On the first invocation, it creates a |
| // persistent memory pointer named "dbRoot" which holds pointers to the first |
| // and last element in the linked list. On each run, a new node is added to |
| // the linked list and all contents of the list are printed. |
| |
| package main |
| |
| import ( |
| "github.com/vmware/go-pmem-transaction/pmem" |
| "github.com/vmware/go-pmem-transaction/transaction" |
| ) |
| |
| const ( |
| // Used to identify a successful initialization of the root object |
| magic = 0x1B2E8BFF7BFBD154 |
| ) |
| |
| // Structure of each node in the linked list |
| type entry struct { |
| id int |
| next *entry |
| } |
| |
| // The root object that stores pointers to the elements in the linked list |
| type root struct { |
| magic int |
| head *entry |
| tail *entry |
| } |
| |
| // A function that populates the contents of the root object transactionally |
| func populateRoot(rptr *root) { |
| txn() { |
| rptr.magic = magic |
| rptr.head = nil |
| rptr.tail = nil |
| } |
| } |
| |
| // Adds a node to the linked list and updates the tail (and head if empty) |
| func addNode(rptr *root) { |
| entry := pnew(entry) |
| txn() { |
| entry.id = rand.Intn(100) |
| |
| if rptr.head == nil { |
| rptr.head = entry |
| } else { |
| rptr.tail.next = entry |
| } |
| rptr.tail = entry |
| } |
| } |
| |
| func main() { |
| firstInit := pmem.Init("database") |
| var rptr *root |
| if firstInit { |
| // Create a new named object called dbRoot and point it to rptr |
| rptr = (*root)(pmem.New("dbRoot", rptr)) |
| populateRoot(rptr) |
| } else { |
| // Retrieve the named object dbRoot |
| rptr = (*root)(pmem.Get("dbRoot", rptr)) |
| if rptr.magic != magic { |
| // An object named dbRoot exists, but its initialization did not |
| // complete previously. |
| populateRoot(rptr) |
| } |
| } |
| addNode(rptr) // Add a new node in the linked list |
| } |
| ``` |
| |