blob: c74d3f1e196607782410bff56f7dfa20ef9da5a3 [file] [log] [blame]
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +00001//===--------------------- Scheduler.h ------------------------*- C++ -*-===//
2//
3// The LLVM Compiler Infrastructure
4//
5// This file is distributed under the University of Illinois Open Source
6// License. See LICENSE.TXT for details.
7//
8//===----------------------------------------------------------------------===//
9/// \file
10///
11/// A scheduler for Processor Resource Units and Processor Resource Groups.
12///
13//===----------------------------------------------------------------------===//
14
15#ifndef LLVM_TOOLS_LLVM_MCA_SCHEDULER_H
16#define LLVM_TOOLS_LLVM_MCA_SCHEDULER_H
17
18#include "Instruction.h"
19#include "LSUnit.h"
20#include "llvm/ADT/DenseMap.h"
21#include "llvm/MC/MCSubtargetInfo.h"
22#include <map>
23
24namespace mca {
25
26class Backend;
Clement Courbet844f22d2018-03-13 13:11:01 +000027class DispatchUnit;
28
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +000029/// Used to notify the internal state of a processor resource.
30///
31/// A processor resource is available if it is not reserved, and there are
32/// available slots in the buffer. A processor resource is unavailable if it
33/// is either reserved, or the associated buffer is full. A processor resource
34/// with a buffer size of -1 is always available if it is not reserved.
35///
36/// Values of type ResourceStateEvent are returned by method
37/// ResourceState::isBufferAvailable(), which is used to query the internal
38/// state of a resource.
39///
40/// The naming convention for resource state events is:
41/// * Event names start with prefix RS_
42/// * Prefix RS_ is followed by a string describing the actual resource state.
43enum ResourceStateEvent {
44 RS_BUFFER_AVAILABLE,
45 RS_BUFFER_UNAVAILABLE,
46 RS_RESERVED
47};
48
49/// \brief A descriptor for processor resources.
50///
51/// Each object of class ResourceState is associated to a specific processor
52/// resource. There is an instance of this class for every processor resource
53/// defined by the scheduling model.
54/// A ResourceState dynamically tracks the availability of units of a processor
55/// resource. For example, the ResourceState of a ProcResGroup tracks the
56/// availability of resource units which are part of the group.
57///
58/// Internally, ResourceState uses a round-robin selector to identify
59/// which unit of the group shall be used next.
60class ResourceState {
Andrea Di Biagio0c541292018-03-10 16:55:07 +000061 // Index to the MCProcResourceDesc in the processor Model.
62 unsigned ProcResourceDescIndex;
Andrea Di Biagio4704f032018-03-20 12:25:54 +000063 // A resource mask. This is generated by the tool with the help of
64 // function `mca::createProcResourceMasks' (see Support.h).
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +000065 uint64_t ResourceMask;
66
67 // A ProcResource can specify a number of units. For the purpose of dynamic
68 // scheduling, a processor resource with more than one unit behaves like a
69 // group. This field has one bit set for every unit/resource that is part of
70 // the group.
71 // For groups, this field defaults to 'ResourceMask'. For non-group
72 // resources, the number of bits set in this mask is equivalent to the
73 // number of units (i.e. field 'NumUnits' in 'ProcResourceUnits').
74 uint64_t ResourceSizeMask;
75
76 // A simple round-robin selector for processor resources.
77 // Each bit of the mask identifies a sub resource within this group.
78 //
79 // As an example, lets assume that this ResourceState describes a
80 // processor resource group composed of the following three units:
81 // ResourceA -- 0b001
82 // ResourceB -- 0b010
83 // ResourceC -- 0b100
84 //
85 // Each unit is identified by a ResourceMask which always contains a
86 // single bit set. Field NextInSequenceMask is initially set to value
87 // 0xb111. That value is obtained by OR'ing the resource masks of
88 // processor resource that are part of the group.
89 //
90 // NextInSequenceMask -- 0b111
91 //
92 // Field NextInSequenceMask is used by the resource manager (i.e.
93 // an object of class ResourceManager) to select the "next available resource"
94 // from the set. The algorithm would prioritize resources with a bigger
95 // ResourceMask value.
96 //
97 // In this example, there are three resources in the set, and 'ResourceC'
98 // has the highest mask value. The round-robin selector would firstly select
99 // 'ResourceC', then 'ResourceB', and eventually 'ResourceA'.
100 //
101 // When a resource R is used, its corresponding bit is cleared from the set.
102 //
103 // Back to the example:
104 // If 'ResourceC' is selected, then the new value of NextInSequenceMask
105 // becomes 0xb011.
106 //
107 // When NextInSequenceMask becomes zero, it is reset to its original value
108 // (in this example, that value would be 0b111).
109 uint64_t NextInSequenceMask;
110
111 // Some instructions can only be issued on very specific pipeline resources.
112 // For those instructions, we know exactly which resource would be consumed
113 // without having to dynamically select it using field 'NextInSequenceMask'.
114 //
115 // The resource mask bit associated to the (statically) selected
116 // processor resource is still cleared from the 'NextInSequenceMask'.
117 // If that bit was already zero in NextInSequenceMask, then we update
118 // mask 'RemovedFromNextInSequence'.
119 //
120 // When NextInSequenceMask is reset back to its initial value, the algorithm
121 // removes any bits which are set in RemoveFromNextInSequence.
122 uint64_t RemovedFromNextInSequence;
123
124 // A mask of ready units.
125 uint64_t ReadyMask;
126
127 // Buffered resources will have this field set to a positive number bigger
128 // than 0. A buffered resource behaves like a separate reservation station
129 // implementing its own buffer for out-of-order execution.
130 // A buffer of 1 is for units that force in-order execution.
131 // A value of 0 is treated specially. In particular, a resource with
132 // A BufferSize = 0 is for an in-order issue/dispatch resource.
133 // That means, this resource is reserved starting from the dispatch event,
134 // until all the "resource cycles" are consumed after the issue event.
135 // While this resource is reserved, no other instruction may be dispatched.
136 int BufferSize;
137
138 // Available slots in the buffer (zero, if this is not a buffered resource).
139 unsigned AvailableSlots;
140
141 // Maximum number of buffer slots seen used during one cycle.
142 // This helps tracking dynamic dispatch stalls caused by the lack of
143 // entries in the scheduler's queue.
144 unsigned MaxUsedSlots;
145
146 // True if this is resource is currently unavailable.
147 // An instruction may "reserve" a resource for a number of cycles.
148 // During those cycles, the reserved resource cannot be used for other
149 // instructions, even if the ReadyMask is set.
150 bool Unavailable;
151
152 bool isSubResourceReady(uint64_t ID) const { return ReadyMask & ID; }
153
154 /// Returns the mask identifier of the next available resource in the set.
155 uint64_t getNextInSequence() const {
156 assert(NextInSequenceMask);
157 return llvm::PowerOf2Floor(NextInSequenceMask);
158 }
159
160 /// Returns the mask of the next available resource within the set,
161 /// and updates the resource selector.
162 void updateNextInSequence() {
163 NextInSequenceMask ^= getNextInSequence();
164 if (!NextInSequenceMask)
165 NextInSequenceMask = ResourceSizeMask;
166 }
167
168 uint64_t computeResourceSizeMaskForGroup(uint64_t ResourceMask) {
169 assert(llvm::countPopulation(ResourceMask) > 1);
170 return ResourceMask ^ llvm::PowerOf2Floor(ResourceMask);
171 }
172
173public:
Andrea Di Biagio0c541292018-03-10 16:55:07 +0000174 ResourceState(const llvm::MCProcResourceDesc &Desc, unsigned Index,
175 uint64_t Mask)
176 : ProcResourceDescIndex(Index), ResourceMask(Mask) {
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000177 bool IsAGroup = llvm::countPopulation(ResourceMask) > 1;
178 ResourceSizeMask = IsAGroup ? computeResourceSizeMaskForGroup(ResourceMask)
179 : ((1ULL << Desc.NumUnits) - 1);
180 NextInSequenceMask = ResourceSizeMask;
181 RemovedFromNextInSequence = 0;
182 ReadyMask = ResourceSizeMask;
183 BufferSize = Desc.BufferSize;
184 AvailableSlots = BufferSize == -1 ? 0U : static_cast<unsigned>(BufferSize);
185 MaxUsedSlots = 0;
186 Unavailable = false;
187 }
188
Andrea Di Biagio0c541292018-03-10 16:55:07 +0000189 unsigned getProcResourceID() const { return ProcResourceDescIndex; }
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000190 uint64_t getResourceMask() const { return ResourceMask; }
191 int getBufferSize() const { return BufferSize; }
192 unsigned getMaxUsedSlots() const { return MaxUsedSlots; }
193
194 bool isBuffered() const { return BufferSize > 0; }
195 bool isInOrder() const { return BufferSize == 1; }
196 bool isADispatchHazard() const { return BufferSize == 0; }
197 bool isReserved() const { return Unavailable; }
198
199 void setReserved() { Unavailable = true; }
200 void clearReserved() { Unavailable = false; }
201
202 // A resource is ready if it is not reserved, and if there are enough
203 // available units.
204 // If a resource is also a dispatch hazard, then we don't check if
205 // it is reserved because that check would always return true.
206 // A resource marked as "dispatch hazard" is always reserved at
207 // dispatch time. When this method is called, the assumption is that
208 // the user of this resource has been already dispatched.
209 bool isReady(unsigned NumUnits = 1) const {
210 return (!isReserved() || isADispatchHazard()) &&
211 llvm::countPopulation(ReadyMask) >= NumUnits;
212 }
213 bool isAResourceGroup() const {
214 return llvm::countPopulation(ResourceMask) > 1;
215 }
216
217 bool containsResource(uint64_t ID) const { return ResourceMask & ID; }
218
219 void markSubResourceAsUsed(uint64_t ID) {
220 assert(isSubResourceReady(ID));
221 ReadyMask ^= ID;
222 }
223
224 void releaseSubResource(uint64_t ID) {
225 assert(!isSubResourceReady(ID));
226 ReadyMask ^= ID;
227 }
228
229 unsigned getNumUnits() const {
230 return isAResourceGroup() ? 1U : llvm::countPopulation(ResourceSizeMask);
231 }
232
233 uint64_t selectNextInSequence();
234 void removeFromNextInSequence(uint64_t ID);
235
236 ResourceStateEvent isBufferAvailable() const {
237 if (isADispatchHazard() && isReserved())
238 return RS_RESERVED;
239 if (!isBuffered() || AvailableSlots)
240 return RS_BUFFER_AVAILABLE;
241 return RS_BUFFER_UNAVAILABLE;
242 }
243
244 void reserveBuffer() {
245 if (AvailableSlots)
246 AvailableSlots--;
247 unsigned UsedSlots = static_cast<unsigned>(BufferSize) - AvailableSlots;
248 MaxUsedSlots = std::max(MaxUsedSlots, UsedSlots);
249 }
250
251 void releaseBuffer() {
252 if (BufferSize > 0)
253 AvailableSlots++;
254 assert(AvailableSlots <= static_cast<unsigned>(BufferSize));
255 }
256
257#ifndef NDEBUG
258 void dump() const;
259#endif
260};
261
262/// \brief A resource unit identifier.
263///
264/// This is used to identify a specific processor resource unit using a pair
265/// of indices where the 'first' index is a processor resource mask, and the
266/// 'second' index is an index for a "sub-resource" (i.e. unit).
267typedef std::pair<uint64_t, uint64_t> ResourceRef;
268
Andrea Di Biagio0c541292018-03-10 16:55:07 +0000269// First: a MCProcResourceDesc index identifying a buffered resource.
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000270// Second: max number of buffer entries used in this resource.
Andrea Di Biagio0c541292018-03-10 16:55:07 +0000271typedef std::pair<unsigned, unsigned> BufferUsageEntry;
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000272
273/// A resource manager for processor resource units and groups.
274///
275/// This class owns all the ResourceState objects, and it is responsible for
276/// acting on requests from a Scheduler by updating the internal state of
277/// ResourceState objects.
278/// This class doesn't know about instruction itineraries and functional units.
279/// In future, it can be extended to support itineraries too through the same
280/// public interface.
281class ResourceManager {
282 // The resource manager owns all the ResourceState.
283 using UniqueResourceState = std::unique_ptr<ResourceState>;
284 llvm::SmallDenseMap<uint64_t, UniqueResourceState> Resources;
285
286 // Keeps track of which resources are busy, and how many cycles are left
287 // before those become usable again.
288 llvm::SmallDenseMap<ResourceRef, unsigned> BusyResources;
289
290 // A table to map processor resource IDs to processor resource masks.
291 llvm::SmallVector<uint64_t, 8> ProcResID2Mask;
292
293 // Adds a new resource state in Resources, as well as a new descriptor in
294 // ResourceDescriptor.
Andrea Di Biagio0c541292018-03-10 16:55:07 +0000295 void addResource(const llvm::MCProcResourceDesc &Desc, unsigned Index,
296 uint64_t Mask);
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000297
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000298 // Populate resource descriptors.
Andrea Di Biagio4704f032018-03-20 12:25:54 +0000299 void initialize(const llvm::MCSchedModel &SM);
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000300
301 // Returns the actual resource unit that will be used.
302 ResourceRef selectPipe(uint64_t ResourceID);
303
304 void use(ResourceRef RR);
305 void release(ResourceRef RR);
306
307 unsigned getNumUnits(uint64_t ResourceID) const {
308 assert(Resources.find(ResourceID) != Resources.end());
309 return Resources.find(ResourceID)->getSecond()->getNumUnits();
310 }
311
312 // Reserve a specific Resource kind.
313 void reserveBuffer(uint64_t ResourceID) {
314 assert(isBufferAvailable(ResourceID) ==
315 ResourceStateEvent::RS_BUFFER_AVAILABLE);
316 ResourceState &Resource = *Resources[ResourceID];
317 Resource.reserveBuffer();
318 }
319
320 void releaseBuffer(uint64_t ResourceID) {
321 Resources[ResourceID]->releaseBuffer();
322 }
323
324 ResourceStateEvent isBufferAvailable(uint64_t ResourceID) const {
325 const ResourceState &Resource = *Resources.find(ResourceID)->second;
326 return Resource.isBufferAvailable();
327 }
328
329 bool isReady(uint64_t ResourceID, unsigned NumUnits) const {
330 const ResourceState &Resource = *Resources.find(ResourceID)->second;
331 return Resource.isReady(NumUnits);
332 }
333
334public:
Andrea Di Biagio4704f032018-03-20 12:25:54 +0000335 ResourceManager(const llvm::MCSchedModel &SM)
336 : ProcResID2Mask(SM.getNumProcResourceKinds()) {
337 initialize(SM);
338 }
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000339
Andrea Di Biagio44bfcd22018-03-19 19:09:38 +0000340 // Returns RS_BUFFER_AVAILABLE if buffered resources are not reserved, and if
341 // there are enough available slots in the buffers.
Andrea Di Biagioe1a1da12018-03-13 13:58:02 +0000342 ResourceStateEvent
Andrea Di Biagio44bfcd22018-03-19 19:09:38 +0000343 canBeDispatched(const llvm::ArrayRef<uint64_t> Buffers) const;
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000344
Andrea Di Biagio44bfcd22018-03-19 19:09:38 +0000345 // Consume a slot in every buffered resource from array 'Buffers'. Resource
346 // units that are dispatch hazards (i.e. BufferSize=0) are marked as reserved.
347 void reserveBuffers(const llvm::ArrayRef<uint64_t> Buffers);
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000348
Andrea Di Biagio44bfcd22018-03-19 19:09:38 +0000349 // Release buffer entries previously allocated by method reserveBuffers.
350 void releaseBuffers(const llvm::ArrayRef<uint64_t> Buffers);
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000351
352 void reserveResource(uint64_t ResourceID) {
353 ResourceState &Resource = *Resources[ResourceID];
354 assert(!Resource.isReserved());
355 Resource.setReserved();
356 }
357
358 void releaseResource(uint64_t ResourceID) {
359 ResourceState &Resource = *Resources[ResourceID];
360 Resource.clearReserved();
361 }
362
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000363 // Returns true if all resources are in-order, and there is at least one
364 // resource which is a dispatch hazard (BufferSize = 0).
365 bool mustIssueImmediately(const InstrDesc &Desc);
366
367 bool canBeIssued(const InstrDesc &Desc) const;
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000368
369 void issueInstruction(
370 unsigned Index, const InstrDesc &Desc,
371 llvm::SmallVectorImpl<std::pair<ResourceRef, unsigned>> &Pipes);
372
373 void cycleEvent(llvm::SmallVectorImpl<ResourceRef> &ResourcesFreed);
374
375 void getBuffersUsage(std::vector<BufferUsageEntry> &Usage) const {
376 for (const std::pair<uint64_t, UniqueResourceState> &Resource : Resources) {
377 const ResourceState &RS = *Resource.second;
378 if (RS.isBuffered())
Andrea Di Biagio0c541292018-03-10 16:55:07 +0000379 Usage.emplace_back(std::pair<unsigned, unsigned>(RS.getProcResourceID(),
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000380 RS.getMaxUsedSlots()));
381 }
382 }
383
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000384#ifndef NDEBUG
385 void dump() const {
386 for (const std::pair<uint64_t, UniqueResourceState> &Resource : Resources)
387 Resource.second->dump();
388 }
389#endif
390}; // namespace mca
391
392/// Class Scheduler is responsible for issuing instructions to pipeline
393/// resources. Internally, it delegates to a ResourceManager the management of
394/// processor resources.
395/// This class is also responsible for tracking the progress of instructions
396/// from the dispatch stage, until the write-back stage.
397///
398/// An nstruction dispatched to the Scheduler is initially placed into either
399/// the 'WaitQueue' or the 'ReadyQueue' depending on the availability of the
400/// input operands. Instructions in the WaitQueue are ordered by instruction
401/// index. An instruction is moved from the WaitQueue to the ReadyQueue when
402/// register operands become available, and all memory dependencies are met.
403/// Instructions that are moved from the WaitQueue to the ReadyQueue transition
404/// from state 'IS_AVAILABLE' to state 'IS_READY'.
405///
406/// At the beginning of each cycle, the Scheduler checks if there are
407/// instructions in the WaitQueue that can be moved to the ReadyQueue. If the
408/// ReadyQueue is not empty, then older instructions from the queue are issued
409/// to the processor pipelines, and the underlying ResourceManager is updated
410/// accordingly. The ReadyQueue is ordered by instruction index to guarantee
411/// that the first instructions in the set are also the oldest.
412///
413/// An Instruction is moved from the ReadyQueue the `IssuedQueue` when it is
414/// issued to a (one or more) pipeline(s). This event also causes an instruction
415/// state transition (i.e. from state IS_READY, to state IS_EXECUTING).
416/// An Instruction leaves the IssuedQueue when it reaches the write-back stage.
417class Scheduler {
418 const llvm::MCSchedModel &SM;
419
420 // Hardware resources that are managed by this scheduler.
421 std::unique_ptr<ResourceManager> Resources;
422 std::unique_ptr<LSUnit> LSU;
423
424 // The Backend gets notified when instructions are ready/issued/executed.
Clement Courbet844f22d2018-03-13 13:11:01 +0000425 Backend *const Owner;
426
427 // The dispatch unit gets notified when instructions are executed.
428 DispatchUnit *DU;
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000429
430 using QueueEntryTy = std::pair<unsigned, Instruction *>;
431 std::map<unsigned, Instruction *> WaitQueue;
432 std::map<unsigned, Instruction *> ReadyQueue;
433 std::map<unsigned, Instruction *> IssuedQueue;
434
435 void notifyInstructionIssued(
436 unsigned Index,
437 const llvm::ArrayRef<std::pair<ResourceRef, unsigned>> &Used);
438 void notifyInstructionExecuted(unsigned Index);
439 void notifyInstructionReady(unsigned Index);
440 void notifyResourceAvailable(const ResourceRef &RR);
441
442 /// Issue instructions from the ready queue by giving priority to older
443 /// instructions.
444 void issue();
445
446 /// Issue an instruction without updating the ready queue.
Andrea Di Biagio44bfcd22018-03-19 19:09:38 +0000447 void issueInstruction(Instruction &IS, unsigned InstrIndex);
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000448 void updatePendingQueue();
449 void updateIssuedQueue();
450
451public:
452 Scheduler(Backend *B, const llvm::MCSchedModel &Model, unsigned LoadQueueSize,
453 unsigned StoreQueueSize, bool AssumeNoAlias)
454 : SM(Model), Resources(llvm::make_unique<ResourceManager>(SM)),
455 LSU(llvm::make_unique<LSUnit>(LoadQueueSize, StoreQueueSize,
456 AssumeNoAlias)),
457 Owner(B) {}
458
Clement Courbet844f22d2018-03-13 13:11:01 +0000459 void setDispatchUnit(DispatchUnit *DispUnit) { DU = DispUnit; }
460
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000461 /// Scheduling events.
462 ///
463 /// The DispatchUnit is responsible for querying the Scheduler before
464 /// dispatching new instructions. Queries are performed through method
465 /// `Scheduler::CanBeDispatched`, which returns an instance of this enum to
466 /// tell if the dispatch would fail or not. If scheduling resources are
467 /// available, and the instruction can be dispatched, then the query returns
468 /// HWS_AVAILABLE. A values different than HWS_AVAILABLE means that the
469 /// instruction cannot be dispatched during this cycle.
470 ///
471 /// Each event name starts with prefix "HWS_", and it is followed by
472 /// a substring which describes the reason why the Scheduler was unavailable
473 /// (or "AVAILABLE" if the instruction is allowed to be dispatched).
474 ///
475 /// HWS_QUEUE_UNAVAILABLE is returned if there are not enough available slots
476 /// in the scheduler's queue. That means, one (or more) buffered resources
477 /// consumed by the instruction were full.
478 ///
479 /// HWS_LD_QUEUE_UNAVAILABLE is returned when an instruction 'mayLoad', and
480 /// the load queue in the load/store unit (implemented by class LSUnit) is
481 /// full. Similarly, HWS_ST_QUEUE_UNAVAILABLE is returned when the store
482 /// queue is full, and the instruction to be dispatched 'mayStore'.
483 ///
484 /// HWS_DISPATCH_GROUP_RESTRICTION is only returned in special cases where the
485 /// instruction consumes an in-order issue/dispatch resource (i.e. a resource
486 /// with `BufferSize=0`), and the pipeline resource is not immediately
487 /// available.
488 enum Event {
489 HWS_AVAILABLE,
490 HWS_QUEUE_UNAVAILABLE,
491 HWS_DISPATCH_GROUP_RESTRICTION,
492 HWS_LD_QUEUE_UNAVAILABLE,
493 HWS_ST_QUEUE_UNAVAILABLE
494 };
495
496 Event canBeDispatched(const InstrDesc &Desc) const;
Andrea Di Biagio44bfcd22018-03-19 19:09:38 +0000497 void scheduleInstruction(unsigned Idx, Instruction &MCIS);
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000498
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000499 void cycleEvent(unsigned Cycle);
500
501 void getBuffersUsage(std::vector<BufferUsageEntry> &Usage) const {
502 Resources->getBuffersUsage(Usage);
503 }
504
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000505#ifndef NDEBUG
506 void dump() const;
507#endif
508};
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000509} // Namespace mca
510
511#endif