This is an archive of the discontinued LLVM Phabricator instance.

[libc][WIP] move realloc into alloc_checker
Needs ReviewPublic

Authored by michaelrj on Aug 10 2023, 2:23 PM.

Details

Summary

By putting all allocation through alloc_checker it's easier to check
when allocation is being done. I didn't find an easy way to use realloc
through new, so currently I just call the function manually.

Diff Detail

Event Timeline

michaelrj created this revision.Aug 10 2023, 2:23 PM
Herald added projects: Restricted Project, Restricted Project. · View Herald TranscriptAug 10 2023, 2:23 PM
michaelrj requested review of this revision.Aug 10 2023, 2:23 PM
michaelrj updated this revision to Diff 549170.

cleanup some experimental code

What I meant about a custom operator new to do realloc was something like this (C++20 example and without the AllocChecker arg, which would be added here too):

#include <span>                                                                 
#include <cstdlib>                                                              
                                                                                
template<typename T>                                                            
[[nodiscard]] inline void* operator new[] (size_t size, std::span<T> old) noexcept {                                                                           
  void* ptr = ::realloc(old.data(), old.size_bytes() + size);        
  if (!ptr) { return nullptr; }                                                 
  return static_cast<char*>(ptr) + old.size_bytes();                            
}                                                                               
                                                                                
struct S { int x = 23; };                                                       
                                                                                
S* extend(S* old, size_t count) {                                               
  return new (std::span{old,count}) S[count];                                   
}                                                                               
                                                                                
int* extend(int* old, size_t count) {                                           
  return new (std::span{old,count}) int[count];                                 
}

Instead of the local span equivalent you might want to use a custom type just so the name makes it more obvious what's being done, e.g. new (Realloc{old, count}, ac) T[n];.

That example shows how for a type where default-initialization is not just uninitialized, it does what you want like new for the original allocation does, while for a type that can be uninitialized, that's still an option with no extra overhead.
And of course you can use it with constructor arguments in a situation where that makes sense.

Note that realloc is also sometimes used to trim an allocation to shorter than it was, and I'm not sure there's a way to do an analogous trick for that case such that destructors get run naturally.

libc/src/__support/CPP/new.h
59

You might consider making this templated so realloc<T> takes and returns a T* and takes a count that it multiplies by sizeof(T). That's also a good opportunity to static_assert(alignof(T) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__);.
That makes it more similar to new in terms of the invariants around static types and alignment constraints, though it's still importantly different for any type that doesn't have trivial (i.e. uninitialized) default construction.

libc/src/__support/char_vector.h
37

Superfluous parens aren't usually used with delete.

What I meant about a custom operator new to do realloc was something like this (C++20 example and without the AllocChecker arg, which would be added here too):

#include <span>                                                                 
#include <cstdlib>                                                              
                                                                                
template<typename T>                                                            
[[nodiscard]] inline void* operator new[] (size_t size, std::span<T> old) noexcept {                                                                           
  void* ptr = ::realloc(old.data(), old.size_bytes() + size);        
  if (!ptr) { return nullptr; }                                                 
  return static_cast<char*>(ptr) + old.size_bytes();                            
}                                                                               
                                                                                
struct S { int x = 23; };                                                       
                                                                                
S* extend(S* old, size_t count) {                                               
  return new (std::span{old,count}) S[count];                                   
}                                                                               
                                                                                
int* extend(int* old, size_t count) {                                           
  return new (std::span{old,count}) int[count];                                 
}

Instead of the local span equivalent you might want to use a custom type just so the name makes it more obvious what's being done, e.g. new (Realloc{old, count}, ac) T[n];.

That example shows how for a type where default-initialization is not just uninitialized, it does what you want like new for the original allocation does, while for a type that can be uninitialized, that's still an option with no extra overhead.
And of course you can use it with constructor arguments in a situation where that makes sense.

Note that realloc is also sometimes used to trim an allocation to shorter than it was, and I'm not sure there's a way to do an analogous trick for that case such that destructors get run naturally.

Should we restrict this to trivially destructible types?

Should we restrict this to trivially destructible types?

realloc should be restricted to trivially copyable types, because realloc is going to copy them byte-by-byte for the existing elements in the array.
It doesn't really need to be restricted to trivially destructible types, because eventual delete[] should still work as it would with any array from new[].
Likewise, if we had a front-end for the trimming case, then that could explicitly call destructors on the tail elements if need be.

That said, it seems unlikely we'll ever use this for things that aren't trivially destructible, or even anything for which we're not doing uninitialized default initialization, so having conservative static_assert for now and revisiting when use cases arise seems sensible.

Should we restrict this to trivially destructible types?

realloc should be restricted to trivially copyable types, because realloc is going to copy them byte-by-byte for the existing elements in the array.

Ah yes, trivially copyable also.

It doesn't really need to be restricted to trivially destructible types, because eventual delete[] should still work as it would with any array from new[].

Discussing for academic knowledge:
If realloc allocates a whole new array, existing objects are copied over and so we need them to be trivially copyable. But, since the old array is free-d, we will need the trivially destructible property also?
Further, after the call to the reallocating new, wouldn't the compiler try to initialize the copied over objects also? If yes, we will need the trivially constructible property also?

Likewise, if we had a front-end for the trimming case, then that could explicitly call destructors on the tail elements if need be.

By front-end, do you mean the caller of the reallocating new?