Motivation
There are a few tools in the LLVM world that need to convert LLVM IR—which is now moving to opaque pointers—to an IR whose pointer types are still typed. The main ones I’m aware of are the DXIL backend and the SPIR-V backend. In an opaque pointer world, it is necessary to recover the types of pointers to be able to properly emit these IR, and getting these types wrong can result in code that just plain doesn’t work.
To be clear, this doesn’t seek to roll back opaque pointers, or enforce everybody else in LLVM to worry about the burden of typed pointers for a few small backends. As it turns out, in my work on converting the LLVM-to-SPIR-V translator to support opaque pointer IR, I have found that I can get most of the way there with the existing features that LLVM has. The following code is a sketch of the translation process that highlights the basic philosophy:
PointerUnion<Type *, DeferredType *> getPointerElementType(Value *V) {
if (auto *GV = dyn_cast<GlobalVariable>(V)) {
// Some values have obvious types…
return GV->getValueType();
} else if (auto *Select = dyn_cast<SelectInst>(V)) {
// … others propagate types…
return getPointerElementType(Select->getFalseOperand());
} else if (isa<IntToPtrInst>(V)) {
// … and some have no intrinsic type, and must be inferred from use.
return new DeferredType(V);
}
}
// NB: Preparatory pass needed to convert all constant expressions into instructions
void typeCheckInstruction(Instruction *I) {
SmallVector<std::pair<Use *, Type *>, 4> Uses;
// For each operand, collect the correct type at each use site.
collectUseTypes(I, Uses);
for (auto &Pair : Uses) {
if (getPointerElementType(*Pair.first) != Pair.second) {
// Create a bitcast ptr %arg to ptr so we can give it a different type.
CastInst::CreatePointerCast(*Pair.first, Pair.first->getType());
}
}
}
If the goal of opaque pointers is to make LLVM passes generally not have to worry about spurious bitcasts obscuring pointer sources, then the primary task of recovering typed pointers is to reinsert where those bitcasts would have been. And, for the most part, putting those bitcasts in different places than where they used to be is not an issue. However, there are a few cases where this is unsafe.
The first major class of issues is function parameters. Especially when you make a function call that crosses a module boundary—whether it’s the case of the kernel whose caller will be the driver code, or the case of a function declaration whose definition will be in another library—it creates issues if the function types are not the same. The linking procedures for these libraries can’t link a function foo if one module says it takes an i32*
parameter and another says it takes an i8*
parameter. These languages are designed without mandatory support for function pointers, and handling mismatched arguments by bitcasting from one function type to another is not possible. It is thus essential for frontends to indicate the correct function types so that the backends can translate them correctly.
The second major class of issues is that OpenCL has several opaque types (such as image, event, sampler types) that need to be precisely preserved through to the end of the translation process. In the SPIR-V backend, it is simply not possible to express a bitcast to or from these types. These types are presently represented in the SPIR-V target as pointers-to-opaque-structs with a particular name, and in a typed pointer world, LLVM generally tries sufficiently hard enough to represent these types that it doesn’t cause issues. But when opaque pointers are enabled, optimization passes are less eager to throw away this information—I’ve already observed one optimization pass shrugging its shoulders and replacing the pointer type with an i64
, leaving me with IR that is simply not possible to codegen.
Because of these issues, I would like to propose two modifications to LLVM IR to make it possible to handle generation of SPIR-V, DXIL, and similar backends in an opaque pointer world. In addition, there are two changes to LLVM I also propose that would help the translation process but are not themselves necessary.
Better type information at function boundaries
The first change I want to propose is to allow the elementtype
attribute to be present on non-intrinsic functions, while also expanding its scope to also be a return attribute. This would allow front-ends to indicate what the correct pointer element type ought to be for the function parameter or return value (note that support for return values is as important as parameters here). I’m proposing to reuse the elementtype
attribute here on the basis that a) it imparts no other special semantics other than “this is what the pointer element type would be were the pointer typed” and b) it requires minimal modifications to LLVM to add this support (a few changes to the verifier, and a tweak to Attributes.td
).
Alternatives
As mentioned previously, something along the lines of this change is going to be necessary. There is a draft patch to convey this information via metadata here: ⚙ D127579 [clang][WIP] add option to keep types of ptr args for non-kernel functions in metadata, but, as discussed in the most recent GPU working group meeting, that approach has flaws. It relies on emitting types in the front-end representation, which is somewhat inconvenient to reparse (and more difficult for frontends that do not start with OpenCL code). It also relies on conveying information via metadata, which is more liable to being dropped by an optimization pass. In any case, maintaining information essential to correctness in the form of metadata constitutes an abuse of the purpose of metadata, as dropping metadata isn’t supposed to cause the program to break.
Another alternative that was brought up in the patch is conveying type information via mangled names. While it is possible to recover the type information via a mangled name, this requires building in an Itanium name demangler into the backend infrastructure to recover relevant information, and handling the maximum possible insanity of name manglings. While LLVM does have an internal demangler that can be leveraged, going from “here’s a mangled string” to “this is the type of every parameter” is still challenging with that API. The LLVM-SPIRV translator contains a use of that API to recover a small subset of important types for various optimization passes—it would take a fair bit more work to extend that to generally recover all the types that would be needed to properly generate function types.
Opaque types in LLVM
Another change I would like to propose is to add a new system of opaque types to LLVM. Effectively, we would add a new way kind of opaque type, e.g., opaque("opencl.sampler_t")
, that would replace what is currently handled in SPIR-V as pointers-to-opaque-structs. These types would be usable as first-class types, and could exist as SSA values, function parameters or return types, members of struct types or array types. It would be possible to pass these arounds as values through phi statements, or allocate them via alloca and load/store them as appropriate. It would not be possible in general to bitcast to or from these types.
Ideally, it would be nice to specify these types as having an unknown size, much like scalable vector types, but if that is not workable; it is probably sufficient to have their type definition ascribe a nominal size to them (say, the size of a pointer).
It was pointed out in the most recent GPU working group meeting that it would also be nice to have the names be parameterizable: the SPIR-V Image type contains 8 different parameters (sample type, dimension, depth, arrayed, multisampled, sampled, format, and access kind), but it isn’t that big of a deal to encode these parameters into a single string (which is already what happens today these types).
I’m not entirely certain what level of constant support is needed for these types. A zeroing initializer like ConstantPointerNull
or ConstantAggregateZero
is needed (for at least some of these types). Similarly, having undef
and poison
values for these types are plausible and may indeed be necessary for our memory semantics anyways. There are, I believe, a few cases where more complex constant parameters for these types may need to be constructed, but it’s possible that this can be worked around: I have yet to do any prototyping on this aspect of the proposal.
Semantically, these types and values of these types would generally be unintrospectable by a target-independent pass. Targets could ascribe particular meanings to particular opaque types, and therefore optimize them as they see fit.
Since I haven’t yet attempted to prototype this, there may be roadblocks and other issues that I haven’t considered. I am aware that this change is likely to be contentious, so I wanted to gauge the willingness of the community to potentially move in this direction before taking the effort necessary to start prototyping. I would imagine, however, that various kinds of opaque types might be useful for other projects. Indeed, the existing x86_amx
and x86_mmx
types might be replaced with an x86-specific opaque type were this part of the proposal accepted.
Alternatives
For alternatives, there is first the existing approach of pointers-to-opaque-structs with specific names. Given the general semantics of LLVM, this approach was always somewhat problematic. But with opaque pointers, pointer manipulation become almost completely agnostic of pointer types and the ability to preserve opaque struct types as pointer element types itself vanishes—it would not surprise me if someone were to propose removing opaque struct types altogether once typed pointers are removed. I do not believe this alternative is even remotely feasible.
The other major alternative that has been discussed is representing these opaque types as pointers in different address spaces. In a simple experiment I conducted (after discovering that existing passes caused one of my test cases to generate an unacceptable ptrtoint
instruction), I found that using a separate address space was insufficient to keep the type maintained as a pointer and not an integer, although I did not do a follow-up test to see if indicating the address space as being a non-integral pointer type. Making these address spaces be non-integral is a possibility, but given that the parameterization of some of these types means we would require a few hundred thousand address spaces at least, we would at least need changes to the data layout string representation to compress the specification to something more manageable.
A typing pass infrastructure
This is in the “nice to have” category. Given that there are multiple backends that are going to want this typing functionality, it would be nice if there were a common infrastructure that anyone who wanted types could use. While the implementation of such a pass can be rather straightforward, there is a nontrivial amount of work needed to identify that, for example, the ptr-valued extractvalue instruction needs to have the same type as the second parameter of the cmpxchg instruction whose result the extractvalue is using (I daresay that many readers would not have thought this a case that needed to be handled).
Presently, there are two independent implementations of this that I am aware of. The first is the analysis that @beanz has created for the DXIL backend. The second is the typing pass I have created for the LLVM-SPIRV translator. The latter is a more complete effort, since I could leverage a fuller existing test suite to uncover more corner cases in the IR effort.
Typed pointer types
In my current implementation, I’ve represented pointer types largely by means of an llvm::Type*
representing the pointer element type (together with the original llvm::Type*
, which contains the address space information). This approach does not scale to multiple levels of indirection (but needing to support multiple levels is unnecessary). More importantly, it does not scale well to where types are embedded in other types, especially function types (although struct types may also be a point of concern).
Representing a type system with pointer types properly effectively requires duplicating a decent fraction of the LLVM type system. Having a special pointer type allows existing infrastructure to represent the correct types of pointers in nested type specifications, such as in function types, vectors of pointers, or struct types. A side benefit is that it makes it easier to indicate when a use of a pointer type has a different pointer element type from its definition, as you could use bitcasts with different types (while it is possible to generate no-op bitcast ptr to ptr
instructions, it is not possible to do so for constant expressions). The DXIL backend for LLVM has opted to create its own extension to llvm::Type
for this purpose in ⚙ D122268 Add PointerType analysis for DirectX backend, and it would be nice to generalize this approach for other targets.
My expectation is that such a new typed pointer type would not be generally legal to use in LLVM—the validator would consider any IR that used such a type to be invalid. Instead, this type would be used in limited form in conjunction with the also-proposed pointer typing pass to represent the output.
This proposal also fits in the “nice to have” category; it’s not necessary for such a type to exist to do the typing. However, without such a type, some amount of contortion becomes necessary to avoid having to deal with the extra necessary infrastructure. For example, the implementation pass I wrote uses a PointerUnion<Type *, Value *>
to be able to express pointer-to-pointer references.