Advanced: TLM Memory Management
Avoiding segmentation faults and memory leaks by mastering the tlm_mm_interface and payload acquire/release semantics.
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.
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, 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 in the payload class.
- Rule 1: The Initial State. When an initiator allocates a payload from the MM, its reference count is internally managed. You should
acquire()it if you intend to keep it. - Rule 2: Acquiring. 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 3: Releasing. Once that component is done with the payload, it MUST call
trans->release(). - Rule 4: The Auto-Free Hook. When
release()is called and the internal reference count drops to 0, the payload automatically invokes thefree()method of its attached memory manager, returning it to the pool.
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