Skip to content

Commit f2b7ba9

Browse files
committed
[mlir][OpenMP] Pack task private variables into a heap-allocated context struct
See RFC: https://discourse.llvm.org/t/rfc-openmp-supporting-delayed-task-execution-with-firstprivate-variables/83084 The aim here is to ensure that tasks which are not executed for a while after they are created do not try to reference any data which are now out of scope. This is done by packing the data referred to by the task into a heap allocated structure (freed at the end of the task). I decided to create the task context structure in OpenMPToLLVMIRTranslation instead of adapting how it is done CodeExtractor (via OpenMPIRBuilder] because CodeExtractor is (at least in theory) generic code which could have other unrelated uses.
1 parent c5f1005 commit f2b7ba9

File tree

3 files changed

+254
-37
lines changed

3 files changed

+254
-37
lines changed

mlir/lib/Target/LLVMIR/Dialect/OpenMP/OpenMPToLLVMIRTranslation.cpp

Lines changed: 169 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "mlir/Target/LLVMIR/Dialect/OpenMP/OpenMPToLLVMIRTranslation.h"
1414
#include "mlir/Analysis/TopologicalSortUtils.h"
1515
#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
16+
#include "mlir/Dialect/LLVMIR/LLVMTypes.h"
1617
#include "mlir/Dialect/OpenMP/OpenMPDialect.h"
1718
#include "mlir/Dialect/OpenMP/OpenMPInterfaces.h"
1819
#include "mlir/IR/IRMapping.h"
@@ -24,10 +25,12 @@
2425

