blob: f59f46e3bad590a55b23ba71732a95f6e2a864d9 [file] [log] [blame]
//===- subzero/src/IceThreading.h - Threading functions ---------*- C++ -*-===//
//
// The Subzero Code Generator
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file declares threading-related functions.
///
//===----------------------------------------------------------------------===//
#ifndef SUBZERO_SRC_ICETHREADING_H
#define SUBZERO_SRC_ICETHREADING_H
#include "IceDefs.h"
#include <condition_variable>
#include <mutex>
namespace Ice {
/// BoundedProducerConsumerQueue is a work queue that allows multiple
/// producers and multiple consumers. A producer adds entries using
/// blockingPush(), and may block if the queue is "full". A producer
/// uses notifyEnd() to indicate that no more entries will be added. A
/// consumer removes an item using blockingPop(), which will return
/// nullptr if notifyEnd() has been called and the queue is empty (it
/// never returns nullptr if the queue contained any items).
///
/// The MaxSize ctor arg controls the maximum size the queue can grow
/// to (subject to a hard limit of MaxStaticSize-1). The Sequential
/// arg indicates purely sequential execution in which the single
/// thread should never wait().
///
/// Two condition variables are used in the implementation.
/// GrewOrEnded signals a waiting worker that a producer has changed
/// the state of the queue. Shrunk signals a blocked producer that a
/// consumer has changed the state of the queue.
///
/// The methods begin with Sequential-specific code to be most clear.
/// The lock and condition variables are not used in the Sequential
/// case.
///
/// Internally, the queue is implemented as a circular array of size
/// MaxStaticSize, where the queue boundaries are denoted by the Front
/// and Back fields. Front==Back indicates an empty queue.
template <typename T, size_t MaxStaticSize = 128>
class BoundedProducerConsumerQueue {
BoundedProducerConsumerQueue() = delete;
BoundedProducerConsumerQueue(const BoundedProducerConsumerQueue &) = delete;
BoundedProducerConsumerQueue &
operator=(const BoundedProducerConsumerQueue &) = delete;
public:
BoundedProducerConsumerQueue(bool Sequential, size_t MaxSize = MaxStaticSize)
: MaxSize(std::min(MaxSize, MaxStaticSize)), Sequential(Sequential) {}
void blockingPush(T *Item) {
{
std::unique_lock<GlobalLockType> L(Lock);
// If the work queue is already "full", wait for a consumer to
// grab an element and shrink the queue.
Shrunk.wait(L, [this] { return size() < MaxSize || Sequential; });
push(Item);
}
GrewOrEnded.notify_one();
}
T *blockingPop() {
T *Item = nullptr;
bool ShouldNotifyProducer = false;
{
std::unique_lock<GlobalLockType> L(Lock);
GrewOrEnded.wait(L, [this] { return IsEnded || !empty() || Sequential; });
if (!empty()) {
Item = pop();
ShouldNotifyProducer = !IsEnded;
}
}
if (ShouldNotifyProducer)
Shrunk.notify_one();
return Item;
}
void notifyEnd() {
{
std::lock_guard<GlobalLockType> L(Lock);
IsEnded = true;
}
GrewOrEnded.notify_all();
}
private:
const static size_t MaxStaticSizeMask = MaxStaticSize - 1;
static_assert(!(MaxStaticSize & (MaxStaticSize - 1)),
"MaxStaticSize must be a power of 2");
ICE_CACHELINE_BOUNDARY;
/// WorkItems and Lock are read/written by all.
T *WorkItems[MaxStaticSize];
ICE_CACHELINE_BOUNDARY;
/// Lock guards access to WorkItems, Front, Back, and IsEnded.
GlobalLockType Lock;
ICE_CACHELINE_BOUNDARY;
/// GrewOrEnded is written by the producers and read by the
/// consumers. It is notified (by the producer) when something is
/// added to the queue, in case consumers are waiting for a non-empty
/// queue.
std::condition_variable GrewOrEnded;
/// Back is the index into WorkItems[] of where the next element will
/// be pushed. (More precisely, Back&MaxStaticSize is the index.)
/// It is written by the producers, and read by all via size() and
/// empty().
size_t Back = 0;
ICE_CACHELINE_BOUNDARY;
/// Shrunk is notified (by the consumer) when something is removed
/// from the queue, in case a producer is waiting for the queue to
/// drop below maximum capacity. It is written by the consumers and
/// read by the producers.
std::condition_variable Shrunk;
/// Front is the index into WorkItems[] of the oldest element,
/// i.e. the next to be popped. (More precisely Front&MaxStaticSize
/// is the index.) It is written by the consumers, and read by all
/// via size() and empty().
size_t Front = 0;
ICE_CACHELINE_BOUNDARY;
/// MaxSize and Sequential are read by all and written by none.
const size_t MaxSize;
const bool Sequential;
/// IsEnded is read by the consumers, and only written once by the
/// producer.
bool IsEnded = false;
/// The lock must be held when the following methods are called.
bool empty() const { return Front == Back; }
size_t size() const { return Back - Front; }
void push(T *Item) {
WorkItems[Back++ & MaxStaticSizeMask] = Item;
assert(size() <= MaxStaticSize);
}
T *pop() {
assert(!empty());
return WorkItems[Front++ & MaxStaticSizeMask];
}
};
/// EmitterWorkItem is a simple wrapper around a pointer that
/// represents a work item to be emitted, i.e. a function or a set of
/// global declarations and initializers, and it includes a sequence
/// number so that work items can be emitted in a particular order for
/// deterministic output. It acts like an interface class, but instead
/// of making the classes of interest inherit from EmitterWorkItem, it
/// wraps pointers to these classes. Some space is wasted compared to
/// storing the pointers in a union, but not too much due to the work
/// granularity.
class EmitterWorkItem {
EmitterWorkItem() = delete;
EmitterWorkItem(const EmitterWorkItem &) = delete;
EmitterWorkItem &operator=(const EmitterWorkItem &) = delete;
public:
/// ItemKind can be one of the following:
///
/// WI_Nop: No actual work. This is a placeholder to maintain
/// sequence numbers in case there is a translation error.
///
/// WI_GlobalInits: A list of global declarations and initializers.
///
/// WI_Asm: A function that has already had emitIAS() called on it.
/// The work is transferred via the Assembler buffer, and the
/// originating Cfg has been deleted (to recover lots of memory).
///
/// WI_Cfg: A Cfg that has not yet had emit() or emitIAS() called on
/// it. This is only used as a debugging configuration when we want
/// to emit "readable" assembly code, possibly annotated with
/// liveness and other information only available in the Cfg and not
/// in the Assembler buffer.
enum ItemKind { WI_Nop, WI_GlobalInits, WI_Asm, WI_Cfg };
/// Constructor for a WI_Nop work item.
explicit EmitterWorkItem(uint32_t Seq);
/// Constructor for a WI_GlobalInits work item.
EmitterWorkItem(uint32_t Seq, VariableDeclarationList *D);
/// Constructor for a WI_Asm work item.
EmitterWorkItem(uint32_t Seq, Assembler *A);
/// Constructor for a WI_Cfg work item.
EmitterWorkItem(uint32_t Seq, Cfg *F);
uint32_t getSequenceNumber() const { return Sequence; }
ItemKind getKind() const { return Kind; }
void setGlobalInits(std::unique_ptr<VariableDeclarationList> GloblInits);
std::unique_ptr<VariableDeclarationList> getGlobalInits();
std::unique_ptr<Assembler> getAsm();
std::unique_ptr<Cfg> getCfg();
private:
const uint32_t Sequence;
const ItemKind Kind;
std::unique_ptr<VariableDeclarationList> GlobalInits;
std::unique_ptr<Assembler> Function;
std::unique_ptr<Cfg> RawFunc;
};
} // end of namespace Ice
#endif // SUBZERO_SRC_ICETHREADING_H