Advanced: TLM Memory Management
Avoiding segmentation faults and memory leaks by mastering the tlm_mm_interface and payload acquire/release semantics.
How to Read This Lesson
For TLM, resist the temptation to picture pins. Picture a C++ function call carrying a transaction object, then add timing only where the architectural question needs it.
Advanced TLM Pitfalls: Memory Management
If you browse the Accellera SystemC forums, the most common cause of advanced simulation crashes (Segmentation Faults) is the mismanagement of the TLM 2.0 Generic Payload (tlm_generic_payload).
Unlike a simple int or bool, a tlm_generic_payload is a massive object. It contains pointers to data buffers, byte enable arrays, extension arrays, and response statuses. Allocating and deallocating this object using standard C++ new and delete millions of times per second (e.g., for every memory read/write transaction) will bottleneck your simulation speed completely.
To solve this, the TLM 2.0 LRM dictates the use of a Memory Manager. Let's look at the Accellera TLM kernel source code to see why this is critical.
Source and LRM Trail
For TLM, use the IEEE 1666 TLM clauses in Docs/LRMs/SystemC_LRM_1666-2023.pdf as the portable contract. Then inspect .codex-src/systemc/src/tlm_core/tlm_2: tlm_generic_payload, tlm_fw_transport_if, tlm_bw_transport_if, tlm_initiator_socket, tlm_target_socket, tlm_dmi, and tlm_quantumkeeper.
The Problem with new and delete
A naive implementation of a TLM initiator might look like this:
// BAD CODE - Do not do this!
void send_transaction() {
tlm::tlm_generic_payload* trans = new tlm::tlm_generic_payload();
// ... setup trans ...
socket->b_transport(*trans, delay);
delete trans; // Highly expensive and prone to dangling pointer crashes!
}This is incredibly slow. Instead, the IEEE 1666 LRM requires maintaining a pool of payload objects and reusing them.
The Memory Manager (tlm_mm_interface)
TLM defines the tlm::tlm_mm_interface, providing two core virtual methods:
allocate(): Returns an unused payload from the pool.free(tlm_generic_payload* trans): Returns a payload back to the pool.
Complete Memory Manager Implementation Example
You must attach a memory manager to your payload upon creation. While tlm_utils provides some basic managers (tlm_utils::simple_peq_with_cb), writing a custom compliant one teaches you the exact standard mechanics.
#include <systemc>
#include <tlm>
#include <vector>
// 1. A Custom IEEE 1666 Compliant Memory Manager
class CustomMemoryManager : public tlm::tlm_mm_interface {
std::vector<tlm::tlm_generic_payload*> free_list;
public:
tlm::tlm_generic_payload* allocate() {
if (free_list.empty()) {
// Allocate a new payload and bind it to THIS memory manager
return new tlm::tlm_generic_payload(this);
} else {
tlm::tlm_generic_payload* trans = free_list.back();
free_list.pop_back();
return trans;
}
}
void free(tlm::tlm_generic_payload* trans) override {
trans->reset(); // Critical: Reset fields before returning to pool
free_list.push_back(trans);
}
};
SC_MODULE(InitiatorMM_Demo) {
CustomMemoryManager mm;
SC_CTOR(InitiatorMM_Demo) {
SC_THREAD(run_transactions);
}
void run_transactions() {
// First transaction (allocates new)
tlm::tlm_generic_payload* trans1 = mm.allocate();
trans1->acquire(); // Rule: Acquire before use
std::cout << "Transaction 1 Acquired." << std::endl;
// ... Send through socket (omitted for brevity) ...
trans1->release(); // Drops ref count to 0, calls mm.free() automatically
std::cout << "Transaction 1 Released." << std::endl;
// Second transaction (reuses the same memory block from the pool!)
tlm::tlm_generic_payload* trans2 = mm.allocate();
trans2->acquire();
std::cout << "Transaction 2 Acquired (Reused memory block)." << std::endl;
trans2->release();
}
};
int sc_main(int argc, char* argv[]) {
InitiatorMM_Demo initiator("initiator");
sc_core::sc_start();
return 0;
}The Golden Rules of acquire() and release()
When a payload travels through an SoC, it might pass through multiple routers, caches, and targets. How does the initiator know when it is safe to free() the payload back to the pool? What if a target stored a pointer to the payload in a queue to process it asynchronously via a Payload Event Queue (PEQ)?
This is where acquire() and release() come in. They implement an atomic Reference Counting mechanism embedded directly in the tlm_generic_payload base class.
Under the Hood (Accellera TLM source code):
The tlm_generic_payload contains an integer m_ref_count and a pointer tlm_mm_interface* m_mm.
- Rule 1: Acquiring. When you call
trans->acquire(), the source code simply executesm_ref_count++. If any component (router, target, observer) intends to keep a pointer to the payload after the current function call (e.g.,b_transportornb_transport) returns, it MUST calltrans->acquire(). - Rule 2: Releasing. Once that component is done with the payload, it MUST call
trans->release(). The source code executesm_ref_count--. - Rule 3: The Auto-Free Hook. Inside the
release()implementation, there is a check:if (m_ref_count == 0 && m_mm != 0) { m_mm->free(this); }. This automatically returns the payload to your memory manager pool. - Rule 4: Extensions cleanup: During
m_mm->free(), you must calltrans->reset(). In the Accellera kernel,reset()iterates over them_extensionsvector. If an extension has a memory manager attached, it frees it. If it doesn't, it might leave dangling memory unless managed correctly byfree_all_extensions().
Failure to follow these rules will result in catastrophic memory leaks (forgetting to release) or impossible-to-debug segmentation faults (releasing too early while a target is still reading data). Always use a Memory Manager in production code.
Comments and Corrections