2526
#include "llvm/ADT/ArrayRef.h"
2627
#include "llvm/ADT/SetVector.h"
28+
#include "llvm/ADT/SmallVector.h"
2729
#include "llvm/ADT/TypeSwitch.h"
2830
#include "llvm/Frontend/OpenMP/OMPConstants.h"
2931
#include "llvm/Frontend/OpenMP/OMPIRBuilder.h"
3032
#include "llvm/IR/DebugInfoMetadata.h"
33+
#include "llvm/IR/DerivedTypes.h"
3134
#include "llvm/IR/IRBuilder.h"
3235
#include "llvm/IR/ReplaceConstant.h"
3336
#include "llvm/Support/FileSystem.h"
@@ -1331,19 +1334,16 @@ findAssociatedValue(Value privateVar, llvm::IRBuilderBase &builder,
13311334

13321335
/// Initialize a single (first)private variable. You probably want to use
13331336
/// allocateAndInitPrivateVars instead of this.
1334-
static llvm::Error
1335-
initPrivateVar(llvm::IRBuilderBase &builder,
1336-
LLVM::ModuleTranslation &moduleTranslation,
1337-
omp::PrivateClauseOp &privDecl, Value mlirPrivVar,
1338-
BlockArgument &blockArg, llvm::Value *llvmPrivateVar,
1339-
llvm::SmallVectorImpl<llvm::Value *> &llvmPrivateVars,
1340-
llvm::BasicBlock *privInitBlock,
1341-
llvm::DenseMap<Value, Value> *mappedPrivateVars = nullptr) {
1337+
/// This returns the private variable which has been initialized. This
1338+
/// variable should be mapped before constructing the body of the Op.
1339+
static llvm::Expected<llvm::Value *> initPrivateVar(
1340+
llvm::IRBuilderBase &builder, LLVM::ModuleTranslation &moduleTranslation,
1341+
omp::PrivateClauseOp &privDecl, Value mlirPrivVar, BlockArgument &blockArg,
1342+
llvm::Value *llvmPrivateVar, llvm::BasicBlock *privInitBlock,
1343+
llvm::DenseMap<Value, Value> *mappedPrivateVars = nullptr) {
13421344
Region &initRegion = privDecl.getInitRegion();
13431345
if (initRegion.empty()) {
1344-
moduleTranslation.mapValue(blockArg, llvmPrivateVar);
1345-
llvmPrivateVars.push_back(llvmPrivateVar);
1346-
return llvm::Error::success();
1346+
return llvmPrivateVar;
13471347
}
13481348

13491349
// map initialization region block arguments
@@ -1363,17 +1363,15 @@ initPrivateVar(llvm::IRBuilderBase &builder,
13631363

13641364
assert(phis.size() == 1 && "expected one allocation to be yielded");
13651365

1366-
// prefer the value yielded from the init region to the allocated private
1367-
// variable in case the region is operating on arguments by-value (e.g.
1368-
// Fortran character boxes).
1369-
moduleTranslation.mapValue(blockArg, phis[0]);
1370-
llvmPrivateVars.push_back(phis[0]);
1371-
13721366
// clear init region block argument mapping in case it needs to be
13731367
// re-created with a different source for another use of the same
13741368
// reduction decl
13751369
moduleTranslation.forgetMapping(initRegion);
1376-
return llvm::Error::success();
1370+
1371+
// Prefer the value yielded from the init region to the allocated private
1372+
// variable in case the region is operating on arguments by-value (e.g.
1373+
// Fortran character boxes).
1374+
return phis[0];
13771375
}
13781376

13791377
/// Allocate and initialize delayed private variables. Returns the basic block
@@ -1415,11 +1413,13 @@ static llvm::Expected<llvm::BasicBlock *> allocateAndInitPrivateVars(
14151413
llvm::Value *llvmPrivateVar = builder.CreateAlloca(
14161414
llvmAllocType, /*ArraySize=*/nullptr, "omp.private.alloc");
14171415

1418-
llvm::Error err = initPrivateVar(
1416+
llvm::Expected<llvm::Value *> privateVarOrError = initPrivateVar(
14191417
builder, moduleTranslation, privDecl, mlirPrivVar, blockArg,
1420-
llvmPrivateVar, llvmPrivateVars, privInitBlock, mappedPrivateVars);
1421-
if (err)
1418+
llvmPrivateVar, privInitBlock, mappedPrivateVars);
1419+
if (auto err = privateVarOrError.takeError())
14221420
return err;
1421+
llvmPrivateVars.push_back(privateVarOrError.get());
1422+
moduleTranslation.mapValue(blockArg, privateVarOrError.get());
14231423
}
14241424
return afterAllocas;
14251425
}
@@ -1730,6 +1730,97 @@ buildDependData(std::optional<ArrayAttr> dependKinds, OperandRange dependVars,
17301730
}
17311731
}
17321732

1733+
namespace {
1734+
/// TaskContextStructManager takes care of creating and freeing a structure
1735+
/// containing information needed by the task body to execute.
1736+
class TaskContextStructManager {
1737+
public:
1738+
TaskContextStructManager(llvm::IRBuilderBase &builder,
1739+
LLVM::ModuleTranslation &moduleTranslation)
1740+
: builder{builder}, moduleTranslation{moduleTranslation} {}
1741+
1742+
/// Creates a heap allocated struct containing space for each private
1743+
/// variable. Returns nullptr if there are is no struct needed. Invariant:
1744+
/// privateVarTypes, privateDecls, and the elements of the structure should
1745+
/// all have the same order.
1746+
void
1747+
generateTaskContextStruct(MutableArrayRef<omp::PrivateClauseOp> privateDecls);
1748+
1749+
/// Create GEPs to access each member of the structure representing a private
1750+
/// variable, adding them to llvmPrivateVars.
1751+
void createGEPsToPrivateVars(SmallVectorImpl<llvm::Value *> &llvmPrivateVars);
1752+
1753+
/// De-allocate the task context structure.
1754+
void freeStructPtr();
1755+
1756+
llvm::Value *getStructPtr() { return structPtr; }
1757+
1758+
private:
1759+
llvm::IRBuilderBase &builder;
1760+
LLVM::ModuleTranslation &moduleTranslation;
1761+
1762+
/// The type of each member of the structure, in order.
1763+
SmallVector<llvm::Type *> privateVarTypes;
1764+
1765+
/// A pointer to the structure containing context for this task.
1766+
llvm::Value *structPtr = nullptr;
1767+
/// The type of the structure
1768+
llvm::Type *structTy = nullptr;
1769+
};
1770+
} // namespace
1771+
1772+
void TaskContextStructManager::generateTaskContextStruct(
1773+
MutableArrayRef<omp::PrivateClauseOp> privateDecls) {
1774+
if (privateDecls.empty())
1775+
return;
1776+
privateVarTypes.reserve(privateDecls.size());
1777+
1778+
for (omp::PrivateClauseOp &privOp : privateDecls) {
1779+
Type mlirType = privOp.getType();
1780+
privateVarTypes.push_back(moduleTranslation.convertType(mlirType));
1781+
}
1782+
1783+
structTy = llvm::StructType::get(moduleTranslation.getLLVMContext(),
1784+
privateVarTypes);
1785+
1786+
llvm::DataLayout dataLayout =
1787+
builder.GetInsertBlock()->getModule()->getDataLayout();
1788+
llvm::Type *intPtrTy = builder.getIntPtrTy(dataLayout);
1789+
llvm::Constant *allocSize = llvm::ConstantExpr::getSizeOf(structTy);
1790+
1791+
// Heap allocate the structure
1792+
structPtr = builder.CreateMalloc(intPtrTy, structTy, allocSize,
1793+
/*ArraySize=*/nullptr, /*MallocF=*/nullptr,
1794+
"omp.task.context_ptr");
1795+
}
1796+
1797+
void TaskContextStructManager::createGEPsToPrivateVars(
1798+
SmallVectorImpl<llvm::Value *> &llvmPrivateVars) {
1799+
if (!structPtr) {
1800+
assert(privateVarTypes.empty());
1801+
return;
1802+
}
1803+
1804+
// Create GEPs for each struct member and initialize llvmPrivateVars to point
1805+
llvmPrivateVars.reserve(privateVarTypes.size());
1806+
llvm::Value *zero = builder.getInt32(0);
1807+
for (auto [i, eleTy] : llvm::enumerate(privateVarTypes)) {
1808+
llvm::Value *iVal = builder.getInt32(i);
1809+
llvm::Value *gep = builder.CreateGEP(structTy, structPtr, {zero, iVal});
1810+
llvmPrivateVars.push_back(gep);
1811+
}
1812+
}
1813+
1814+
void TaskContextStructManager::freeStructPtr() {
1815+
if (!structPtr)
1816+
return;
1817+
1818+
llvm::IRBuilderBase::InsertPointGuard guard{builder};
1819+
// Ensure we don't put the call to free() after the terminator
1820+
builder.SetInsertPoint(builder.GetInsertBlock()->getTerminator());
1821+
builder.CreateFree(structPtr);
1822+
}
1823+
17331824
/// Converts an OpenMP task construct into LLVM IR using OpenMPIRBuilder.
17341825
static LogicalResult
17351826
convertOmpTaskOp(omp::TaskOp taskOp, llvm::IRBuilderBase &builder,
@@ -1744,6 +1835,7 @@ convertOmpTaskOp(omp::TaskOp taskOp, llvm::IRBuilderBase &builder,
17441835
SmallVector<mlir::Value> mlirPrivateVars;
17451836
SmallVector<llvm::Value *> llvmPrivateVars;
17461837
SmallVector<omp::PrivateClauseOp> privateDecls;
1838+
TaskContextStructManager taskStructMgr{builder, moduleTranslation};
17471839
mlirPrivateVars.reserve(privateBlockArgs.size());
17481840
llvmPrivateVars.reserve(privateBlockArgs.size());
17491841
collectPrivatizationDecls(taskOp, privateDecls);
@@ -1796,27 +1888,50 @@ convertOmpTaskOp(omp::TaskOp taskOp, llvm::IRBuilderBase &builder,
17961888
// Allocate and initialize private variables
17971889
// TODO: package private variables up in a structure
17981890
builder.SetInsertPoint(initBlock->getTerminator());
1799-
for (auto [privDecl, mlirPrivVar, blockArg] :
1800-
llvm::zip_equal(privateDecls, mlirPrivateVars, privateBlockArgs)) {
1801-
llvm::Type *llvmAllocType =
1802-
moduleTranslation.convertType(privDecl.getType());
18031891

1804-
// Allocations:
1805-
builder.SetInsertPoint(allocaIP.getBlock()->getTerminator());
1806-
llvm::Value *llvmPrivateVar = builder.CreateAlloca(
1807-
llvmAllocType, /*ArraySize=*/nullptr, "omp.private.alloc");
1808-
1809-
// builder.SetInsertPoint(initBlock->getTerminator());
1810-
auto err =
1892+
// Create task variable structure
1893+
llvm::SmallVector<llvm::Value *> privateVarAllocations;
1894+
taskStructMgr.generateTaskContextStruct(privateDecls);
1895+
// GEPs so that we can initialize the variables. Don't use these GEPs inside
1896+
// of the body otherwise it will be the GEP not the struct which is fowarded
1897+
// to the outlined function. GEPs forwarded in this way are passed in a
1898+
// stack-allocated (by OpenMPIRBuilder) structure which is not safe for tasks
1899+
// which may not be executed until after the current stack frame goes out of
1900+
// scope.
1901+
taskStructMgr.createGEPsToPrivateVars(privateVarAllocations);
1902+
1903+
for (auto [privDecl, mlirPrivVar, blockArg, llvmPrivateVarAlloc] :
1904+
llvm::zip_equal(privateDecls, mlirPrivateVars, privateBlockArgs,
1905+
privateVarAllocations)) {
1906+
llvm::Expected<llvm::Value *> privateVarOrErr =
18111907
initPrivateVar(builder, moduleTranslation, privDecl, mlirPrivVar,
1812-
blockArg, llvmPrivateVar, llvmPrivateVars, initBlock);
1813-
if (err)
1908+
blockArg, llvmPrivateVarAlloc, initBlock);
1909+
if (auto err = privateVarOrErr.takeError())
18141910
return handleError(std::move(err), *taskOp.getOperation());
1911+
1912+
llvm::IRBuilderBase::InsertPointGuard guard(builder);
1913+
builder.SetInsertPoint(builder.GetInsertBlock()->getTerminator());
1914+
1915+
// TODO: this is a bit of a hack for Fortran character boxes
1916+
if ((privateVarOrErr.get() != llvmPrivateVarAlloc) &&
1917+
!mlir::isa<LLVM::LLVMPointerType>(blockArg.getType())) {
1918+
builder.CreateStore(privateVarOrErr.get(), llvmPrivateVarAlloc);
1919+
// Load it so we have the value pointed to by the GEP
1920+
llvmPrivateVarAlloc = builder.CreateLoad(privateVarOrErr.get()->getType(),
1921+
llvmPrivateVarAlloc);
1922+
}
1923+
assert(llvmPrivateVarAlloc->getType() ==
1924+
moduleTranslation.convertType(blockArg.getType()));
1925+
1926+
// Mapping blockArg -> llvmPrivateVarAlloc is done inside the body callback
1927+
// so that OpenMPIRBuilder doesn't try to pass each GEP address through a
1928+
// stack allocated structure.
18151929
}
18161930

18171931
// firstprivate copy region
18181932
if (failed(initFirstPrivateVars(builder, moduleTranslation, mlirPrivateVars,
1819-
llvmPrivateVars, privateDecls, copyBlock)))
1933+
privateVarAllocations, privateDecls,
1934+
copyBlock)))
18201935
return llvm::failure();
18211936

18221937
// Set up for call to createTask()
@@ -1826,6 +1941,22 @@ convertOmpTaskOp(omp::TaskOp taskOp, llvm::IRBuilderBase &builder,
18261941
InsertPointTy codegenIP) -> llvm::Error {
18271942
// translate the body of the task:
18281943
builder.restoreIP(codegenIP);
1944+
1945+
// Find and map the addresses of each variable within the task context
1946+
// structure
1947+
taskStructMgr.createGEPsToPrivateVars(llvmPrivateVars);
1948+
for (auto [blockArg, llvmPrivateVar] :
1949+
llvm::zip_equal(privateBlockArgs, llvmPrivateVars)) {
1950+
// Fix broken pass-by-value case for Fortran character boxes
1951+
if (!mlir::isa<LLVM::LLVMPointerType>(blockArg.getType())) {
1952+
llvmPrivateVar = builder.CreateLoad(
1953+
moduleTranslation.convertType(blockArg.getType()), llvmPrivateVar);
1954+
}
1955+
assert(llvmPrivateVar->getType() ==
1956+
moduleTranslation.convertType(blockArg.getType()));
1957+
moduleTranslation.mapValue(blockArg, llvmPrivateVar);
1958+
}
1959+
18291960
auto continuationBlockOrError = convertOmpOpRegions(
18301961
taskOp.getRegion(), "omp.task.region", builder, moduleTranslation);
18311962
if (failed(handleError(continuationBlockOrError, *taskOp)))
@@ -1837,6 +1968,9 @@ convertOmpTaskOp(omp::TaskOp taskOp, llvm::IRBuilderBase &builder,
18371968
llvmPrivateVars, privateDecls)))
18381969
return llvm::make_error<PreviouslyReportedError>();
18391970

