214 lines
6.9 KiB
C++
214 lines
6.9 KiB
C++
//
|
|
// eng-malloc
|
|
//
|
|
// The engine has its own private eng::malloc which it uses to allocate
|
|
// everything. The engine's malloc heap only contains engine data structures.
|
|
// This helps achieve determinism when playing a replay log.
|
|
//
|
|
// About determinism: one of the key rules for maintaining deterministic
|
|
// behavior is to not ever use operations that execute in arbitrary order.
|
|
// For example, don't iterate over an unordered map, because there's no
|
|
// rule about what order the items are produced. They could be produced
|
|
// in a different order during replay than during the original recording.
|
|
// Actually, you can occasionally get away with it
|
|
//
|
|
// The engine's eng::malloc is a thin wrapper around Doug Lea's Malloc, a good
|
|
// general-purpose single-threaded malloc. It's probably not the fastest any
|
|
// more (it was, once), but it's still quite good. It's also fairly easy
|
|
// to work with.
|
|
//
|
|
// In order to get all engine data structures into the eng::malloc heap, you
|
|
// need to jump through quite a few hoops:
|
|
//
|
|
// * When writing a class that gets allocated using operator new,
|
|
// always derive from eng::opnew. This adds a custom operator new
|
|
// to your class, which causes your class to be allocated using eng::malloc.
|
|
// If you write a class that isn't ever supposed to be allocated using
|
|
// operator new, derive from eng::nevernew instead.
|
|
//
|
|
// * When using STL containers, you need to use the eng variant:
|
|
// eng::map, eng::set, eng::vector, eng::unordered_map, eng::unordered_set,
|
|
// and eng::deque. These classes derive from eng::opnew, and they also
|
|
// use eng::malloc for their internal nodes.
|
|
//
|
|
// * Use eng::string instead of std::string. Use eng::ostringstream
|
|
// instead of std::ostringstream.
|
|
//
|
|
// * Simple classes like std::pair, std::string_view, std::less, std::hash, and
|
|
// so forth are not wrapped, because it is not normal to allocate these
|
|
// classes using operator new. Do not use operator new or delete on
|
|
// these classes.
|
|
//
|
|
// * Instead of std::make_shared, use eng::make_shared. You need this
|
|
// because std::make_shared doesn't respect your custom operator new.
|
|
//
|
|
// * Failing to jump through all these hoops won't break your code in any
|
|
// obvious way - you'll just have some of your data structures in the malloc
|
|
// heap instead of the eng::malloc heap. This won't break
|
|
// determinism unless you iterate over a data structure like an unordered map
|
|
// but it creates a situation where we can't detect
|
|
// nondeterminism.
|
|
//
|
|
// * Sometimes we deliberately put certain data structures into the malloc
|
|
// heap, because we know that those particular data structures won't be
|
|
// identical between record and replay. In that situation, the fact that
|
|
// we don't detect the nondeterminism is actually a benefit.
|
|
//
|
|
// * Be aware that most C++ streams use the system malloc heap, and there's no
|
|
// way to change that. That's ok, it's fine if some small percentage of our
|
|
// data goes into the malloc heap. By the way, eng::ostringstream uses
|
|
// the eng::malloc heap.
|
|
//
|
|
#ifndef ENG_MALLOC_HPP
|
|
#define ENG_MALLOC_HPP
|
|
|
|
#include <cstddef>
|
|
#include <memory>
|
|
#include <cassert>
|
|
|
|
namespace eng {
|
|
#ifdef __linux__
|
|
void* malloc(size_t x);
|
|
void free(void *p);
|
|
void* realloc(void*, size_t);
|
|
int memhash();
|
|
#else
|
|
inline void *malloc(size_t x) { return ::malloc(x); }
|
|
inline void free(void *p) { return ::free(p); }
|
|
inline void *realloc(void *p, size_t x) { return ::realloc(p, x); }
|
|
inline int memhash() { return 0; }
|
|
#endif
|
|
} // namespace eng
|
|
|
|
// An allocator for lua states that uses eng::malloc and eng::free
|
|
namespace eng {
|
|
inline void *l_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
|
|
if (nsize == 0) {
|
|
::eng::free(ptr);
|
|
return NULL;
|
|
} else {
|
|
return ::eng::realloc(ptr, nsize);
|
|
}
|
|
}
|
|
} // namespace eng
|
|
|
|
// eng_allocator is similar to std::allocator, but allocates
|
|
// objects using eng::malloc and eng::free.
|
|
template <class T>
|
|
class eng_allocator
|
|
{
|
|
public:
|
|
using value_type = T;
|
|
eng_allocator() noexcept {}
|
|
template <class U> eng_allocator(eng_allocator<U> const&) noexcept {}
|
|
|
|
value_type* allocate(std::size_t n)
|
|
{
|
|
return static_cast<value_type*>(eng::malloc(n*sizeof(value_type)));
|
|
}
|
|
|
|
void deallocate(value_type* p, std::size_t) noexcept
|
|
{
|
|
eng::free(p);
|
|
}
|
|
};
|
|
|
|
// Another name for eng_allocator is eng::allocator.
|
|
namespace eng {
|
|
template<class T>
|
|
using allocator = ::eng_allocator<T>;
|
|
} // namespace eng
|
|
|
|
// Mandated equality and inequality operators for eng_allocator.
|
|
template <class T, class U>
|
|
bool operator==(const eng_allocator<T> &, const eng_allocator<U> &) noexcept
|
|
{
|
|
return true;
|
|
}
|
|
template <class T, class U>
|
|
bool operator!=(const eng_allocator<T> &, const eng_allocator<U> &) noexcept
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// eng::opnew. A class containing operator new and operator delete,
|
|
// meant to be used as a base class for inheritance.
|
|
namespace eng {
|
|
class opnew {
|
|
public:
|
|
void *operator new(size_t size)
|
|
{
|
|
return ::eng::malloc(size);
|
|
}
|
|
|
|
void operator delete(void *p, size_t size)
|
|
{
|
|
return ::eng::free(p);
|
|
}
|
|
|
|
void *operator new[](size_t size)
|
|
{
|
|
return ::eng::malloc(size);
|
|
}
|
|
|
|
void operator delete[](void *p, size_t size)
|
|
{
|
|
return ::eng::free(p);
|
|
}
|
|
};
|
|
} // namespace eng
|
|
|
|
// eng::nevernew. A class containing private operator new and
|
|
// operator delete, making it impossible to 'new' the class.
|
|
// This means the class must be embedded as a field in some other
|
|
// class, and it gets allocated when its enclosing object gets
|
|
// allocated.
|
|
namespace eng {
|
|
class nevernew {
|
|
private:
|
|
void *operator new(size_t size)
|
|
{
|
|
assert(false && "not supposed to 'new' this class");
|
|
return NULL;
|
|
}
|
|
|
|
void operator delete(void *p, size_t size)
|
|
{
|
|
assert(false && "not supposed to 'delete' this class");
|
|
}
|
|
|
|
void *operator new[](size_t size)
|
|
{
|
|
assert(false && "not supposed to 'new' this class");
|
|
return NULL;
|
|
}
|
|
|
|
void operator delete[](void *p, size_t size)
|
|
{
|
|
assert(false && "not supposed to 'delete' this class");
|
|
}
|
|
};
|
|
} // namespace eng
|
|
|
|
|
|
// eng::make_shared allocates shared objects using eng::malloc.
|
|
namespace eng {
|
|
template<class T, class... Args>
|
|
inline ::std::shared_ptr<T> make_shared(Args&&... args) {
|
|
return std::allocate_shared<T>(eng::allocator<T>(), args...);
|
|
}
|
|
} // namespace eng
|
|
|
|
// eng::make_unique doesn't do anything different than std::make_unique: they
|
|
// both use operator new and delete. You must derive from eng::opnew to change
|
|
// operator new and delete.
|
|
namespace eng {
|
|
template<class T, class... Args>
|
|
inline ::std::unique_ptr<T> make_unique(Args&&... args) {
|
|
return std::make_unique<T>(args...);
|
|
}
|
|
} // namespace eng
|
|
|
|
#endif // ENG_MALLOC_HPP
|
|
|