Index: llvm/include/llvm/Transforms/IPO/OpenMPOpt.h =================================================================== --- llvm/include/llvm/Transforms/IPO/OpenMPOpt.h +++ llvm/include/llvm/Transforms/IPO/OpenMPOpt.h @@ -21,7 +21,6 @@ #include "llvm/Analysis/MemorySSA.h" namespace llvm { - namespace omp { using namespace types; @@ -133,6 +132,69 @@ DenseMap> UsesMap; }; + /// Used to store information about a runtime call that involves + /// host to device memory offloading. For example: + /// __tgt_target_data_begin(..., + /// i8** %offload_baseptrs, i8** %offload_ptrs, i64* %offload_sizes, + /// ...) + struct MemoryTransfer { + + /// Used to map the values physically (in the IR) stored in an offload + /// array, to a vector in memory. + struct OffloadArray { + AllocaInst &Array; /// Physical array (in the IR). + SmallVector StoredValues; /// Mapped values. + InformationCache &InfoCache; + + /// Factory function for creating and initializing the OffloadArray with + /// the values stored in \p Array before the instruction \p Before is + /// reached. + /// This MUST be used instead of the constructor. + static std::unique_ptr initialize( + AllocaInst &Array, + Instruction &Before, + InformationCache &InfoCache); + + /// Use the factory function initialize(...) instead. + OffloadArray(AllocaInst &Array, InformationCache &InfoCache) + : Array(Array), InfoCache(InfoCache) {} + + private: + /// Traverses the BasicBlocks collecting the stores made to + /// OffloadArray::Array, leaving OffloadArray::StoredValues with the + /// values stored before the instruction \p Before is reached. + bool getValues(Instruction &Before); + + /// Returns the index of OffloadArray::Array where the store is being + /// made. Returns -1 if the index can't be deduced. + int32_t getAccessedIdx(StoreInst &S); + + /// Returns true all values in \p V are not nullptrs. + static bool isFilled(const SmallVectorImpl &V); + }; + + CallBase *RuntimeCall; /// Call that involves a memotry transfer. + InformationCache &InfoCache; + + /// These help mapping the values in offload_baseptrs, offload_ptrs, and + /// offload_sizes, respectively. + std::unique_ptr BasePtrs; + std::unique_ptr Ptrs; + std::unique_ptr Sizes; + + MemoryTransfer(CallBase *RuntimeCall, InformationCache &InfoCache) : + RuntimeCall{RuntimeCall}, InfoCache{InfoCache} + {} + + /// Maps the values physically (the IR) stored in the offload arrays + /// offload_baseptrs, offload_ptrs, offload_sizes to their corresponding + /// members, MemoryTransfer::BasePtrs, MemoryTransfer::Ptrs, + /// MemoryTransfer::Sizes. + /// Returns false if one of the arrays couldn't be processed or some of the + /// values couldn't be found. + bool getValuesInOffloadArrays(); + }; + /// The slice of the module we are allowed to look at. SmallPtrSetImpl &ModuleSlice; @@ -166,6 +228,7 @@ struct OpenMPOpt { + using MemoryTransfer = OMPInformationCache::MemoryTransfer; using OptimizationRemarkGetter = function_ref; @@ -188,6 +251,12 @@ static CallInst *getCallIfRegularCall( Value &V, OMPInformationCache::RuntimeFunctionInfo *RFI = nullptr); + /// Returns the integer representation of \p V. + static int64_t getIntLiteral(const Value *V) { + assert(V && "Getting Integer value of nullptr"); + return (dyn_cast(V))->getZExtValue(); + } + private: /// Try to delete parallel regions if possible. bool deleteParallelRegions(); @@ -195,6 +264,14 @@ /// Try to eliminiate runtime calls by reusing existing ones. bool deduplicateRuntimeCalls(); + /// Tries to hide the latency of runtime calls that involve host to + /// device memory transfers by splitting them into their "issue" and "wait". + /// versions. The "issue" is moved upwards as much as possible. The "wait" is + /// moved downards as much as possible. The "issue" issues the memory transfer + /// asynchronously, returning a handle. The "wait" waits in the returned + /// handle for the memory transfer to finish. + bool hideMemTransfersLatency(); + static Value *combinedIdentStruct(Value *CurrentIdent, Value *NextIdent, bool GlobalOnly, bool &SingleChoice); Index: llvm/lib/Transforms/IPO/OpenMPOpt.cpp =================================================================== --- llvm/lib/Transforms/IPO/OpenMPOpt.cpp +++ llvm/lib/Transforms/IPO/OpenMPOpt.cpp @@ -26,6 +26,8 @@ #include "llvm/Transforms/IPO.h" #include "llvm/Transforms/IPO/Attributor.h" #include "llvm/Transforms/Utils/CallGraphUpdater.h" +#include "llvm/Analysis/ValueTracking.h" +#include "llvm/Analysis/MemorySSA.h" using namespace llvm; using namespace omp; @@ -95,6 +97,9 @@ // Definitions of the OMPInformationCache helper structure. //===----------------------------------------------------------------------===// +using MemoryTransfer = OMPInformationCache::MemoryTransfer; +using OffloadArray = MemoryTransfer::OffloadArray; + void OMPInformationCache::RuntimeFunctionInfo::foreachUse( function_ref CB, Function *F, UseVector *Uses) { SmallVector ToBeDeleted; @@ -234,6 +239,186 @@ return true; } +//===----------------------------------------------------------------------===// +// Definitions of the MemoryTransfer helper structure. +//===----------------------------------------------------------------------===// + +bool MemoryTransfer::getValuesInOffloadArrays() { + // A runtime call that involves memory offloading looks something like: + // call void @__tgt_target_data_begin(arg0, arg1, + // i8** %offload_baseptrs, i8** %offload_ptrs, i64* %offload_sizes, + // ...) + // So, the idea is to access the allocas that allocate space for these offload + // arrays, offload_baseptrs, offload_ptrs, offload_sizes. + // Therefore: + // i8** %offload_baseptrs. + const unsigned BasePtrsArgNum = 2; + Use *BasePtrsArg = RuntimeCall->arg_begin() + BasePtrsArgNum; + // i8** %offload_ptrs. + const unsigned PtrsArgNum = 3; + Use *PtrsArg = RuntimeCall->arg_begin() + PtrsArgNum; + // i8** %offload_sizes. + const unsigned SizesArgNum = 4; + Use *SizesArg = RuntimeCall->arg_begin() + SizesArgNum; + + const DataLayout &DL = InfoCache.getDL(); + + // Get values stored in **offload_baseptrs. + auto *V = GetUnderlyingObject(BasePtrsArg->get(), DL); + if (!isa(V)) { + LLVM_DEBUG(dbgs() << TAG << "Couldn't get offload_baseptrs, only" + << " alloca arrays supported. In call to " + << RuntimeCall->getName() << " in function " + << RuntimeCall->getCaller()->getName() << "\n"); + return false; + } + + auto *Array = cast(V); + BasePtrs = OffloadArray::initialize(*Array, *RuntimeCall, InfoCache); + if (!BasePtrs) { + LLVM_DEBUG(dbgs() << TAG << "Couldn't get offload_baseptrs in call to " + << RuntimeCall->getName() << " in function " + << RuntimeCall->getCaller()->getName() << "\n"); + return false; + } + + // Get values stored in **offload_ptrs. + V = GetUnderlyingObject(PtrsArg->get(), DL); + if (!isa(V)) { + LLVM_DEBUG(dbgs() << TAG << "Couldn't get offload_ptrs, only" + << " alloca arrays supported. In call to " + << RuntimeCall->getName() << " in function " + << RuntimeCall->getCaller()->getName() << "\n"); + return false; + } + Array = cast(V); + Ptrs = OffloadArray::initialize(*Array, *RuntimeCall, InfoCache); + if (!Ptrs) { + LLVM_DEBUG(dbgs() << TAG << "Couldn't get offload_ptrs in call to " + << RuntimeCall->getName() << " in function " + << RuntimeCall->getCaller()->getName() << "\n"); + return false; + } + + // Get values stored in **offload_sizes. + V = GetUnderlyingObject(SizesArg->get(), DL); + // Sometimes the frontend generates this array as a constant global array. + if (!isa(V)) { + if (!isa(V)) { + LLVM_DEBUG(dbgs() << TAG << "Couldn't get offload_sizes, only" + << " alloca arrays supported. In call to " + << RuntimeCall->getName() << " in function " + << RuntimeCall->getCaller()->getName() << "\n"); + return false; + } + + Array = cast(V); + Sizes = OffloadArray::initialize(*Array, *RuntimeCall, InfoCache); + if (!Sizes) { + LLVM_DEBUG(dbgs() << TAG << "Couldn't get offload_sizes in call to " + << RuntimeCall->getName() << " in function " + << RuntimeCall->getCaller()->getName() << "\n"); + return false; + } + } + + return true; +} + +std::unique_ptr OffloadArray::initialize( + AllocaInst &Array, Instruction &Before, InformationCache &InfoCache) { + if (!Array.getAllocatedType()->isArrayTy()) { + LLVM_DEBUG(dbgs() << TAG << "Allocated type is not array.\n"); + return nullptr; + } + + auto OA = std::make_unique(Array, InfoCache); + bool Success = OA->getValues(Before); + if (!Success) { + LLVM_DEBUG(dbgs() << TAG << "Error getting values in offload array.\n"); + return nullptr; + } + + return OA; +} + +bool OffloadArray::getValues(Instruction &Before) { + // Initialize container. + const uint64_t NumValues = + Array.getAllocatedType()->getArrayNumElements(); + StoredValues.assign(NumValues, nullptr); + + // TODO: This assumes the instruction \p Before is in the same BasicBlock + // as OffloadArray::Array. Make it general, for any control flow graph. + auto *BB = Array.getParent(); + if (BB != Before.getParent()) { + LLVM_DEBUG(dbgs() << TAG << "The lower limit instruction is in a" + << " different BasicBlock.\n"); + return false; + } + + const DataLayout &DL = InfoCache.getDL(); + for (auto &I : *BB) { + if (&I == &Before) break; + + if (isa(&I)) { + auto *Dst = GetUnderlyingObject(I.getOperand(1), DL); + + if (Dst == &Array) { + int32_t AccessedIdx = getAccessedIdx(*cast(&I)); + if (AccessedIdx < 0) { + LLVM_DEBUG(dbgs() << TAG << "Unexpected StoreInst\n"); + return false; + } + + StoredValues[AccessedIdx] = GetUnderlyingObject(I.getOperand(0), DL); + } + } + } + + return isFilled(StoredValues); +} + +int32_t OffloadArray::getAccessedIdx(StoreInst &S) { + auto *Dst = S.getOperand(1); + if (!isa(Dst)) { + LLVM_DEBUG(dbgs() << TAG << "Unrecognized store pattern.\n"); + return -1; + } + auto *DstInst = cast(Dst); + + Value *Access = DstInst; + if (DstInst->isCast()) { + Access = DstInst->getOperand(0); + + // Direct cast from the AllocaInst, which means a store to the + // first position of the array. + if (Access == &Array) return 0; + } + + if (!isa(Access)) { + LLVM_DEBUG(dbgs() << TAG << "Unrecognized store pattern.\n"); + return -1; + } + auto *GEPInst = cast(Access); + + auto *ArrayIdx = GEPInst->idx_begin() + 1; + if (ArrayIdx == GEPInst->idx_end()) { + LLVM_DEBUG(dbgs() << TAG << "Unrecognized store pattern.\n"); + return -1; + } + + return OpenMPOpt::getIntLiteral(ArrayIdx->get()); +} + +bool OffloadArray::isFilled(const SmallVectorImpl &V) { + for (auto *E : V) + if (!E) + return false; + + return true; +} + //===----------------------------------------------------------------------===// // Declarations and definitions of AAICVTracker. //===----------------------------------------------------------------------===// @@ -443,6 +628,7 @@ Changed |= runAttributor(); Changed |= deduplicateRuntimeCalls(); Changed |= deleteParallelRegions(); + Changed |= hideMemTransfersLatency(); return Changed; } @@ -558,6 +744,31 @@ return Changed; } +bool OpenMPOpt::hideMemTransfersLatency() { + OMPInformationCache::RuntimeFunctionInfo &RFI = + OMPInfoCache.RFIs[OMPRTL___tgt_target_data_begin]; + + bool Changed = false; + auto SplitDataTransfer = [&] (Use &U, Function &Decl) { + auto *RTCall = getCallIfRegularCall(U, &RFI); + if (!RTCall) + return false; + + MemoryTransfer MT(RTCall, OMPInfoCache); + bool Success = MT.getValuesInOffloadArrays(); + if (!Success) { + LLVM_DEBUG(dbgs() << TAG << "Couldn't get offload arrays in call to " + << MT.RuntimeCall->getName() << " in function " + << MT.RuntimeCall->getCaller()->getName() << "\n"); + return false; + } + return false; + }; + + RFI.foreachUse(SplitDataTransfer); + return Changed; +} + Value *OpenMPOpt::combinedIdentStruct(Value *CurrentIdent, Value *NextIdent, bool GlobalOnly, bool &SingleChoice) { if (CurrentIdent == NextIdent) Index: llvm/unittests/Transforms/IPO/CMakeLists.txt =================================================================== --- llvm/unittests/Transforms/IPO/CMakeLists.txt +++ llvm/unittests/Transforms/IPO/CMakeLists.txt @@ -8,3 +8,5 @@ LowerTypeTests.cpp WholeProgramDevirt.cpp ) + +add_subdirectory(OpenMPOpt) Index: llvm/unittests/Transforms/IPO/OpenMPOpt/CMakeLists.txt =================================================================== --- /dev/null +++ llvm/unittests/Transforms/IPO/OpenMPOpt/CMakeLists.txt @@ -0,0 +1,12 @@ +set(LLVM_LINK_COMPONENTS + ipo + Core + Analysis + AsmParser + FrontendOpenMP + TransformUtils + ) + +add_llvm_unittest(OpenMPOptUnitTests + HideMemTransferLatencyTest.cpp + ) \ No newline at end of file Index: llvm/unittests/Transforms/IPO/OpenMPOpt/HideMemTransferLatencyTest.cpp =================================================================== --- /dev/null +++ llvm/unittests/Transforms/IPO/OpenMPOpt/HideMemTransferLatencyTest.cpp @@ -0,0 +1,159 @@ +//===- HideMemTransferLatencyTest.cpp -------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "OpenMPOptTest.h" +#include "llvm/IR/ValueSymbolTable.h" + +namespace { + +class HideMemTransferLatencyTest : public OpenMPOptTest { +protected: + NiceMock MMP; + MockSCCPass MSCCP; + ModulePassManager MPM; + + FunctionAnalysisManager FAM; + ModuleAnalysisManager MAM; + CGSCCAnalysisManager CGAM; + + HideMemTransferLatencyTest() + : OpenMPOptTest(), FAM(true), MAM(true), CGAM(true) { + MAM.registerPass([] { return LazyCallGraphAnalysis(); }); + FAM.registerPass([] { return TargetLibraryAnalysis(); }); + FAM.registerPass([]{ return AAManager(); }); + FAM.registerPass([]{ return DominatorTreeAnalysis(); }); + FAM.registerPass([]{ return MemorySSAAnalysis(); }); + CGAM.registerPass([&] { return FunctionAnalysisManagerCGSCCProxy(); }); + + // Register required pass instrumentation analysis. + FAM.registerPass([] { return PassInstrumentationAnalysis(); }); + MAM.registerPass([] { return PassInstrumentationAnalysis(); }); + CGAM.registerPass([] {return PassInstrumentationAnalysis(); }); + + // Cross-register proxies. + MAM.registerPass([&] { return FunctionAnalysisManagerModuleProxy(FAM); }); + MAM.registerPass([&] { return CGSCCAnalysisManagerModuleProxy(CGAM); }); + CGAM.registerPass([&] { return ModuleAnalysisManagerCGSCCProxy(MAM); }); + FAM.registerPass([&] { return CGSCCAnalysisManagerFunctionProxy(CGAM); }); + FAM.registerPass([&] { return ModuleAnalysisManagerFunctionProxy(MAM); }); + + CGSCCPassManager CGPM; + CGPM.addPass(MSCCP.getPass()); + + MPM.addPass(createModuleToPostOrderCGSCCPassAdaptor(std::move(CGPM))); + } +}; + +TEST_F(HideMemTransferLatencyTest, GetValuesInOfflArrays) { + const char *ModuleString = + "@.offload_maptypes = private unnamed_addr constant [1 x i64] [i64 33]\n" + "define dso_local i32 @dataTransferOnly(double* noalias %a, i32 %size) {\n" + "entry:\n" + " %.offload_baseptrs = alloca [1 x i8*], align 8\n" + " %.offload_ptrs = alloca [1 x i8*], align 8\n" + " %.offload_sizes = alloca [1 x i64], align 8\n" + + " %call = call i32 @rand()\n" + + " %conv = zext i32 %size to i64\n" + " %0 = shl nuw nsw i64 %conv, 3\n" + " %1 = getelementptr inbounds [1 x i8*], [1 x i8*]* %.offload_baseptrs, i64 0, i64 0\n" + " %2 = bitcast [1 x i8*]* %.offload_baseptrs to double**\n" + " store double* %a, double** %2, align 8\n" + " %3 = getelementptr inbounds [1 x i8*], [1 x i8*]* %.offload_ptrs, i64 0, i64 0\n" + " %4 = bitcast [1 x i8*]* %.offload_ptrs to double**\n" + " store double* %a, double** %4, align 8\n" + " %5 = getelementptr inbounds [1 x i64], [1 x i64]* %.offload_sizes, i64 0, i64 0\n" + " store i64 %0, i64* %5, align 8\n" + " call void @__tgt_target_data_begin(i64 -1, i32 1, i8** nonnull %1, i8** nonnull %3, i64* nonnull %5, i64* getelementptr inbounds ([1 x i64], [1 x i64]* @.offload_maptypes, i64 0, i64 0))\n" + + " %rem = urem i32 %call, %size\n" + + " call void @__tgt_target_data_end(i64 -1, i32 1, i8** nonnull %1, i8** nonnull %3, i64* nonnull %5, i64* getelementptr inbounds ([1 x i64], [1 x i64]* @.offload_maptypes, i64 0, i64 0))\n" + " ret i32 %rem\n" + "}\n" + + "declare dso_local void @__tgt_target_data_begin(i64, i32, i8**, i8**, i64*, i64*)\n" + "declare dso_local void @__tgt_target_data_end(i64, i32, i8**, i8**, i64*, i64*)\n" + "declare dso_local i32 @rand()"; + + std::unique_ptr M = parseModuleString(ModuleString); + + EXPECT_CALL(MSCCP, run(_, _, _, _)) + .WillRepeatedly(Invoke([](LazyCallGraph::SCC &C, CGSCCAnalysisManager &AM, + LazyCallGraph &CG, CGSCCUpdateResult &UR) { + SmallPtrSet ModuleSlice; + SmallVector SCC; + for (LazyCallGraph::Node &N : C) { + SCC.push_back(&N.getFunction()); + ModuleSlice.insert(SCC.back()); + } + + EXPECT_FALSE(SCC.empty()); + + FunctionAnalysisManager &FAM = + AM.getResult(C, CG).getManager(); + + AnalysisGetter AG(FAM); + CallGraphUpdater CGUpdater; + CGUpdater.initialize(CG, C, AM, UR); + + SetVector Functions(SCC.begin(), SCC.end()); + BumpPtrAllocator Allocator; + OMPInformationCache InfoCache(*(Functions.back()->getParent()), AG, Allocator, + /*CGSCC*/ &Functions, ModuleSlice); + Attributor A(Functions, InfoCache, CGUpdater); + + auto &RFI = InfoCache.RFIs[OMPRTL___tgt_target_data_begin]; + auto GetValuesInOfflArrays = [&](Use &U, Function &Decl) { + auto *RTCall = OpenMPOpt::getCallIfRegularCall(U, &RFI); + EXPECT_TRUE(RTCall); + + OMPInformationCache::MemoryTransfer MT(RTCall, InfoCache); + bool Success = MT.getValuesInOffloadArrays(); + EXPECT_TRUE(Success); + + Module &M = *RTCall->getModule(); + Function &F = *M.getFunction("dataTransferOnly"); + auto *Arg = F.getArg(0); + + // Check for **offload_baseptrs. + auto &BasePtrsValues = MT.BasePtrs->StoredValues; + EXPECT_EQ(BasePtrsValues.size(), (size_t) 1); + EXPECT_EQ(BasePtrsValues[0], Arg); + + // Check for **offload_ptrs. + auto &PtrsValues = MT.Ptrs->StoredValues; + EXPECT_EQ(PtrsValues.size(), (size_t) 1); + EXPECT_EQ(PtrsValues[0], Arg); + + // Check for **offload_sizes. + auto &SizesValues = MT.Sizes->StoredValues; + EXPECT_EQ(SizesValues.size(), (size_t) 1); + std::string ValueName; + raw_string_ostream OS(ValueName); + for (auto &I : F.getEntryBlock()) { + I.print(OS); + if (OS.str() == " %0 = shl nuw nsw i64 %conv, 3") { + EXPECT_EQ(&I, SizesValues[0]); + break; + } + ValueName.clear(); + } + + return false; + }; + RFI.foreachUse(GetValuesInOfflArrays); + + return PreservedAnalyses::all(); + })); + + MPM.run(*M, MAM); +} + +} // end anonymous namespace \ No newline at end of file Index: llvm/unittests/Transforms/IPO/OpenMPOpt/OpenMPOptTest.h =================================================================== --- /dev/null +++ llvm/unittests/Transforms/IPO/OpenMPOpt/OpenMPOptTest.h @@ -0,0 +1,101 @@ +//===- OpenMPOptTest.h - Base file for OpenMPOpt unittests ----------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_UNITTESTS_TRANSFORMS_IPO_OPENMPOPT_H +#define LLVM_UNITTESTS_TRANSFORMS_IPO_OPENMPOPT_H + +#include "llvm/Transforms/IPO/OpenMPOpt.h" +#include "llvm/AsmParser/Parser.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +using namespace llvm; +using namespace omp; + +namespace { + +using testing::NiceMock; +using testing::Matcher; +using testing::Return; +using testing::Invoke; +using testing::_; + +template , + typename... ExtraArgTs> +class MockPassBase { +public: + class Pass : public PassInfoMixin { + friend MockPassBase; + + DerivedT *Handle; + + Pass(DerivedT &Handle) : Handle(&Handle) { + static_assert(std::is_base_of::value, + "Must pass the derived type to this template!"); + } + + public: + PreservedAnalyses run(IRUnitT &IR, AnalysisManagerT &AM, + ExtraArgTs... ExtraArgs) { + return Handle->run(IR, AM, ExtraArgs...); + } + }; + + Pass getPass() { return Pass(static_cast(*this)); } + +protected: + /// Derived classes should call this in their constructor to set up default + /// mock actions. (We can't do this in our constructor because this has to + /// run after the DerivedT is constructed.) + void setDefaults() { + ON_CALL(static_cast(*this), + run(_, _, testing::Matcher(_)...)) + .WillByDefault(Return(PreservedAnalyses::all())); + } +}; + +struct MockFunctionPass + : MockPassBase { + MOCK_METHOD2(run, PreservedAnalyses(Function &, FunctionAnalysisManager &)); + + MockFunctionPass() { setDefaults(); } +}; + +struct MockModulePass : MockPassBase { + MOCK_METHOD2(run, PreservedAnalyses(Module &, ModuleAnalysisManager &)); + + MockModulePass() { setDefaults(); } +}; + + +struct MockSCCPass : MockPassBase { + MOCK_METHOD4(run, + PreservedAnalyses(LazyCallGraph::SCC &, CGSCCAnalysisManager &, + LazyCallGraph &, CGSCCUpdateResult &)); + +}; + +class OpenMPOptTest : public ::testing::Test { +protected: + std::unique_ptr Ctx; + + OpenMPOptTest() : Ctx(new LLVMContext) {} + + std::unique_ptr parseModuleString(const char *ModuleString) { + SMDiagnostic Err; + auto M = parseAssemblyString(ModuleString, Err, *Ctx); + EXPECT_TRUE(M); + return M; + } +}; + +} // end anonymous namespace + +#endif // LLVM_UNITTESTS_TRANSFORMS_IPO_OPENMPOPT_H \ No newline at end of file