1971+
// Free heap allocated task context structure at the end of the task.
1972+
taskStructMgr.freeStructPtr();
1973+
18401974
return llvm::Error::success();
18411975
};
18421976

mlir/test/Target/LLVMIR/openmp-llvm.mlir

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2829,12 +2829,13 @@ llvm.func @task(%arg0 : !llvm.ptr) {
28292829
// CHECK: %[[VAL_14:.*]] = load ptr, ptr %[[VAL_13]], align 8
28302830
// CHECK: br label %task.body
28312831
// CHECK: task.body: ; preds = %task.alloca
2832+
// CHECK: %[[VAL_15:.*]] = getelementptr { i32 }, ptr %[[VAL_14]], i32 0, i32 0
28322833
// CHECK: br label %omp.task.region
28332834
// CHECK: omp.task.region: ; preds = %task.body
2834-
// CHECK: call void @foo(ptr %[[VAL_14]])
2835+
// CHECK: call void @foo(ptr %[[VAL_15]])
28352836
// CHECK: br label %omp.region.cont
28362837
// CHECK: omp.region.cont: ; preds = %omp.task.region
2837-
// CHECK: call void @destroy(ptr %[[VAL_14]])
2838+
// CHECK: call void @destroy(ptr %[[VAL_15]])
28382839
// CHECK: br label %task.exit.exitStub
28392840
// CHECK: task.exit.exitStub: ; preds = %omp.region.cont
28402841
// CHECK: ret void
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// RUN: mlir-translate -mlir-to-llvmir %s | FileCheck %s
2+
3+
omp.private {type = private} @privatizer : i32
4+
5+
omp.private {type = firstprivate} @firstprivatizer : i32 copy {
6+
^bb0(%arg0: !llvm.ptr, %arg1: !llvm.ptr):
7+
%0 = llvm.load %arg0 : !llvm.ptr -> i32
8+
llvm.store %0, %arg1 : i32, !llvm.ptr
9+
omp.yield(%arg1 : !llvm.ptr)
10+
}
11+
12+
llvm.func @task_privatization_test() {
13+
%c0 = llvm.mlir.constant(0: i32) : i32
14+
%c1 = llvm.mlir.constant(1: i32) : i32
15+
%0 = llvm.alloca %c1 x i32 : (i32) -> !llvm.ptr
16+
%1 = llvm.alloca %c1 x i32 : (i32) -> !llvm.ptr
17+
llvm.store %c0, %0 : i32, !llvm.ptr
18+
llvm.store %c1, %1 : i32, !llvm.ptr
19+
20+
omp.task private(@privatizer %0 -> %arg0, @firstprivatizer %1 -> %arg1 : !llvm.ptr, !llvm.ptr) {
21+
%2 = llvm.load %arg1 : !llvm.ptr -> i32
22+
llvm.store %2, %arg0 : i32, !llvm.ptr
23+
omp.terminator
24+
}
25+
llvm.return
26+
}
27+
28+
// CHECK: define void @task_privatization_test()
29+
// CHECK: %[[STRUCT_ARG:.*]] = alloca { ptr }, align 8
30+
// CHECK: %[[VAL_0:.*]] = alloca i32, align 4
31+
// CHECK: %[[VAL_1:.*]] = alloca i32, align 4
32+
// CHECK: store i32 0, ptr %[[VAL_0]], align 4
33+
// CHECK: store i32 1, ptr %[[VAL_1]], align 4
34+
// CHECK: br label %entry
35+
// CHECK: entry:
36+
// CHECK: br label %omp.private.init
37+
// CHECK: omp.private.init:
38+
// CHECK: %[[VAL_5:.*]] = tail call ptr @malloc(i64 ptrtoint (ptr getelementptr ([[STRUCT_KMP_PRIVATES_T:.*]], ptr null, i32 1) to i64))
39+
// CHECK: %[[VAL_7:.*]] = getelementptr { i32, i32 }, ptr %[[VAL_5]], i32 0, i32 0
40+
// CHECK: %[[VAL_8:.*]] = getelementptr { i32, i32 }, ptr %[[VAL_5]], i32 0, i32 1
41+
// CHECK: br label %omp.private.copy1
42+
// CHECK: omp.private.copy1:
43+
// CHECK: %[[VAL_10:.*]] = load i32, ptr %[[VAL_1]], align 4
44+
// CHECK: store i32 %[[VAL_10]], ptr %[[VAL_8]], align 4
45+
// CHECK: br label %omp.private.copy
46+
// CHECK: omp.private.copy:
47+
// CHECK: br label %omp.task.start
48+
// CHECK: omp.task.start:
49+
// CHECK: br label %codeRepl
50+
// CHECK: codeRepl:
51+
// CHECK: %[[GEP_OMP_TASK_CONTEXT_PTR:.*]] = getelementptr { ptr }, ptr %[[STRUCT_ARG]], i32 0, i32 0
52+
// CHECK: store ptr %[[VAL_5]], ptr %[[GEP_OMP_TASK_CONTEXT_PTR]], align 8
53+
// CHECK: %[[VAL_14:.*]] = call i32 @__kmpc_global_thread_num(ptr @1)
54+
// CHECK: %[[VAL_15:.*]] = call ptr @__kmpc_omp_task_alloc(ptr @1, i32 %[[VAL_14]], i32 1, i64 40, i64 8, ptr @task_privatization_test..omp_par)
55+
// CHECK: %[[ALLOCATED_TASK_STRUCT:.*]] = load ptr, ptr %[[VAL_15]], align 8
56+
// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr align 1 %[[ALLOCATED_TASK_STRUCT]], ptr align 1 %[[STRUCT_ARG]], i64 8, i1 false)
57+
// CHECK: %[[VAL_16:.*]] = call i32 @__kmpc_omp_task(ptr @1, i32 %[[VAL_14]], ptr %[[VAL_15]])
58+
// CHECK: br label %[[VAL_17:.*]]
59+
// CHECK: task.exit:
60+
// CHECK: ret void
61+
62+
// CHECK-LABEL: define internal void @task_privatization_test..omp_par(
63+
// CHECK-SAME: i32 %[[GLOBAL_TID_VAL:.*]], ptr %[[OMP_TASK_CONTEXT_PTR_PTR_PTR_PTR:.*]])
64+
// CHECK: task.alloca:
65+
// CHECK: %[[OMP_TASK_CONEXT_PTR_PTR_PTR:.*]] = load ptr, ptr %[[OMP_TASK_CONTEXT_PTR_PTR_PTR_PTR]], align 8
66+
// CHECK: %[[OMP_TASK_CONTEXT_PTR_PTR:.*]] = getelementptr { ptr }, ptr %[[OMP_TASK_CONTEXT_PTR_PTR_PTR:.*]], i32 0, i32 0
67+
// CHECK: %[[OMP_TASK_CONTEXT_PTR:.*]] = load ptr, ptr %[[OMP_TASK_CONTEXT_PTR_PTR:.*]], align 8
68+
// CHECK: br label %[[VAL_18:.*]]
69+
// CHECK: task.body: ; preds = %[[VAL_19:.*]]
70+
// CHECK: %[[VAL_20:.*]] = getelementptr { i32, i32 }, ptr %[[OMP_TASK_CONTEXT_PTR]], i32 0, i32 0
71+
// CHECK: %[[VAL_22:.*]] = getelementptr { i32, i32 }, ptr %[[OMP_TASK_CONTEXT_PTR]], i32 0, i32 1
72+
// CHECK: br label %[[VAL_23:.*]]
73+
// CHECK: omp.task.region: ; preds = %[[VAL_18]]
74+
// CHECK: %[[VAL_24:.*]] = load i32, ptr %[[VAL_22]], align 4
75+
// CHECK: store i32 %[[VAL_24]], ptr %[[VAL_20]], align 4
76+
// CHECK: br label %[[VAL_25:.*]]
77+
// CHECK: omp.region.cont: ; preds = %[[VAL_23]]
78+
// CHECK: tail call void @free(ptr %[[OMP_TASK_CONTEXT_PTR]])
79+
// CHECK: br label %[[VAL_26:.*]]
80+
// CHECK: task.exit.exitStub: ; preds = %[[VAL_25]]
81+
// CHECK: ret void
82+

0 commit comments

Comments
 (0)