[InstCombine] addrspacecast assumed associative with gep

The following combine(-enabling transformation) makes me uncomfortable:

gep(addrspacecast(gep p0) to p1)

addrspacecast(gep(gep p0)) to p1

It’s applied at visitAddrSpaceCast in InstCombineCasts.cpp.

Before this, I’d always assumed address spaces were very much “user domain”. Datalayout even supports marking a space as “non-integral”, to designate that manipulation as bits is impossible. In my case, I have a different byte width in p1 over p0 (I know - but we all knew it was coming). All of p0 can be addressed in p1, but the reverse transformation is only sometimes possible as GEPs behave differently on the other side of the cast. This transformation of course breaks that.

By my reading of ISO 18037:2006(E), it is required that if one address in p0 can be cast to p1, they all can, but I cannot find any requirements placed on the reverse operation - and further, this is a C standard, not a low-level machine.

I realise this may be seen as an abuse of mechanics, but is it really intended that addrspacecast and geps can be rearranged in all circumstances, in all address space combinations? To me, the fewer assumptions made about addrspaces the better - callbacks should be used before optimising past them.

What do you mean exactly by "behave differently on the other side of the cast”? Do you have a concrete example?

Questions about what’s allowed with addrspacecast arithmetic have come up before (e.g. https://reviews.llvm.org/D31924) but it’s never really converged into explicit guidance for the LangRef. Is this with a non-integral address space? I would like to make the LangRef clearer on what’s allowed and what’s not. Having some concrete examples would be helpful to clarify this, and possibly remove the combine.

-Matt

What do you mean exactly by "behave differently on the other side of the cast”? Do you have a concrete example?

I was hesitant to say only in that it is probably an "abuse of mechanics" and definitely playing with fire, _however_ the target I'm working on has extensive bit operations for a subset of memory, including atomic test-and-set, etc. It's convenient to be able to pass around addresses of these bits, in particular for handling SFRs and interfacing with peripherals that require the use of these instructions. I know this could be handled with intrinsic functions (as with much in LLVM), however this removes the opportunity for globally allocating low cost atomic flags.

Modelled as address spaces, p0 and p<bit> would have different CHAR_BITs, of 8 and 1 respectively. This somewhat works as one would expect too, with, ((bit *)&somevalue)[3] producing bit3 of somevalue - at least until the first time you access through a struct or array:

  gep(addrspacecast(gep p0, 4) to p<bit>, 3) == (p0 + 4) * 8 + 3

As LLVM optimises this expression to:

  addrspacecast(gep p0, 4 + 3) to p<bit>

Producing something entirely different.

But that said, I cannot think of a case where this would be a problem _except_ where CHAR_BIT would be different in one addrspace to the other, which LLVM doesn't claim to support AFAIK. Or somewhat generalised, where pX is a subset of pY, but pY has other addressable values between each valid pX in pY.

I note LLVM assumes that an address will load the same value regardless of what addrspace it is read from, which I understand as matching GPU behaviour well. I know some processors have different opcodes to retrieve/store high and low instruction words through the same pointer, which means they could not be modelled as separate addrspaces either, but rather requiring implicit functions to dereference - and this is likely to be the better way, to be fair.

My personal thoughts are that LLVM should not require these assumptions of GPU-style addrspace layouts, but offer a target hook to make addrspacecast a blackbox for gep and load/store combining. Or at least spell out somewhere exactly what the rules are on addrspacecast, which seems to be what's missing at the moment - is it something that can be used to describe a form of addressable memory, or is it formally required that they share the same byte size and that if an address is castable from one addrspace to another, that those values are strict aliases of one another.

> What do you mean exactly by "behave differently on the other side of the cast”? Do you have a concrete example?

I was hesitant to say only in that it is probably an "abuse of mechanics" and definitely playing with fire, _however_ the target I'm working on has extensive bit operations for a subset of memory, including atomic test-and-set, etc. It's convenient to be able to pass around addresses of these bits, in particular for handling SFRs and interfacing with peripherals that require the use of these instructions. I know this could be handled with intrinsic functions (as with much in LLVM), however this removes the opportunity for globally allocating low cost atomic flags.

Modelled as address spaces, p0 and p<bit> would have different CHAR_BITs, of 8 and 1 respectively. This somewhat works as one would expect too, with, ((bit *)&somevalue)[3] producing bit3 of somevalue - at least until the first time you access through a struct or array:

        gep(addrspacecast(gep p0, 4) to p<bit>, 3) == (p0 + 4) * 8 + 3

As LLVM optimises this expression to:

        addrspacecast(gep p0, 4 + 3) to p<bit>

Producing something entirely different.

I'm trying to figure out what the actual pointer types would be in
your, and I keep coming back to the fact that the underlying problem
here seems to be that the meaning of i1 in memory is horribly
confused...

That is, I suspect that the inner gep in your first example should be
an i8 gep and the outer gep should be an i1 gep, which means that a
pointer bitcast is missing somewhere.

I think you're right in that we probably need to say something about
which types a gep can actually operate on in a well-defined manner, on
a per-address space basis. This may be entirely based on the byte
size.

Cheers,
Nicolai

I quickly gave up on s1, as you say - InstCombine gets stuck in loops and all kinds of terrible things. For interfacing with Clang, treating them as bools works well enough.

You are right though, it had completely skipped my mind that the gep there includes the width of those s8 bools, so it's really not as bad as I thought, *head desk* moment.

Only really remains the undocumented point of load/store aliasing where the same address can be read/written in different ways (ie microchip's tblrdl/tblrdh program memory operations - high byte is aliased to the low byte, but accessed via different operations), but I agree we're getting minor here and such false-aliasing is probably better blocked via intrinsic functions. Sorry for the noise :blush:

Do you mean load pointers are little endian in one address space and big endian in another?

-Matt

Since Matt posted this, he has indeed tried to improve the LangRef:

https://reviews.llvm.org/D63525

I'd encourage anyone who is using address spaces to give feedback on this review. This is a seriously underspecified area of LLVM IR semantics and I'm *very* happy to see Matt taking the lead in improving it!

David