An Introduction to Persistent Memory Pointers in C++

Overview

Persistent pointers are vital to persistent memory programming because they protect against corruption and have additional functionality over regular pointers to support persistent memory. A persistent memory pointer is a type of smart pointer which retains shared ownership of an object and features its own garbage collector, similar to the well-known smart pointer std::shared_ptr. Similarly, a persistent memory pointer wraps around a type and provides additional features. In this article, we will discuss in more detail what a persistent pointer is and how to use it.

Persistent Memory

This article assumes that you have a basic understanding of persistent memory concepts and are familiar with the basic features of the Persistent Memory Development Kit (PMDK). If not, please visit the Intel® Developer Zone Persistent Memory Programming site, where you’ll find the information you need to get started.

Why Use Persistent Memory Pointers?

Programming with persistent memory uses memory mapped files called persistent memory pools. The PMDK incorporates some useful APIs to manage data synchronization and other complexities of programming using memory mapped files. Due to security considerations in modern operating systems, memory mapped files are usually mapped to random memory addresses in the application’s virtual address space through a process called Address Space Layout Randomization (ASLR). With ASLR, every time you memory map a file, you can end up with a different pool base address. This means that regular pointers in a persistent data structure may point to an address that is not mapped. In the best case, this will cause a segmentation fault. Or your program could corrupt data and not crash at all – making it harder to debug. Persistent pointers solve this problem with additional features giving them the ability to point to objects within a persistent memory pool. Unlike volatile pointers, a persistent pointer can manage the translation between persistent addresses and runtime addresses. Persistent pointers are random access iterators, which is a necessary condition to be considered a smart pointer in C++.

Persistent pointer syntax

Let’s take a look at an example of the syntax of a persistent pointer in the Panaconda* code sample. Panaconda is a game of Snake that demonstrates persistent memory pools, pointers, and transactions. We’ll look at a small code snippet from the code sample:

Bool game_board::is_wall_collision(point point)

{
    int i = 0;
    bool result = false;
    persistent_ptr<board_element> wallp;
    
    while ((wallp = layout->get(i++)) != nullptr) {
        if (point == *(wallp->get_position().get())) {
            result = true;
            break;
        }
    }

    return result;
}  

The is_wall_collision() function is used to detect whether the snake had a collision with a wall. The sample starts off with the creation of a persistent pointer called wallp, which is used to loop through all the wall elements of the game board. The loop exits either when all wall elements have been checked, or when the passed in point variable equals the wall element, implying that the snake ran into the wall. 

Persistent pointer limitations

Persistent pointers have the following limitations:

  • They don’t point to polymorphic objects in C++. This means pointers for the base class are not type-compatible with pointers of a derived class. This is due to the lack of a vtable implementation that can be stored safely in persistent memory. This limitation may be addressed in future versions of the C++ bindings.
  • A persistent pointer adds itself to a transaction, but does not add the data object it points to. By adding itself, it ensures that modifications to the pointer object remain safe in the event of a power failure. The programmer needs to make sure that if changes are expected to the underlying object, its simple variables are declared using the special class p<> and its pointers are also stored as persistent_ptr<>. This way, the object’s contents will add themselves to the transaction automatically. 
  • Persistent pointers do not manage object lifetime. In other words, there is no garbage collection. Memory must be explicitly deallocated.

Allocation and Deallocation

Allocation

Memory allocation in PMDK can be done in two ways:

  • make_persistent_atomic<>(). This function will allocate, in a fail-safe manner, a single object in our persistent memory pool.
  • make_persistent<>(). This way is useful if we need to perform multiple allocations atomically, or if we need to perform extra operations as a result of allocating the object (such as adding it to a larger data structure). Of course, we can also use a single make_persistent<>() inside a transaction too. In the following snippet, we do just that:
auto pop = pool_base::create(...);

persistent_ptr<entry> pentry;

transaction::exec_txt(pop, [&] { pentry = make_persistent<entry>(); });

This code snippet first shows the creation of a persistent_ptr named pentry. After that it shows the declaration of a transaction that allocates one entry object using the call to make_persistent.

Deallocation

To destroy an object after it is done being used, we use the delete_persistent function, which makes a call to the object’s destructor, if there is one. Let’s see the sample syntax of deallocating an object that would be found inside a transaction:

 delete_persistent<entry>(pentry);

An example

Let’s take a look at an example usage of make_persistent and delete_persistent in a code sample called PMAN. PMAN is a game of Pac-Man* that’s designed to take advantage of persistent memory using the PMDK’s libpmemobj library. Read the article Code Sample: PMAN – A Persistent Memory Version of the Game Pac-Man for details. We’ll focus on a small section of the code:

Void state::new_game(const std::string &map_file){
    transaction::manual tx(pop);
    board = make_persistent<board_state>(map_file);
    pl = make_persistent<player>(POS_MIDDLE);
    intro_p = make_persistent<list<intro>>();
    bombs = make_persistent<list<bomb>>();
    aliens = make_persistent<list<alien>>();
    aliens->push_back(make_persistent<alien>(UP_LEFT));
    transaction::commit();
} 

The function new_game is used to allocate objects for a new game. It allocates board, p1, intro_p, bombs, and aliens using the make_persistent function. Another method is shown below that demonstrates delete_persistent:

Void state::resume()
{
    {
        transaction::manual tx(pop);

        delete_persistent<player>(pl);
        pl = nullptr;

        aliens->clear();
        delete_persistent<list<alien>>(aliens);

        bombs->clear();
        delete_persistent<list<bomb>>(bombs);
        intro_p->clear();

        delete_persistent<list<intro>>(intro_p);
        transaction::commit();
    }
    
    reset_game();
}

In this code snippet we see the declaration of a transaction inside the resume() function, which is used to resume a game. There are multiple calls to delete_persistent(), which are responsible for deallocating the objects created in a previous invocation of the new game function. After deallocating the objects, the function calls reset_game() to reset the game.

Atomic allocations

As mentioned above, you can also use atomic allocations. Atomic allocations are fail-safe, which means that they are equivalent to the use of non-atomic allocations inside a transaction. Atomic allocations are much faster because they do not need general transactions, although a small redo log is still used to make sure memory reservation metadata and the destination pointer do not get corrupted. These allocations are not as flexible given that we cannot atomically allocate an object and add it to a data structure at the same time. This limitation has the risk of producing memory leaks; for example, the system crashes after the allocation success but before the persistent pointer is linked to a persistent data structure. Fortunately, memory leaks in PMDK can be recovered. If you want to know more, please read the article Find your Leaked Persistent Memory Objects Using the Persistent Memory Development Kit (PMDK).

To deallocate objects atomically, use delete_persistent_atomic(). Let’s look at the sample syntax for both make_persistent_atomic() and delete_persistent_atomic().

auto pop = pool_base::create(...);

persistent_ptr<entry> pentry;

make_persistent_atomic<entry> (pop, pentry);

delete_persistent_atomic<entry> (pentry);

Summary

In this article we described persistent pointers, and how and when to use one. To summarize, they are smart pointers that have additional features designed to work well with persistent memory. One of the limitations of persistent pointers is that they cannot manage the object’s lifetime. This requires you to handle allocation and deallocation manually. Unlike the case with volatile memory, in persistent memory we cannot use malloc or new to manage allocation and free or delete for deallocation, since persistent memory lives in memory-mapped files. Instead, the make_persistent, make_persistent_atomic, delete_persistent and delete_persistent_atomic functions from PMDK are used. 

For more PMDK programming code samples visit the pmdk-examples respository on GitHub

有关编译器优化的更完整信息,请参阅优化通知