SWDEV-555347 - Remove lock contention in async events loop (#878)
* SWDEV-555347 - Remove lock contention in async events loop * SWDEV-555347 - Introduce Pool of AsyncEventItems * create generic mempool for AsyncEventItem * Use BaseShared allocate and free for async event pool --------- Co-authored-by: Rahul Manocha <rmanocha@amd.com>
This commit is contained in:
@@ -168,6 +168,7 @@ void HostQueue::finish(bool cpu_wait) {
|
||||
|
||||
command = getLastQueuedCommand(true);
|
||||
if (command == nullptr) {
|
||||
ClPrint(LOG_DEBUG, LOG_CMD, "No command awaiting completion on host");
|
||||
return;
|
||||
}
|
||||
// Force blocking wait if requested. That allows to avoid a build up of unreleased CPU commands
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
#include "core/util/locks.h"
|
||||
#include "core/util/os.h"
|
||||
#include "core/util/utils.h"
|
||||
#include "core/util/mpsc_queue.hpp"
|
||||
|
||||
#include "core/inc/amd_loader_context.hpp"
|
||||
#include "core/inc/amd_hsa_code.hpp"
|
||||
@@ -595,6 +596,89 @@ class Runtime {
|
||||
std::vector<void*> arg_;
|
||||
};
|
||||
|
||||
// Event item structure to hold all signal information
|
||||
struct AsyncEventItem {
|
||||
hsa_signal_t signal;
|
||||
hsa_signal_condition_t cond;
|
||||
hsa_signal_value_t value;
|
||||
hsa_amd_signal_handler handler;
|
||||
void* arg;
|
||||
HsaEvent* hsa_event; //!< A list of HSA events for KFD wait
|
||||
uint64_t age; //!< The age list for KFD wait
|
||||
|
||||
AsyncEventItem() : signal{0}, cond(HSA_SIGNAL_CONDITION_EQ), value(0),
|
||||
handler(nullptr), arg(nullptr), hsa_event(nullptr), age(0) {}
|
||||
|
||||
AsyncEventItem(hsa_signal_t sig, hsa_signal_condition_t c, hsa_signal_value_t val,
|
||||
hsa_amd_signal_handler h, void* a)
|
||||
: signal(sig), cond(c), value(val), handler(h), arg(a),
|
||||
hsa_event(nullptr), age(0) {}
|
||||
|
||||
AsyncEventItem(const AsyncEventItem& other)
|
||||
: signal(other.signal), cond(other.cond), value(other.value),
|
||||
handler(other.handler), arg(other.arg), hsa_event(other.hsa_event), age(other.age) {}
|
||||
|
||||
void init(hsa_signal_t sig, hsa_signal_condition_t c, hsa_signal_value_t v, hsa_amd_signal_handler h, void* a) {
|
||||
signal = sig;
|
||||
cond = c;
|
||||
value = v;
|
||||
handler = h;
|
||||
arg = a;
|
||||
}
|
||||
// Helper operator to convert signal to Signal* for easier access
|
||||
Signal* operator->() {
|
||||
if (signal.handle == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return core::Signal::Convert(signal);
|
||||
}
|
||||
};
|
||||
|
||||
class AsyncEventsPool : private BaseShared {
|
||||
public:
|
||||
AsyncEventsPool() : block_size_(preallocblocks_ * minblock_) {}
|
||||
~AsyncEventsPool() { clear(); }
|
||||
|
||||
AsyncEventItem* alloc();
|
||||
void free(AsyncEventItem* item);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
static const size_t minblock_ = 4096 / sizeof(AsyncEventItem);
|
||||
static const size_t preallocblocks_ = 512;
|
||||
static const size_t maxblocksize_ = 1ULL << 28;
|
||||
HybridMutex lock_;
|
||||
std::vector<AsyncEventItem*> free_list_;
|
||||
std::vector<std::pair<void*, size_t>> block_list_;
|
||||
size_t block_size_;
|
||||
};
|
||||
// New concurrent events structure using lock-free queue
|
||||
struct ConcurrentAsyncEvents {
|
||||
ConcurrentAsyncEvents() {}
|
||||
|
||||
void PushBack(hsa_signal_t signal, hsa_signal_condition_t cond,
|
||||
hsa_signal_value_t value, hsa_amd_signal_handler handler, void* arg);
|
||||
|
||||
void Clear();
|
||||
|
||||
size_t Size();
|
||||
|
||||
bool empty() { return event_queue_.empty(); }
|
||||
|
||||
//! Get all events for processing
|
||||
bool GetAllEvents(std::vector<AsyncEventItem>& all_events);
|
||||
|
||||
//! Get single event for processing
|
||||
bool GetEvent(AsyncEventItem& event);
|
||||
|
||||
//! Add events back to queue (for events that need to be kept)
|
||||
void AddEventsBack(const std::vector<AsyncEventItem>& events);
|
||||
private:
|
||||
//AsyncEventItem Queue
|
||||
::rocr::MPSCQueue<AsyncEventItem*> event_queue_;
|
||||
AsyncEventsPool asyncEventPool_;
|
||||
};
|
||||
|
||||
struct PrefetchRange;
|
||||
typedef std::map<uintptr_t, PrefetchRange> prefetch_map_t;
|
||||
|
||||
@@ -747,8 +831,10 @@ class Runtime {
|
||||
struct AsyncEventsInfo {
|
||||
AsyncEventsControl control;
|
||||
AsyncEvents events;
|
||||
AsyncEvents new_events;
|
||||
ConcurrentAsyncEvents new_events;
|
||||
bool monitor_exceptions;
|
||||
|
||||
AsyncEventsInfo() : control(), events(), new_events(), monitor_exceptions(false) {}
|
||||
};
|
||||
|
||||
struct AsyncEventsInfo asyncSignals_;
|
||||
|
||||
@@ -372,7 +372,6 @@ hsa_signal_value_t InterruptSignal::CasAcqRel(hsa_signal_value_t expected,
|
||||
}
|
||||
/// @brief Notify driver of signal value change if necessary.
|
||||
void InterruptSignal::SetEvent() {
|
||||
std::atomic_signal_fence(std::memory_order_seq_cst);
|
||||
if (InWaiting()) HSAKMT_CALL(hsaKmtSetEvent(event_));
|
||||
}
|
||||
|
||||
|
||||
@@ -832,7 +832,6 @@ hsa_status_t Runtime::SetAsyncSignalHandler(hsa_signal_t signal,
|
||||
hsa_signal_value_t value,
|
||||
hsa_amd_signal_handler handler,
|
||||
void* arg) {
|
||||
|
||||
struct AsyncEventsInfo* asyncInfo = &asyncSignals_;
|
||||
int priority = runtime_singleton_->flag().async_events_thread_priority();
|
||||
|
||||
@@ -847,7 +846,6 @@ hsa_status_t Runtime::SetAsyncSignalHandler(hsa_signal_t signal,
|
||||
}
|
||||
}
|
||||
|
||||
ScopedAcquire<HybridMutex> scope_lock(&asyncInfo->control.lock);
|
||||
|
||||
// Lazy initializer
|
||||
if (asyncInfo->control.async_events_thread_ == NULL) {
|
||||
@@ -1886,26 +1884,19 @@ void Runtime::AsyncEventsLoop(void* _eventsInfo) {
|
||||
// Insert new signals and find plain functions
|
||||
typedef std::pair<void (*)(void*), void*> func_arg_t;
|
||||
std::vector<func_arg_t> functions;
|
||||
{
|
||||
ScopedAcquire<HybridMutex> scope_lock(&async_events_control_.lock);
|
||||
for (size_t i = 0; i < new_async_events_.Size(); i++) {
|
||||
if (new_async_events_.signal_[i].handle == 0) {
|
||||
functions.push_back(
|
||||
func_arg_t((void (*)(void*))new_async_events_.handler_[i],
|
||||
new_async_events_.arg_[i]));
|
||||
continue;
|
||||
}
|
||||
async_events_.PushBack(
|
||||
new_async_events_.signal_[i], new_async_events_.cond_[i],
|
||||
new_async_events_.value_[i], new_async_events_.handler_[i],
|
||||
new_async_events_.arg_[i]);
|
||||
std::vector<AsyncEventItem> new_events;
|
||||
new_async_events_.GetAllEvents(new_events);
|
||||
for (const auto& event : new_events) {
|
||||
if (event.signal.handle == 0) {
|
||||
functions.push_back(func_arg_t((void (*)(void*))event.handler, event.arg));
|
||||
continue;
|
||||
}
|
||||
new_async_events_.Clear();
|
||||
async_events_.PushBack(event.signal, event.cond, event.value, event.handler,event.arg);
|
||||
}
|
||||
|
||||
// Call plain functions
|
||||
for (size_t i = 0; i < functions.size(); i++)
|
||||
for (size_t i = 0; i < functions.size(); i++) {
|
||||
functions[i].first(functions[i].second);
|
||||
}
|
||||
functions.clear();
|
||||
}
|
||||
|
||||
@@ -1914,11 +1905,131 @@ void Runtime::AsyncEventsLoop(void* _eventsInfo) {
|
||||
hsa_signal_handle(async_events_.signal_[i])->Release();
|
||||
async_events_.Clear();
|
||||
|
||||
for (size_t i = 0; i < new_async_events_.Size(); i++)
|
||||
hsa_signal_handle(new_async_events_.signal_[i])->Release();
|
||||
std::vector<AsyncEventItem> remaining_events;
|
||||
new_async_events_.GetAllEvents(remaining_events);
|
||||
for (const auto& event : remaining_events) {
|
||||
if (event.signal.handle != 0) {
|
||||
hsa_signal_handle(event.signal)->Release();
|
||||
}
|
||||
}
|
||||
new_async_events_.Clear();
|
||||
}
|
||||
|
||||
void Runtime::AsyncEventsPool::clear() {
|
||||
ifdebug {
|
||||
size_t capacity = 0;
|
||||
for (auto& block : block_list_) capacity += block.second;
|
||||
if (capacity != free_list_.size())
|
||||
debug_print("Warning: Resource leak detected by AsyncEventsPool, %ld items leaked.\n",
|
||||
capacity - free_list_.size());
|
||||
}
|
||||
|
||||
for (auto& block : block_list_) free_()(block.first);
|
||||
block_list_.clear();
|
||||
free_list_.clear();
|
||||
}
|
||||
|
||||
Runtime::AsyncEventItem* Runtime::AsyncEventsPool::alloc() {
|
||||
ScopedAcquire<HybridMutex> lock(&lock_);
|
||||
if (free_list_.empty()) {
|
||||
AsyncEventItem* block = reinterpret_cast<AsyncEventItem*>(
|
||||
allocate_()(block_size_ * sizeof(AsyncEventItem), __alignof(AsyncEventItem), core::MemoryRegion::AllocateNonPaged, 0));
|
||||
if (block == nullptr) {
|
||||
block_size_ = minblock_;
|
||||
block = reinterpret_cast<AsyncEventItem*>(
|
||||
allocate_()(block_size_ * sizeof(AsyncEventItem), __alignof(AsyncEventItem), core::MemoryRegion::AllocateNonPaged, 0));
|
||||
if (block == nullptr) throw std::bad_alloc();
|
||||
}
|
||||
|
||||
MAKE_NAMED_SCOPE_GUARD(throwGuard, [&]() { free_()(block); });
|
||||
block_list_.push_back(std::make_pair(block, block_size_));
|
||||
throwGuard.Dismiss();
|
||||
|
||||
for (int i = 0; i < block_size_; i++) {
|
||||
free_list_.push_back(&block[i]);
|
||||
}
|
||||
if (block_size_ > maxblocksize_)
|
||||
block_size_ *= 2;
|
||||
}
|
||||
AsyncEventItem* ret = free_list_.back();
|
||||
new (ret) AsyncEventItem();
|
||||
free_list_.pop_back();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Runtime::AsyncEventsPool::free(AsyncEventItem* ptr) {
|
||||
if (ptr == nullptr) return;
|
||||
|
||||
ptr->~AsyncEventItem();
|
||||
ScopedAcquire<HybridMutex> lock(&lock_);
|
||||
|
||||
ifdebug {
|
||||
bool valid = false;
|
||||
for (auto& block : block_list_) {
|
||||
if ((block.first <= ptr) &&
|
||||
(uintptr_t(ptr) < uintptr_t(block.first) + block.second * sizeof(AsyncEventItem))) {
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(valid && "Object does not belong to pool.");
|
||||
}
|
||||
free_list_.push_back(ptr);
|
||||
}
|
||||
void Runtime::ConcurrentAsyncEvents::PushBack(hsa_signal_t signal,
|
||||
hsa_signal_condition_t cond,
|
||||
hsa_signal_value_t value,
|
||||
hsa_amd_signal_handler handler, void* arg) {
|
||||
// Allocate memory for the new event item
|
||||
AsyncEventItem* item = asyncEventPool_.alloc();
|
||||
item->init(signal, cond, value, handler, arg);
|
||||
event_queue_.enqueue(item);
|
||||
}
|
||||
|
||||
void Runtime::ConcurrentAsyncEvents::Clear() {
|
||||
// Dequeue all items to clear the queue
|
||||
while (!event_queue_.empty()) {
|
||||
AsyncEventItem* item = event_queue_.dequeue();
|
||||
asyncEventPool_.free(item);
|
||||
}
|
||||
asyncEventPool_.clear();
|
||||
}
|
||||
|
||||
bool Runtime::ConcurrentAsyncEvents::GetEvent(AsyncEventItem& event) {
|
||||
AsyncEventItem* item = event_queue_.dequeue();
|
||||
if (item != nullptr) {
|
||||
event = *item;
|
||||
asyncEventPool_.free(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Runtime::ConcurrentAsyncEvents::GetAllEvents(std::vector<AsyncEventItem>& all_events) {
|
||||
AsyncEventItem* item = nullptr;
|
||||
while (!event_queue_.empty()) {
|
||||
item = event_queue_.dequeue();
|
||||
if (item == nullptr) {
|
||||
return false;
|
||||
}
|
||||
all_events.emplace_back(*item);
|
||||
asyncEventPool_.free(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Runtime::ConcurrentAsyncEvents::AddEventsBack(const std::vector<AsyncEventItem>& events) {
|
||||
for (const auto& event : events) {
|
||||
AsyncEventItem* item = asyncEventPool_.alloc();
|
||||
*item = event;
|
||||
event_queue_.enqueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
size_t Runtime::ConcurrentAsyncEvents::Size() {
|
||||
return event_queue_.size();
|
||||
}
|
||||
|
||||
void Runtime::BindErrorHandlers() {
|
||||
if (!core::g_use_interrupt_wait || gpu_agents_.empty()) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
************************************************************************************************************************
|
||||
*
|
||||
* Copyright (C) 2007-2022 Advanced Micro Devices, Inc. All rights reserved.
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
***********************************************************************************************************************/
|
||||
|
||||
#ifndef HSA_RUNTIME_CORE_UTIL_MPSC_QUEUE_HPP_
|
||||
#define HSA_RUNTIME_CORE_UTIL_MPSC_QUEUE_HPP_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#include <cassert>
|
||||
|
||||
/*
|
||||
* Lock-free Multi-Producer Single-Consumer (MPSC) queue.
|
||||
*
|
||||
* Algorithm:
|
||||
* - Based on a classic singly-linked list with a dummy stub node.
|
||||
* - Producers:
|
||||
* 1. Allocate & construct a node (value constructed in-place).
|
||||
* 2. Set node->next = nullptr.
|
||||
* 3. prev = tail_.exchange(node, acq_rel).
|
||||
* 4. prev->next.store(node, release). (Publishes the node to the consumer.)
|
||||
* - Consumer (single thread):
|
||||
* 1. Reads head_->next (acquire).
|
||||
* 2. If null -> empty.
|
||||
* 3. Else claim next, move its value out, advance head_, delete old dummy.
|
||||
*
|
||||
* Progress:
|
||||
* - Enqueue is wait-free for producers (bounded steps, no loops).
|
||||
* - Dequeue is lock-free (O(1); waits only if empty).
|
||||
*
|
||||
* Thread-safety:
|
||||
* - Multiple threads may call enqueue() concurrently.
|
||||
* - Exactly ONE thread may call any of: dequeue(), dequeue_batch(), clear(), destructor.
|
||||
*
|
||||
* Memory ordering rationale:
|
||||
* - tail_.exchange(..., acq_rel) pairs with consumer's acquire load of next pointer.
|
||||
* - The prev->next.store(..., release) publishes the new node after its value is fully constructed.
|
||||
* - Consumer's next.load(acquire) ensures it sees a fully initialized node.
|
||||
*
|
||||
* Custom Allocator:
|
||||
* - Alloc template parameter (defaults std::allocator<T>).
|
||||
* - Rebound to internal Node type.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace rocr {
|
||||
|
||||
template<typename T, typename Alloc = std::allocator<T>>
|
||||
class MPSCQueue {
|
||||
private:
|
||||
struct Node {
|
||||
std::atomic<Node*> next;
|
||||
T value;
|
||||
|
||||
template<typename... Args>
|
||||
explicit Node(Args&&... args)
|
||||
: next(nullptr), value(std::forward<Args>(args)...) {}
|
||||
// Dummy node ctor (no value)
|
||||
Node() : next(nullptr), value() {}
|
||||
};
|
||||
|
||||
using NodeAlloc = typename std::allocator_traits<Alloc>::template rebind_alloc<Node>;
|
||||
using NodeAllocTraits = std::allocator_traits<NodeAlloc>;
|
||||
|
||||
// Cache line padding to reduce false-sharing between head & tail hot fields.
|
||||
struct alignas(64) PaddedAtomicPtr {
|
||||
std::atomic<Node*> ptr;
|
||||
char pad[64 - sizeof(std::atomic<Node*>) > 0 ? 64 - sizeof(std::atomic<Node*>) : 1];
|
||||
};
|
||||
|
||||
public:
|
||||
MPSCQueue()
|
||||
: alloc_()
|
||||
{
|
||||
Node* stub = create_node_stub();
|
||||
head_.ptr.store(stub, std::memory_order_relaxed);
|
||||
tail_.ptr.store(stub, std::memory_order_relaxed);
|
||||
qsize_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
explicit MPSCQueue(const Alloc& alloc)
|
||||
: alloc_(alloc)
|
||||
{
|
||||
Node* stub = create_node_stub();
|
||||
head_.ptr.store(stub, std::memory_order_relaxed);
|
||||
tail_.ptr.store(stub, std::memory_order_relaxed);
|
||||
qsize_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Non-copyable / non-movable (simplify invariants)
|
||||
MPSCQueue(const MPSCQueue&) = delete;
|
||||
MPSCQueue& operator=(const MPSCQueue&) = delete;
|
||||
MPSCQueue(MPSCQueue&&) = delete;
|
||||
MPSCQueue& operator=(MPSCQueue&&) = delete;
|
||||
|
||||
~MPSCQueue() {
|
||||
clear(); // drains and deletes any remaining nodes
|
||||
Node* stub = head_.ptr.load(std::memory_order_relaxed);
|
||||
if (stub) {
|
||||
destroy_node(stub);
|
||||
}
|
||||
qsize_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Enqueue by const reference
|
||||
inline void enqueue(const T& value) {
|
||||
emplace(value);
|
||||
}
|
||||
|
||||
// Enqueue by rvalue
|
||||
inline void enqueue(T&& value) {
|
||||
emplace(std::move(value));
|
||||
}
|
||||
|
||||
// Perfect-forwarding construction
|
||||
template<typename... Args>
|
||||
inline void emplace(Args&&... args) {
|
||||
Node* n = allocate_node(std::forward<Args>(args)...);
|
||||
// Publish:
|
||||
Node* prev = tail_.ptr.exchange(n, std::memory_order_acq_rel);
|
||||
// Link from previous tail
|
||||
prev->next.store(n, std::memory_order_release);
|
||||
qsize_.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Try dequeue single element; returns false if empty
|
||||
inline T dequeue() {
|
||||
Node* head = head_.ptr.load(std::memory_order_relaxed);
|
||||
Node* next = head->next.load(std::memory_order_acquire);
|
||||
if (!next) {
|
||||
return nullptr; // empty
|
||||
}
|
||||
// Move value out
|
||||
T out = std::move(next->value);
|
||||
// Advance head
|
||||
head_.ptr.store(next, std::memory_order_relaxed);
|
||||
// Old head was dummy
|
||||
destroy_node(head);
|
||||
qsize_.fetch_sub(1, std::memory_order_acq_rel);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Non-blocking peek (observes front without removing). Not linearizable with concurrent enqueues,
|
||||
// but safe for single-consumer inspection.
|
||||
inline bool peek(T& out) const {
|
||||
Node* head = head_.ptr.load(std::memory_order_relaxed);
|
||||
Node* next = head->next.load(std::memory_order_acquire);
|
||||
if (!next) return false;
|
||||
out = next->value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Batch dequeue up to max_items. Returns actual count.
|
||||
inline size_t dequeue_batch(std::vector<T>& out) {
|
||||
size_t count = 0;
|
||||
while (1) {
|
||||
Node* head = head_.ptr.load(std::memory_order_relaxed);
|
||||
Node* next = head->next.load(std::memory_order_acquire);
|
||||
if (!next) break;
|
||||
out.emplace_back(std::move(next->value));
|
||||
head_.ptr.store(next, std::memory_order_relaxed);
|
||||
destroy_node(head);
|
||||
++count;
|
||||
}
|
||||
if (count) {
|
||||
qsize_.fetch_sub(count, std::memory_order_acq_rel);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// empty check (safe for single consumer scenario)
|
||||
inline bool empty() const {
|
||||
return qsize_.load(std::memory_order_acquire) == 0;
|
||||
}
|
||||
|
||||
// Drain everything
|
||||
inline void clear() {
|
||||
T dummy;
|
||||
do {
|
||||
dummy = dequeue();
|
||||
} while (dummy != nullptr);
|
||||
}
|
||||
|
||||
inline size_t size() const {
|
||||
return qsize_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
private:
|
||||
NodeAlloc alloc_;
|
||||
PaddedAtomicPtr head_;
|
||||
PaddedAtomicPtr tail_;
|
||||
alignas(64) std::atomic<size_t> qsize_;
|
||||
|
||||
Node* create_node_stub() {
|
||||
Node* n = NodeAllocTraits::allocate(alloc_, 1);
|
||||
try {
|
||||
NodeAllocTraits::construct(alloc_, n);
|
||||
} catch (...) {
|
||||
NodeAllocTraits::deallocate(alloc_, n, 1);
|
||||
throw;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
Node* allocate_node(Args&&... args) {
|
||||
Node* n = NodeAllocTraits::allocate(alloc_, 1);
|
||||
try {
|
||||
NodeAllocTraits::construct(alloc_, n, std::forward<Args>(args)...);
|
||||
} catch (...) {
|
||||
NodeAllocTraits::deallocate(alloc_, n, 1);
|
||||
throw;
|
||||
}
|
||||
n->next.store(nullptr, std::memory_order_relaxed);
|
||||
return n;
|
||||
}
|
||||
|
||||
void destroy_node(Node* n) {
|
||||
NodeAllocTraits::destroy(alloc_, n);
|
||||
NodeAllocTraits::deallocate(alloc_, n, 1);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
#endif // HSA_RUNTIME_CORE_UTIL_MPSC_QUEUE_HPP_
|
||||
Reference in New Issue
Block a user