[cfe-dev] [RFC] Introducing a byte type to LLVM

Nuno Lopes via cfe-dev cfe-dev at lists.llvm.org
Sun Jun 6 11:47:33 PDT 2021


I think Joshua gave a very nice motivation already. I'll just complement with an alias analysis example. (plus see my reply to Chris)

define @f() {
  %p = alloca i8
  %q = alloca i8**
  store i8 42, %p
  store i8* %p, i8** %q

  %pp = some ptr arithmetic; full may-alias
  %v = load i64, %pp
  call @g(%v)

  %w = load i8, %p  ; call we store-forward & RAUW 42?
}

So, we pass an integer to some function. If we don't distinguish between pointers and integers (current memory model), we need to assume that %v may carry the address of %p, as the load may have done an implicit pointer-to-integer cast. Therefore, pointer %p is escaped.
Thus, in this model we cannot do store forwarding of 42 to the load of %w. Except that LLVM doesn't consider integer loads as escaping pointers. This is wrong, and that's one of the reasons why we miscompile integer/pointer casts. The other reason is what I explained in the reply to John & Chris, which is that some integer optimizations aren't correct for pointers, so we cannot represent pointers as integers (without appropriate p2i/i2p casts).

Another model is to say that integers can only carry integers. Above there's no pointer-to-integer cast of %p, hence %v cannot have the address of %p, and thus store-forwarding can happen.
This model requires a "universal holder" different than (ab)using integers.

Introducing a byte type means we only need to careful with char loads, but not with short, int, long, etc loads. Clang would lower char loads to byte loads, and the remaining integer types straight to i<n> loads. So we can go full speed on integers, and throttle down just on chars, which is all we can do because the semantics of chars in C.

Nuno


-----Original Message-----
From: Johannes Doerfert <johannesdoerfert at gmail.com> 
Sent: 04 June 2021 22:12
To: Nuno Lopes <nunoplopes at sapo.pt>; 'George Mitenkov' <georgemitenk0v at gmail.com>
Cc: 'Ralf Jung' <jung at mpi-sws.org>; cfe-dev at lists.llvm.org; llvm-dev at lists.llvm.org
Subject: Re: [RFC] Introducing a byte type to LLVM

I think the lack of a end-to-end example is making this harder (for me) to understand.


On 6/4/21 2:17 PM, Nuno Lopes wrote:
> It's true: an alternative to introducing a new byte type is to make alias analysis more conservative. But I think it's way worse than you think.
> Making AA conservative means that every single integer load must be considered to be escaping a pointer.

What pointer is escaping if I have an integer load. I'm sorry for my 
questions but I assume I'm not the only one that might
want to understand this properly. So far, I've assumed if I store a 
pointer away w/o knowing what happens to the memory the
pointer escapes. This is our current model and it's not that bad.


>   Unless you know the last stored value was an integer, you would have to conservatively assume that the load may be escaping some pointer (even if only partially). This sounds to me like terrible. Type punning through memory is probably very uncommon, and it seems we would be slowing down all programs because of such uncommon case.
>
> Therefore I feel it's best if we introduce a way to explicitly mark potential type punning situations, such that AA (and optimizers) only need to be conservative when needed. It's not the case that right now we can detect the few potential type punning cases and be conservative just there. We would need to be conservative in e.g. every function that loads an argument pointer.
> I agree it's scary to introduce a new type in the IR; we don't do that very often. But we need to fix the bugs in LLVM.

What doesn't add up for me is that one the one side the argument 
"everything needs to be conservative" w/o byte types, but at the same 
time we can somehow determine when to use byte types locally? If the 
latter would not be the case, how would we know when we need a byte type?

Why would we not be conservative/explicit about type punning whenever we 
would introduce byte types with this proposal? I unfortunately don't 
understand what the byte types give us. My best guess is that they 
explicitly mark byte operations on pointers, which we could do otherwise 
I assume (explicit p2i, i2p). If there is more to it, I think we need an 
end to end example that shows how we cannot make type punning explicit 
with current means and a new type is conceptually better.

