blob: c56a2792cd319ece8095b7f7c1efc367ec4e3b2f [file] [log] [blame]
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +00001//===--------------------- InstrBuilder.cpp ---------------------*- 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/// This file implements the InstrBuilder interface.
12///
13//===----------------------------------------------------------------------===//
14
15#include "InstrBuilder.h"
16#include "llvm/MC/MCInst.h"
17#include "llvm/Support/Debug.h"
18#include "llvm/Support/raw_ostream.h"
19
20#define DEBUG_TYPE "llvm-mca"
21
22namespace mca {
23
24using namespace llvm;
25
Andrea Di Biagio94fafdf2018-03-24 16:05:36 +000026static void initializeUsedResources(InstrDesc &ID,
27 const MCSchedClassDesc &SCDesc,
28 const MCSubtargetInfo &STI,
29 ArrayRef<uint64_t> ProcResourceMasks) {
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +000030 const MCSchedModel &SM = STI.getSchedModel();
31
32 // Populate resources consumed.
33 using ResourcePlusCycles = std::pair<uint64_t, ResourceUsage>;
34 std::vector<ResourcePlusCycles> Worklist;
35 for (unsigned I = 0, E = SCDesc.NumWriteProcResEntries; I < E; ++I) {
36 const MCWriteProcResEntry *PRE = STI.getWriteProcResBegin(&SCDesc) + I;
37 const MCProcResourceDesc &PR = *SM.getProcResource(PRE->ProcResourceIdx);
38 uint64_t Mask = ProcResourceMasks[PRE->ProcResourceIdx];
39 if (PR.BufferSize != -1)
40 ID.Buffers.push_back(Mask);
41 CycleSegment RCy(0, PRE->Cycles, false);
42 Worklist.emplace_back(ResourcePlusCycles(Mask, ResourceUsage(RCy)));
43 }
44
45 // Sort elements by mask popcount, so that we prioritize resource units over
46 // resource groups, and smaller groups over larger groups.
Mandeep Singh Grang8db564e2018-04-01 21:24:53 +000047 llvm::sort(Worklist.begin(), Worklist.end(),
48 [](const ResourcePlusCycles &A, const ResourcePlusCycles &B) {
49 unsigned popcntA = countPopulation(A.first);
50 unsigned popcntB = countPopulation(B.first);
51 if (popcntA < popcntB)
52 return true;
53 if (popcntA > popcntB)
54 return false;
55 return A.first < B.first;
56 });
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +000057
58 uint64_t UsedResourceUnits = 0;
59
60 // Remove cycles contributed by smaller resources.
61 for (unsigned I = 0, E = Worklist.size(); I < E; ++I) {
62 ResourcePlusCycles &A = Worklist[I];
63 if (!A.second.size()) {
64 A.second.NumUnits = 0;
65 A.second.setReserved();
66 ID.Resources.emplace_back(A);
67 continue;
68 }
69
70 ID.Resources.emplace_back(A);
71 uint64_t NormalizedMask = A.first;
72 if (countPopulation(A.first) == 1) {
73 UsedResourceUnits |= A.first;
74 } else {
75 // Remove the leading 1 from the resource group mask.
76 NormalizedMask ^= PowerOf2Floor(NormalizedMask);
77 }
78
79 for (unsigned J = I + 1; J < E; ++J) {
80 ResourcePlusCycles &B = Worklist[J];
81 if ((NormalizedMask & B.first) == NormalizedMask) {
82 B.second.CS.Subtract(A.second.size());
83 if (countPopulation(B.first) > 1)
84 B.second.NumUnits++;
85 }
86 }
87 }
88
89 // A SchedWrite may specify a number of cycles in which a resource group
90 // is reserved. For example (on target x86; cpu Haswell):
91 //
92 // SchedWriteRes<[HWPort0, HWPort1, HWPort01]> {
93 // let ResourceCycles = [2, 2, 3];
94 // }
95 //
96 // This means:
97 // Resource units HWPort0 and HWPort1 are both used for 2cy.
98 // Resource group HWPort01 is the union of HWPort0 and HWPort1.
99 // Since this write touches both HWPort0 and HWPort1 for 2cy, HWPort01
100 // will not be usable for 2 entire cycles from instruction issue.
101 //
102 // On top of those 2cy, SchedWriteRes explicitly specifies an extra latency
103 // of 3 cycles for HWPort01. This tool assumes that the 3cy latency is an
104 // extra delay on top of the 2 cycles latency.
105 // During those extra cycles, HWPort01 is not usable by other instructions.
106 for (ResourcePlusCycles &RPC : ID.Resources) {
107 if (countPopulation(RPC.first) > 1 && !RPC.second.isReserved()) {
108 // Remove the leading 1 from the resource group mask.
109 uint64_t Mask = RPC.first ^ PowerOf2Floor(RPC.first);
110 if ((Mask & UsedResourceUnits) == Mask)
111 RPC.second.setReserved();
112 }
113 }
114
Andrea Di Biagio7b3d1622018-03-20 12:58:34 +0000115 DEBUG({
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000116 for (const std::pair<uint64_t, ResourceUsage> &R : ID.Resources)
117 dbgs() << "\t\tMask=" << R.first << ", cy=" << R.second.size() << '\n';
118 for (const uint64_t R : ID.Buffers)
119 dbgs() << "\t\tBuffer Mask=" << R << '\n';
Andrea Di Biagio7b3d1622018-03-20 12:58:34 +0000120 });
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000121}
122
123static void computeMaxLatency(InstrDesc &ID, const MCInstrDesc &MCDesc,
124 const MCSchedClassDesc &SCDesc,
125 const MCSubtargetInfo &STI) {
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000126 if (MCDesc.isCall()) {
127 // We cannot estimate how long this call will take.
128 // Artificially set an arbitrarily high latency (100cy).
Andrea Di Biagioc95a1302018-03-13 15:59:59 +0000129 ID.MaxLatency = 100U;
130 return;
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000131 }
132
Andrea Di Biagioc95a1302018-03-13 15:59:59 +0000133 int Latency = MCSchedModel::computeInstrLatency(STI, SCDesc);
134 // If latency is unknown, then conservatively assume a MaxLatency of 100cy.
135 ID.MaxLatency = Latency < 0 ? 100U : static_cast<unsigned>(Latency);
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000136}
137
138static void populateWrites(InstrDesc &ID, const MCInst &MCI,
139 const MCInstrDesc &MCDesc,
140 const MCSchedClassDesc &SCDesc,
141 const MCSubtargetInfo &STI) {
142 computeMaxLatency(ID, MCDesc, SCDesc, STI);
143
144 // Set if writes through this opcode may update super registers.
145 // TODO: on x86-64, a 4 byte write of a general purpose register always
146 // fully updates the super-register.
147 // More in general, (at least on x86) not all register writes perform
148 // a partial (super-)register update.
149 // For example, an AVX instruction that writes on a XMM register implicitly
150 // zeroes the upper half of every aliasing super-register.
151 //
152 // For now, we pessimistically assume that writes are all potentially
153 // partial register updates. This is a good default for most targets, execept
154 // for those like x86 which implement a special semantic for certain opcodes.
155 // At least on x86, this may lead to an inaccurate prediction of the
156 // instruction level parallelism.
157 bool FullyUpdatesSuperRegisters = false;
158
159 // Now Populate Writes.
160
161 // This algorithm currently works under the strong (and potentially incorrect)
162 // assumption that information related to register def/uses can be obtained
163 // from MCInstrDesc.
164 //
165 // However class MCInstrDesc is used to describe MachineInstr objects and not
166 // MCInst objects. To be more specific, MCInstrDesc objects are opcode
167 // descriptors that are automatically generated via tablegen based on the
168 // instruction set information available from the target .td files. That
169 // means, the number of (explicit) definitions according to MCInstrDesc always
170 // matches the cardinality of the `(outs)` set in tablegen.
171 //
172 // By constructions, definitions must appear first in the operand sequence of
173 // a MachineInstr. Also, the (outs) sequence is preserved (example: the first
174 // element in the outs set is the first operand in the corresponding
175 // MachineInstr). That's the reason why MCInstrDesc only needs to declare the
176 // total number of register definitions, and not where those definitions are
177 // in the machine operand sequence.
178 //
179 // Unfortunately, it is not safe to use the information from MCInstrDesc to
180 // also describe MCInst objects. An MCInst object can be obtained from a
181 // MachineInstr through a lowering step which may restructure the operand
182 // sequence (and even remove or introduce new operands). So, there is a high
183 // risk that the lowering step breaks the assumptions that register
184 // definitions are always at the beginning of the machine operand sequence.
185 //
186 // This is a fundamental problem, and it is still an open problem. Essentially
187 // we have to find a way to correlate def/use operands of a MachineInstr to
188 // operands of an MCInst. Otherwise, we cannot correctly reconstruct data
189 // dependencies, nor we can correctly interpret the scheduling model, which
190 // heavily uses machine operand indices to define processor read-advance
191 // information, and to identify processor write resources. Essentially, we
192 // either need something like a MCInstrDesc, but for MCInst, or a way
193 // to map MCInst operands back to MachineInstr operands.
194 //
195 // Unfortunately, we don't have that information now. So, this prototype
196 // currently work under the strong assumption that we can always safely trust
197 // the content of an MCInstrDesc. For example, we can query a MCInstrDesc to
198 // obtain the number of explicit and implicit register defintions. We also
199 // assume that register definitions always come first in the operand sequence.
200 // This last assumption usually makes sense for MachineInstr, where register
201 // definitions always appear at the beginning of the operands sequence. In
202 // reality, these assumptions could be broken by the lowering step, which can
203 // decide to lay out operands in a different order than the original order of
204 // operand as specified by the MachineInstr.
205 //
206 // Things get even more complicated in the presence of "optional" register
207 // definitions. For MachineInstr, optional register definitions are always at
208 // the end of the operand sequence. Some ARM instructions that may update the
209 // status flags specify that register as a optional operand. Since we don't
210 // have operand descriptors for MCInst, we assume for now that the optional
211 // definition is always the last operand of a MCInst. Again, this assumption
212 // may be okay for most targets. However, there is no guarantee that targets
213 // would respect that.
214 //
215 // In conclusion: these are for now the strong assumptions made by the tool:
216 // * The number of explicit and implicit register definitions in a MCInst
217 // matches the number of explicit and implicit definitions according to
218 // the opcode descriptor (MCInstrDesc).
219 // * Register definitions take precedence over register uses in the operands
220 // list.
221 // * If an opcode specifies an optional definition, then the optional
222 // definition is always the last operand in the sequence, and it can be
223 // set to zero (i.e. "no register").
224 //
225 // These assumptions work quite well for most out-of-order in-tree targets
226 // like x86. This is mainly because the vast majority of instructions is
227 // expanded to MCInst using a straightforward lowering logic that preserves
228 // the ordering of the operands.
229 //
230 // In the longer term, we need to find a proper solution for this issue.
231 unsigned NumExplicitDefs = MCDesc.getNumDefs();
232 unsigned NumImplicitDefs = MCDesc.getNumImplicitDefs();
233 unsigned NumWriteLatencyEntries = SCDesc.NumWriteLatencyEntries;
234 unsigned TotalDefs = NumExplicitDefs + NumImplicitDefs;
235 if (MCDesc.hasOptionalDef())
236 TotalDefs++;
237 ID.Writes.resize(TotalDefs);
238 // Iterate over the operands list, and skip non-register operands.
239 // The first NumExplictDefs register operands are expected to be register
240 // definitions.
241 unsigned CurrentDef = 0;
242 unsigned i = 0;
243 for (; i < MCI.getNumOperands() && CurrentDef < NumExplicitDefs; ++i) {
244 const MCOperand &Op = MCI.getOperand(i);
245 if (!Op.isReg())
246 continue;
247
248 WriteDescriptor &Write = ID.Writes[CurrentDef];
249 Write.OpIndex = i;
250 if (CurrentDef < NumWriteLatencyEntries) {
251 const MCWriteLatencyEntry &WLE =
252 *STI.getWriteLatencyEntry(&SCDesc, CurrentDef);
253 // Conservatively default to MaxLatency.
254 Write.Latency = WLE.Cycles == -1 ? ID.MaxLatency : WLE.Cycles;
255 Write.SClassOrWriteResourceID = WLE.WriteResourceID;
256 } else {
257 // Assign a default latency for this write.
258 Write.Latency = ID.MaxLatency;
259 Write.SClassOrWriteResourceID = 0;
260 }
261 Write.FullyUpdatesSuperRegs = FullyUpdatesSuperRegisters;
262 Write.IsOptionalDef = false;
Andrea Di Biagio7b3d1622018-03-20 12:58:34 +0000263 DEBUG({
264 dbgs() << "\t\tOpIdx=" << Write.OpIndex << ", Latency=" << Write.Latency
265 << ", WriteResourceID=" << Write.SClassOrWriteResourceID << '\n';
266 });
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000267 CurrentDef++;
268 }
269
270 if (CurrentDef != NumExplicitDefs)
271 llvm::report_fatal_error(
272 "error: Expected more register operand definitions. ");
273
274 CurrentDef = 0;
275 for (CurrentDef = 0; CurrentDef < NumImplicitDefs; ++CurrentDef) {
276 unsigned Index = NumExplicitDefs + CurrentDef;
277 WriteDescriptor &Write = ID.Writes[Index];
278 Write.OpIndex = -1;
279 Write.RegisterID = MCDesc.getImplicitDefs()[CurrentDef];
Andrea Di Biagio6fd62fe2018-04-02 13:46:49 +0000280 if (Index < NumWriteLatencyEntries) {
281 const MCWriteLatencyEntry &WLE =
282 *STI.getWriteLatencyEntry(&SCDesc, Index);
283 // Conservatively default to MaxLatency.
284 Write.Latency = WLE.Cycles == -1 ? ID.MaxLatency : WLE.Cycles;
285 Write.SClassOrWriteResourceID = WLE.WriteResourceID;
286 } else {
287 // Assign a default latency for this write.
288 Write.Latency = ID.MaxLatency;
289 Write.SClassOrWriteResourceID = 0;
290 }
291
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000292 Write.IsOptionalDef = false;
293 assert(Write.RegisterID != 0 && "Expected a valid phys register!");
294 DEBUG(dbgs() << "\t\tOpIdx=" << Write.OpIndex << ", PhysReg="
295 << Write.RegisterID << ", Latency=" << Write.Latency
296 << ", WriteResourceID=" << Write.SClassOrWriteResourceID
297 << '\n');
298 }
299
300 if (MCDesc.hasOptionalDef()) {
301 // Always assume that the optional definition is the last operand of the
302 // MCInst sequence.
303 const MCOperand &Op = MCI.getOperand(MCI.getNumOperands() - 1);
304 if (i == MCI.getNumOperands() || !Op.isReg())
305 llvm::report_fatal_error(
306 "error: expected a register operand for an optional "
307 "definition. Instruction has not be correctly analyzed.\n",
308 false);
309
310 WriteDescriptor &Write = ID.Writes[TotalDefs - 1];
311 Write.OpIndex = MCI.getNumOperands() - 1;
312 // Assign a default latency for this write.
313 Write.Latency = ID.MaxLatency;
314 Write.SClassOrWriteResourceID = 0;
315 Write.IsOptionalDef = true;
316 }
317}
318
319static void populateReads(InstrDesc &ID, const MCInst &MCI,
320 const MCInstrDesc &MCDesc,
321 const MCSchedClassDesc &SCDesc,
322 const MCSubtargetInfo &STI) {
323 unsigned SchedClassID = MCDesc.getSchedClass();
324 bool HasReadAdvanceEntries = SCDesc.NumReadAdvanceEntries > 0;
325
326 unsigned i = 0;
327 unsigned NumExplicitDefs = MCDesc.getNumDefs();
328 // Skip explicit definitions.
329 for (; i < MCI.getNumOperands() && NumExplicitDefs; ++i) {
330 const MCOperand &Op = MCI.getOperand(i);
331 if (Op.isReg())
332 NumExplicitDefs--;
333 }
334
335 if (NumExplicitDefs)
336 llvm::report_fatal_error(
337 "error: Expected more register operand definitions. ", false);
338
339 unsigned NumExplicitUses = MCI.getNumOperands() - i;
340 unsigned NumImplicitUses = MCDesc.getNumImplicitUses();
341 if (MCDesc.hasOptionalDef()) {
342 assert(NumExplicitUses);
343 NumExplicitUses--;
344 }
345 unsigned TotalUses = NumExplicitUses + NumImplicitUses;
346 if (!TotalUses)
347 return;
348
349 ID.Reads.resize(TotalUses);
350 for (unsigned CurrentUse = 0; CurrentUse < NumExplicitUses; ++CurrentUse) {
351 ReadDescriptor &Read = ID.Reads[CurrentUse];
352 Read.OpIndex = i + CurrentUse;
Andrea Di Biagio0a837ef2018-03-29 14:26:56 +0000353 Read.UseIndex = CurrentUse;
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000354 Read.HasReadAdvanceEntries = HasReadAdvanceEntries;
355 Read.SchedClassID = SchedClassID;
356 DEBUG(dbgs() << "\t\tOpIdx=" << Read.OpIndex);
357 }
358
359 for (unsigned CurrentUse = 0; CurrentUse < NumImplicitUses; ++CurrentUse) {
360 ReadDescriptor &Read = ID.Reads[NumExplicitUses + CurrentUse];
361 Read.OpIndex = -1;
Andrea Di Biagio6fd62fe2018-04-02 13:46:49 +0000362 Read.UseIndex = NumExplicitUses + CurrentUse;
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000363 Read.RegisterID = MCDesc.getImplicitUses()[CurrentUse];
Andrea Di Biagio6fd62fe2018-04-02 13:46:49 +0000364 Read.HasReadAdvanceEntries = HasReadAdvanceEntries;
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000365 Read.SchedClassID = SchedClassID;
366 DEBUG(dbgs() << "\t\tOpIdx=" << Read.OpIndex
367 << ", RegisterID=" << Read.RegisterID << '\n');
368 }
369}
370
Andrea Di Biagio4704f032018-03-20 12:25:54 +0000371void InstrBuilder::createInstrDescImpl(const MCInst &MCI) {
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000372 assert(STI.getSchedModel().hasInstrSchedModel() &&
373 "Itineraries are not yet supported!");
374
375 unsigned short Opcode = MCI.getOpcode();
376 // Obtain the instruction descriptor from the opcode.
377 const MCInstrDesc &MCDesc = MCII.get(Opcode);
378 const MCSchedModel &SM = STI.getSchedModel();
379
380 // Then obtain the scheduling class information from the instruction.
381 const MCSchedClassDesc &SCDesc =
382 *SM.getSchedClassDesc(MCDesc.getSchedClass());
383
384 // Create a new empty descriptor.
Andrea Di Biagio7b3d1622018-03-20 12:58:34 +0000385 std::unique_ptr<InstrDesc> ID = llvm::make_unique<InstrDesc>();
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000386
387 if (SCDesc.isVariant()) {
388 errs() << "warning: don't know how to model variant opcodes.\n"
389 << "note: assume 1 micro opcode.\n";
390 ID->NumMicroOps = 1U;
391 } else {
392 ID->NumMicroOps = SCDesc.NumMicroOps;
393 }
394
395 if (MCDesc.isCall()) {
396 // We don't correctly model calls.
397 errs() << "warning: found a call in the input assembly sequence.\n"
398 << "note: call instructions are not correctly modeled. Assume a "
399 "latency of 100cy.\n";
400 }
401
402 if (MCDesc.isReturn()) {
403 errs() << "warning: found a return instruction in the input assembly "
404 "sequence.\n"
405 << "note: program counter updates are ignored.\n";
406 }
407
408 ID->MayLoad = MCDesc.mayLoad();
409 ID->MayStore = MCDesc.mayStore();
410 ID->HasSideEffects = MCDesc.hasUnmodeledSideEffects();
411
412 initializeUsedResources(*ID, SCDesc, STI, ProcResourceMasks);
413 populateWrites(*ID, MCI, MCDesc, SCDesc, STI);
414 populateReads(*ID, MCI, MCDesc, SCDesc, STI);
415
416 DEBUG(dbgs() << "\t\tMaxLatency=" << ID->MaxLatency << '\n');
417 DEBUG(dbgs() << "\t\tNumMicroOps=" << ID->NumMicroOps << '\n');
418
419 // Now add the new descriptor.
Andrea Di Biagio7b3d1622018-03-20 12:58:34 +0000420 Descriptors[Opcode] = std::move(ID);
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000421}
422
Andrea Di Biagio4704f032018-03-20 12:25:54 +0000423const InstrDesc &InstrBuilder::getOrCreateInstrDesc(const MCInst &MCI) {
Andrea Di Biagio35622482018-03-22 10:19:20 +0000424 if (Descriptors.find_as(MCI.getOpcode()) == Descriptors.end())
Andrea Di Biagio4704f032018-03-20 12:25:54 +0000425 createInstrDescImpl(MCI);
Andrea Di Biagio35622482018-03-22 10:19:20 +0000426 return *Descriptors[MCI.getOpcode()];
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000427}
428
Andrea Di Biagio7b3d1622018-03-20 12:58:34 +0000429std::unique_ptr<Instruction>
430InstrBuilder::createInstruction(unsigned Idx, const MCInst &MCI) {
Andrea Di Biagio4704f032018-03-20 12:25:54 +0000431 const InstrDesc &D = getOrCreateInstrDesc(MCI);
Andrea Di Biagio7b3d1622018-03-20 12:58:34 +0000432 std::unique_ptr<Instruction> NewIS = llvm::make_unique<Instruction>(D);
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000433
434 // Populate Reads first.
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000435 for (const ReadDescriptor &RD : D.Reads) {
436 int RegID = -1;
437 if (RD.OpIndex != -1) {
438 // explicit read.
439 const MCOperand &Op = MCI.getOperand(RD.OpIndex);
440 // Skip non-register operands.
441 if (!Op.isReg())
442 continue;
443 RegID = Op.getReg();
444 } else {
445 // Implicit read.
446 RegID = RD.RegisterID;
447 }
448
449 // Skip invalid register operands.
450 if (!RegID)
451 continue;
452
453 // Okay, this is a register operand. Create a ReadState for it.
454 assert(RegID > 0 && "Invalid register ID found!");
Andrea Di Biagio7b3d1622018-03-20 12:58:34 +0000455 NewIS->getUses().emplace_back(llvm::make_unique<ReadState>(RD, RegID));
Andrea Di Biagio4704f032018-03-20 12:25:54 +0000456 }
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000457
458 // Now populate writes.
459 for (const WriteDescriptor &WD : D.Writes) {
460 unsigned RegID =
461 WD.OpIndex == -1 ? WD.RegisterID : MCI.getOperand(WD.OpIndex).getReg();
Andrea Di Biagio35622482018-03-22 10:19:20 +0000462 // Check if this is a optional definition that references NoReg.
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000463 if (WD.IsOptionalDef && !RegID)
464 continue;
465
Andrea Di Biagio35622482018-03-22 10:19:20 +0000466 assert(RegID && "Expected a valid register ID!");
Andrea Di Biagio7b3d1622018-03-20 12:58:34 +0000467 NewIS->getDefs().emplace_back(llvm::make_unique<WriteState>(WD, RegID));
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000468 }
469
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000470 return NewIS;
471}
Andrea Di Biagio3a6b0922018-03-08 13:05:02 +0000472} // namespace mca