Understanding libpmem for Persistent Memory Programming
Persistent memory (PMEM) represents a fundamental shift in how we approach data persistence. Sitting at the intersection of storage and memory, PMEM offers byte-addressable access with near-DRAM speeds while maintaining data across power cycles. This transformative technology demands new programming models since traditional paradigms for volatile memory or block storage don’t fully capture its unique characteristics.
The libpmem library provides a foundation for effectively utilizing persistent memory in applications. This post explores the essential functions and patterns needed to develop robust persistent memory applications, highlighting key considerations when transitioning from traditional memory management to persistent memory programming.
This blog post provides a practical guide to libpmem functions through C with examples drawn from real-world persistent memory patterns.
Table of Contents
- Core Functions Overview
- Memory Operations
- Advanced Flush Control
- Best Practices for Persistent Memory
- Memory Ordering Considerations
- Optimization Tips
- Common Pitfalls
Core Functions Overview
Based on the official libpmem documentation, let’s explore the essential functions:
1. pmem_map_file()
void *pmem_map_file(const char *path, size_t len, int flags,
mode_t mode, size_t *mapped_lenp, int *is_pmemp);
This function maps a persistent memory file into memory and serves as the entry point for most PMEM applications. It offers several advantages:
- Finds optimal address for large page mappings
- Returns
is_pmem
flag to identify true persistent memory - Handles file creation and mapping in one call
2. pmem_persist()
void pmem_persist(const void *addr, size_t len);
This function ensures data is stored durably in persistent memory. In any PMEM application, you’ll use it for:
- Critical data structures
- Metadata updates
- Pointer modifications
It’s the most optimal way to flush changes for true pmem and performs flush directly from user space when possible.
3. pmem_msync()
int pmem_msync(const void *addr, size_t len);
This is a wrapper around standard msync() and serves as a fallback when memory isn’t true pmem (such as when testing with regular files instead of actual PMEM devices). It ensures argument alignment per POSIX requirements and is used for portability, though it’s slower than pmem_persist().
Memory Operations
1. pmem_memcpy_persist()
void *pmem_memcpy_persist(void *pmemdest, const void *src, size_t len);
This function provides an optimized version of memcpy for persistent memory. It uses non-temporal store instructions on Intel platforms and bypasses processor caches, combining the copy and persistence operations in one efficient call.
2. pmem_is_pmem()
int pmem_is_pmem(const void *addr, size_t len);
This function checks if a memory range is true persistent memory. It has high overhead, so you should cache the result. Applications typically call this during initialization to determine the appropriate flush strategy for the runtime environment.
Advanced Flush Control
1. pmem_flush()
void pmem_flush(const void *addr, size_t len);
This is the first step of persistence: flushing processor caches. It can be used for fine-grained control and may be an empty function on platforms with eADR (Extended ADR, a hardware feature that guarantees cache persistence).
2. pmem_drain()
void pmem_drain(void);
This is the second step: ensuring hardware buffers are drained. It’s a system-wide operation that can be deferred when doing multiple flushes, allowing for optimization in batch operations.
Best Practices for Persistent Memory
- Initialization Pattern
pmem_addr = pmem_map_file(PMEM_PATH, PMEM_SIZE,
PMEM_FILE_CREATE, 0666,
&mapped_len, &is_pmem);
- Data Persistence Pattern
pmem_persist(critical_data, sizeof(critical_data));
- Clean Shutdown
pmem_unmap(pmem_addr, mapped_len);
Memory Ordering Considerations
Persistence is Not Visibility
- CPU barriers (SFENCE) handle thread visibility
- Only pmem functions guarantee persistence
Write Ordering
- No guaranteed order without proper flushing
- Must use pmem functions for durability
Atomicity
- Individual writes up to 8 bytes may be atomic
- Larger operations need explicit handling through techniques like logging
Optimization Tips
Batch Operations
- Group multiple updates before calling pmem_persist()
- Reduces flush overhead but increases time delay for consistency guarantee
Memory Layout
- Align data structures for optimal performance
- Consider cache line boundaries (typically 64 bytes)
- Keep related data together to minimize flush operations
Error Handling
- Always check return values
- Implement proper cleanup on failures
Common Pitfalls
Assuming All Memory is Persistent
- Always check is_pmem flag
- Use appropriate flush strategy for the detected memory type
Unnecessary Flushes
- Don’t flush read-only data
- Batch updates when possible
- Be aware of automatic flushes from library calls
Incomplete Persistence
- Ensure all critical data is flushed
- Don’t forget metadata and pointer updates
- Consider transactions for complex data structure modifications
This overview provides the foundation for developing with persistent memory using libpmem. The programming model requires careful consideration of persistence boundaries, ordering, and failure recovery - but the performance and architectural benefits make it worthwhile for many applications.