~ Johannes


>
> Nuno
>   
>
> -----Original Message-----
> From: Johannes Doerfert via llvm-dev
> Sent: 04 June 2021 17:56
> Subject: Re: [llvm-dev] [cfe-dev] [RFC] Introducing a byte type to LLVM
>
>
> On 6/4/21 11:35 AM, George Mitenkov wrote:
>> Hi Johannes,
>>
>> Sure! The underlying problem is that raw-memory access handlers are
>> treated as integers, while they are not really integers. Especially
>> std::byte that specifically states that it has raw-memory access
>> semantics. This semantic mismatch can make AA wrong and a pointer to
>> escape.
> I would assume being conservative is a possibility here, right?
> So if someone wants to type-pun a pointer we would do very little optimizations around it.
>
>
>> Consider the following LLVM IR that copies a pointer:
>>
>> %src8 = bitcast i8** %src to i8*
>>
>> %dst8 = bitcast i8** %dst to i8*
>>
>> call void @llvm.memcpy.p0i8.p0i8.i32(i8* %dst8, i8* %src8, i32 8, i1
>> false)
>>
>> %load = load i8*, i8** %dst
>>
>> %addr = ptrtoint i8* %load to i64
>>
>> ret i64 %addr
>>
>>
>> If we optimize the call to memcpy, then the IR becomes
>>
>>
>> %src64 = bitcast i8** %src to i64*
>>
>> %dst64 = bitcast i8** %dst to i64*
>>
>> %addr = load i64, i64* %src64, align 1
>>
>> store i64 %addr, i64* %dst64, align 1
>>
>> ret i64 %addr
>>
>>
>> Since there is no "data" type in LLVM like a byte, the ptrtoint is
>> optimized out and the pointer escapes. If we have used a pair of byte
>> load/store that would not happen.
> The pointer (load %src) does escape in this example, doesn't it?
>
> I don't get why "that would not happen" with byte load/store and why that would be correct at all.
>
>
>> The mentioned bug is just an example, but this pattern can be
>>
>> replicated in other cases that optimize memcpy as integer load/stores and
>> drop ptrtoint,
>>
>> undermining alias analysis. To illustrate further, consider loading a
>> pointer like:
>>
>>
>> %v1 = load i8*, %p
>>
>> %v2 = load i8*, %p
>>
>>
>> Instcombine optimizes this into
>>
>>
>> %v1 = load i64, %p
>>
>> %v2 = inttoptr %v1
>>
>>
>> changing the underlying provenance (again, not good for AA).
> This example looks somehow broken (I'll assuming %v1 has integer type).
>
> I agree this is not a great optimization, but is it wrong? We should have
> done the opposite (ptr2int for v1) but even then I'm not sure what AA could
> derive here. We load a pointer and then we escape it into an integer. If the
> ptr2int is not removed, I'm not sure what AA should do about this. I'm also
> not sure how practical this is. Do we have some idea how often such
> cases exist?
>
>
>>    If we had a
>> byte type instead,
>>
>> then we could
>>
>> 1. preserve provenance
>>
>> 2. differentiate between integers and the raw data copied
>>
>>
>> This might be beneficial for some backends (e.g CHERI) that want to
>> differentiate between
>>
>> true integers and bytes that can be integers. The optimizations for known
>> integers
>>
>> can become more aggressive in this case.
>>
> It sounds to me like the problem is our optimization in the presence of
> int2ptr and ptr2int are (still) unsound. We should at least not remove
> ptr2int,
> is that it?
>
> That said, I still don't get how a byte type helps for these examples.
> More concretely:
>
> ref 1) How does a byte type preserve provenance? If it is somehow
> implicitly attached
>          would that not mean we need to now be careful doing
> optimizations on bytes (as we
>          should now be careful doing optimizations around p2i and i2p)?
> Literally shifting
>          the problem from looking for p2i and i2p to looking for bytecast?
>
> ref 2) What optimization can I do if I have the differentiation of
> integers and raw data?
>          Can we somehow get an idea of the optimization potential?
>
>
> ~ Johannes
>
>
>
>> Thanks,
>>
>> George
>>
>> On Fri, Jun 4, 2021 at 7:03 PM Johannes Doerfert <johannesdoerfert at gmail.com>
>> wrote:
>>
>>> Hi George,
>>>
>>> On 6/4/21 10:24 AM, George Mitenkov via cfe-dev wrote:
>>>> Hi all,
>>>>
>>>> Together with Nuno Lopes and Juneyoung Lee we propose to add a new byte
>>>> type to LLVM to fix miscompilations due to load type punning. Please see
>>>> the proposal below. It would be great to hear the
>>>> feedback/comments/suggestions!
>>>>
>>>>
>>>> Motivation
>>>> ==========
>>>>
>>>> char and unsigned char are considered to be universal holders in C. They
>>>> can access raw memory and are used to implement memcpy. i8 is the LLVM’s
>>>> counterpart but it does not have such semantics, which is also not
>>>> desirable as it would disable many optimizations.
>>>>
>>>> We therefore propose to introduce a new byte type that would have a
>>> raw-memory
>>>> access semantics and can be differentiated from i8. This can help to fix
>>>> unsound optimizations and make the lowering of char, unsigned char or
>>>> std::byte correct. Currently, we denote the byte type as b<N>, where N is
>>>> the number of bits.
>>>>
>>>> In our semantics, byte type carries provenance and copying bytes does not
>>>> escape pointers, thereby benefiting alias analysis. If the memory
>>> contains
>>>> poison bits, then the result of loading a byte from it is bitwise poison.
>>> Could you elaborate on the motivation a bit. I'm not sure I follow.
>>> Maybe examples would be good. The only one I found is in
>>> https://bugs.llvm.org/show_bug.cgi?id=37469. For that one I'm
>>> wondering how much we would in reality loose if we make the required
>>> ptr2int explicit when people cast a pointer to an integer. In general,
>>> explicit encoding seems to me preferable and overall less work (new
>>> types kinda scare me TBH).
>>>
>>> ~ Johannes
>>>
>>>
>>>> Benefits
>>>> ========
>>>>
>>>> The small calls to memcpy and memmove that are transformed by Instcombine
>>>> into integer load/store pairs would be correctly transformed into the
>>>> loads/stores of the new byte type no matter what underlying value is
>>> being
>>>> copied (integral value or a pointer value).
>>>>
>>>>       1.
>>>>
>>>>       The new byte type solves the problem of copying the padded data,
>>> with no
>>>>       contamination of the loaded value due to poison bits of the padding.
>>>>       2.
>>>>
>>>>       When copying pointers as bytes implicit pointer-to-integer casts are
>>>>       avoided.The current setting of performing a memcpy using i8s leads to
>>>>       miscompilations (example: bug report 37469
>>>>       <https://bugs.llvm.org/show_bug.cgi?id=37469>) and is bad for alias
>>>>       analysis.
>>>>
>>>>
>>>>
>>>> Lowering of char, unsigned char and std::byte
>>>>
>>>> ======================================
>>>>
>>>> For any function that takes char, unsigned char or std::byte as
>>> arguments,
>>>> we lower these to the b8 instead of i8. The motivation comes from the
>>> fact
>>>> that any pointer can be explicitly copied in bytes, and each byte can be
>>>> passed as an argument to the copying function. Since a copy like that can
>>>> escape a pointer, we need to generate the byte type to avoid the issue.
>>>>
>>>> Example
>>>>
>>>> void foo(unsigned char arg1, char arg2) {...}
>>>>
>>>>                =>
>>>>
>>>> void @foo(zeroext b8 %arg1, signext b8 %arg2) {...}
>>>>
>>>>
>>>>
>>>> std::byte is defined as enum class byte : unsigned char {} and therefore
>>>> all bitwise operations over it can be lowered equivalently to operations
>>> on
>>>> unsigned char (operation is performed over zero-extended to i32 operands,
>>>> and the result is truncated back).
>>>>
>>>>
>>>> ==============================
>>>>
>>>> Byte Type Semantics (Version 1)
>>>>
>>>> ==============================
>>>>
>>>>
>>>> [allowed] alloca/load/store
>>>>
>>>> =====================
>>>>
>>>> The byte type is allowed to be allocated, stored and loaded. No semantic
>>>> changes needed.
>>>>
>>>> [allowed] zext/sext/trunc
>>>>
>>>> ====================
>>>>
>>>> In order to easily adapt the current replacement of i8 with b8, we want
>>> to
>>>> extend zext/sext/trunc instructions’ semantics to allow the first operand
>>>> as a byte type as well. This will reduce the number of instructions
>>> needed
>>>> for the cast.
>>>>
>>>> Example
>>>>
>>>> trunc i32 %int to b8
>>>>
>>>> zext i8 %char to b32
>>>>
>>>> [modified] bitcast
>>>>
>>>> ==============
>>>>
>>>> We modify the semantics of the bitcast to allow casts from and to the
>>> byte
>>>> types. If the byte has a value of the same kind (integral or a pointer)
>>> as
>>>> the other type, then the bitcast is a noop. Otherwise, the result is
>>> poison.
>>>> Example
>>>>
>>>> bitcast i<N> %val to b<N>
>>>>
>>>> bitcast i<N>* %val to b64
>>>>
>>>> bitcast b<N> %byte to i<N>     [byte has an integral value => noop]
>>>>
>>>>
>>>>
>>>> [new] bytecast
>>>>
>>>> ============
>>>>
>>>> During IR generation, we cannot deduce whether the byte value contains a
>>>> pointer or an integral value, and therefore we cannot create a bitcast.
>>>>
>>>> Example
>>>>
>>>> int @cast(char c) { return (int) c; }
>>>>
>>>> Current
>>>>
>>>> Proposed
>>>>
>>>> i32 @cast(i8 signext %c) {
>>>>
>>>>      %1 = alloca i8, align 1
>>>>
>>>>      store i8 %c, i8* %1, align 1
>>>>
>>>>      %2 = load i8, i8* %1, align 1
>>>>
>>>>      %3 = sext i8 %2 to i32
>>>>
>>>>      ret i32 %3
>>>>
>>>> }
>>>>
>>>> i32 @cast(b8 %c) {
>>>>
>>>>      %1 = alloca b8
>>>>
>>>>      store b8 %c, b8* %11
>>>>
>>>>      %2 = load b8, b8* %1
>>>>
>>>>      %3 =    ?    b8 %2 to i32
>>>>
>>>>      ret i32 %3
>>>>
>>>> }
>>>>
>>>> In this example, the operation  ?  can be sext (b8 is i8) or can be
>>>> ptrtoint if the byte has a pointer value.
>>>>
>>>> We therefore introduce a new bytecast instruction. The frontend will
>>> always
>>>> produce a bytecast, which can then be optimized into a more specific cast
>>>> if necessary. Bytecast operands must have the same bitwidth (or size).
>>>>
>>>> The semantics of bytecast instruction is:
>>>>
>>>> bytecast b<N> %byte to T
>>>>
>>>> The kind of the value of the byte
>>>>
>>>> T
>>>>
>>>> Semantics
>>>>
>>>> Pointer
>>>>
>>>> Integral
>>>>
>>>> ptrtoint
>>>>
>>>> Pointer
>>>>
>>>> Pointer
>>>>
>>>> bitcast
>>>>
>>>> Integral
>>>>
>>>> Integral
>>>>
>>>> integral casts (zext, sext, etc.)
>>>>
>>>> Integral
>>>>
>>>> Pointer
>>>>
>>>> inttoptr
>>>>
>>>>
>>>>
>>>> Essentially, this instruction is a selection of the right casts. Once the
>>>> compiler has been able to prove the kind of the byte’s value, the
>>> bytecast
>>>> can be replaced with appropriate cast or become a noop.
>>>>
>>>> Example
>>>>
>>>> bytecast b64 %bytes to i32*
>>>>
>>>> bytecast b8 %byte to i8
>>>>
>>>>
>>>>
>>>> [disabled] and/or/xor
>>>>
>>>> =================
>>>>
>>>> We do not allow bitwise operations over byte types because it’s not
>>> trivial
>>>> to define all cases, particularly the ones involving pointers. If these
>>>> operations are useful for optimizations and/or some frontends, semantics
>>>> for these can be considered separately.
>>>>
>>>> If the operation is desired on the frontend level, then the default
>>>> generated code can always cast the byte type to an integer to operate on
>>>> integer values.
>>>>
>>>>
>>>>
>>>> [disabled] arithmetic
>>>>
>>>> =================
>>>>
>>>> Performing arithmetic operations over the byte type is similarly not
>>>> allowed (A valid question is what does it mean to add to bytes of raw
>>>> memory?). If we want to perform arithmetic, we need to cast a byte to an
>>>> integer (via sext/zext/trunc explicitly).
>>>>
>>>>
>>>>
>>>> [modified] comparison
>>>>
>>>> ==================
>>>>
>>>> We allow performing comparisons, as we may potentially want to compare
>>> the
>>>> ordering of the memory instances, check for null, etc. Comparison is also
>>>> needed since char types are commonly compared. We define the following
>>>> semantic of the byte type comparison.
>>>>
>>>> Case 1: The values of the bytes have the same kinds: compare the values.
>>>>
>>>> Case 2: The values of the bytes have the different kinds: do a bytecast
>>> of
>>>> the non-integral type to the other type and compare the integral values.
>>>>
>>>>
>>>> Example
>>>>
>>>> =======
>>>>
>>>> unsigned char sum(unsigned char a, unsigned char b) { return a + b; }
>>>>
>>>> Current
>>>>
>>>> Proposed
>>>>
>>>> zeroext i8 @sum(i8 zeroext %a, i8 zeroext %b) {
>>>>
>>>>      %1 = alloca i8
>>>>
>>>>      %2 = alloca i8
>>>>
>>>>      store i8 %a, i8* %1
>>>>
>>>>      store i8 %b, i8* %2
>>>>
>>>>      %3 = load i8, i8* %1
>>>>
>>>>      %4 = zext i8 %3 to i32
>>>>
>>>>      %5 = load i8, i8* %2
>>>>
>>>>      %6 = zext i8 %5 to i32
>>>>
>>>>      %7 = add nsw i32 %4, %6
>>>>
>>>>      %8 = trunc i32 %7 to i8
>>>>
>>>>      ret i8 %8
>>>>
>>>> }
>>>>
>>>> zeroext b8 @sum(b8 zeroext %a, b8 zeroext %b) {
>>>>
>>>>      %1 = alloca b8
>>>>
>>>>      %2 = alloca b8
>>>>
>>>>      store b8 %a, b8* %1
>>>>
>>>>      store b8 %b, b8* %2
>>>>
>>>>      %3 = load b8, b8* %1
>>>>
>>>>      %4 = bytecast b8 %3 to i8
>>>>
>>>>      %5 = zext i8 %4 to i32
>>>>
>>>>      %6 = load b8, b8* %2
>>>>
>>>>      %7 = bytecast b8 %6 to i8
>>>>
>>>>      %8 = zext i8 %7 to i32
>>>>
>>>>      %9 = add nsw i32 %5, %8
>>>>
>>>>      %10 = trunc i32 %9 to b8
>>>>
>>>>      ret b8 %10
>>>>
>>>> }
>>>>
>>>>
>>>>
>>>>
>>>>
>>>> Thanks,
>>>>
>>>> George
>>>>
>>>>
>>>> _______________________________________________
>>>> cfe-dev mailing list
>>>> cfe-dev at lists.llvm.org
>>>> https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-dev
> _______________________________________________
> LLVM Developers mailing list
> llvm-dev at lists.llvm.org
> https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev
>



More information about the cfe-dev mailing list