How can I get the sizes of the allocations that needs to be done for this handler?

69 Views Asked by At

The below code is using a handler that provides a custom allocator. The allocator is provided with an instance of BucketPool that can allocate different sizes, but the sizes of the allocations that can be made needs to be specified at compile time.

To use BucketPool I would just specify some arbitrary template parameter, and then look at the compilation errors to see what size is actually needed, then add that size as a template parameter, and then iterate until there is no compilation error.

I noticed that the sizes of the allocations that needs to be made can change, for example when updating boost to a newer version.

How can I figure out the sizes at compile time so that I don't need to update this when I move to a new version? Maybe there is some trait that can be used for this...

Edit: c++17 is what I have available. So any solution using that is preferable.

#include <chrono>
#include <iostream>
#include <utility>

#include "boost/asio.hpp"
#include <boost/pool/pool.hpp>
#include <boost/make_shared.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/bind/bind.hpp>

namespace details {

template <size_t SIZE, size_t... REST>
class Bucket;

template <size_t SIZE>
class Bucket<SIZE> {

public:
  Bucket() = default;

protected:
  void* do_alloc(const std::size_t numElements) {
    assert(numElements == 1);
    return allocator_.malloc();
  }

  void do_dealloc(void* const ptr, const std::size_t numElements) {
    assert(numElements == 1);
    allocator_.free(ptr, numElements);
  }

private:
  boost::pool<boost::default_user_allocator_new_delete> allocator_{SIZE};
};

template <size_t SIZE, size_t... REST>
class Bucket
  : public Bucket<SIZE>
  , public Bucket<REST>... {};

}  // namespace details

template <size_t SIZE, size_t... REST>
class BucketPool : public details::Bucket<SIZE, REST...> {
public:
  template <size_t S>
  void* alloc(const std::size_t numElements) {
    return details::Bucket<S>::do_alloc(numElements);
  }

  template <size_t S>
  void dealloc(void* const ptr, const std::size_t numElements) {
    assert(numElements == 1);
    details::Bucket<S>::do_dealloc(ptr, numElements);
  }
};


using strand_t = boost::asio::strand<boost::asio::io_context::executor_type>;

template <typename T, typename PoolType>
class ObjectAllocator {
public:
  using value_type = T;

  explicit ObjectAllocator(PoolType& bucketPool) : bucketPool_(bucketPool) {}

  template <typename U, typename K>
  explicit ObjectAllocator(const ObjectAllocator<U, K>& other)
  : bucketPool_(other.bucketPool_) {}

  bool operator==(const ObjectAllocator& lhs) const noexcept {
    return bucketPool_ == lhs.bucketPool_;
  }

  bool operator!=(const ObjectAllocator& lhs) const noexcept {
    return bucketPool_ != lhs.bucketPool_;
  }

  T* allocate(const std::size_t numElements) const {
    return static_cast<T*>(bucketPool_.template alloc<sizeof(T)>(numElements));
  }

  void deallocate(T* const ptr, const std::size_t numElements) const {
    bucketPool_.template dealloc<sizeof(T)>(ptr, numElements);
  }

private:
  template <typename, typename>
  friend class ObjectAllocator;

  PoolType& bucketPool_;
};

template <typename HandlerT>
class AsioTimer {

  class HandlerWrapper : public boost::enable_shared_from_this<HandlerWrapper> {
  public:
    HandlerWrapper(strand_t strand, HandlerT handler)
    : timer_(strand), handler_(handler), milliseconds_(0) {}

    void startTimer(const std::chrono::milliseconds& everyMilliseconds) {
      milliseconds_ = everyMilliseconds;
      timer_.expires_from_now(everyMilliseconds);
      startAsyncWait();
    }

  private:
    void startAsyncWait() {
      timer_.async_wait(MakeCustomAllocationHandler(
        memory_,
        boost::bind(&HandlerWrapper::handleTimerCallback, this->shared_from_this(),
                    boost::asio::placeholders::error)));
    }

    void handleTimerCallback(const boost::system::error_code& e) {
      if (e != boost::asio::error::operation_aborted) {
        handler_();
      }
      timer_.expires_at(timer_.expires_at() + milliseconds_);
      startAsyncWait();
    }

    BucketPool<128> memory_;
    boost::asio::steady_timer timer_;
    HandlerT handler_;
    std::chrono::milliseconds milliseconds_;
  };

public:
  AsioTimer(strand_t strand, HandlerT handler)
  : handlerWrapper_(boost::make_shared<HandlerWrapper>(strand, handler)) {}

  void startTimer(const std::chrono::milliseconds& everyMilliseconds) {
    handlerWrapper_->startTimer(everyMilliseconds);
  }

private:
  boost::shared_ptr<HandlerWrapper> handlerWrapper_;
};

template <typename HandlerT, typename PoolT>
class CustomAllocationHandler {
public:
  using allocator_type = ObjectAllocator<HandlerT, PoolT>;

  CustomAllocationHandler(PoolT& memory, HandlerT handler)
  : memory_(memory), handler_(std::move(handler)) {}

  allocator_type get_allocator() const noexcept {
    return allocator_type(memory_);
  }

  template <typename... Args>
  void operator()(Args&&... args) {
    handler_(std::forward<Args>(args)...);
  }

private:
  PoolT& memory_;
  HandlerT handler_;
};

template <typename HandlerT, typename PoolT>
CustomAllocationHandler<HandlerT, PoolT> MakeCustomAllocationHandler(PoolT& memory,
                                                                     HandlerT handler) {
  return CustomAllocationHandler<HandlerT, PoolT>(memory, std::move(handler));
}


int main() {
  boost::asio::io_context ioContext;

  strand_t myStrand(make_strand(ioContext));

  AsioTimer timer(myStrand, [] { std::cout << "timer called" << std::endl; });
  timer.startTimer(std::chrono::milliseconds(20));

  auto fut = std::async([&ioContext] {
    ioContext.run();
  });

  std::this_thread::sleep_for(std::chrono::seconds(1));
  ioContext.stop();
  fut.get();
}

1

There are 1 best solutions below

3
On

I noticed that the sizes of the allocations that needs to be made can change, for example when updating boost to a newer version.

This is always going to be the case, since these are implementation details.

That's not to say you can't get some useful information, but you have to be prepared to get in some more work to do this specific kind of PGO (Profile Guided Optimization) manually.

I imagine a flag that you set for the program to report a histogram (perhaps over time/program phases) of allocation sizes - perhaps even with longevity deductions.

You could devise a format in which to persist that profile and use it to guide your pool parameters on subsequent runs.

You could even go the entire way and make the file includable so you can compile in the profiled values on a next build.

Aside: Some compilers already allow PGO for code generation (see e.g. What information does GCC Profile Guided Optimization (PGO) collect and which optimizations use it?). This is what gave me the idea.