Monday, October 15, 2012

A twist on PImpl

PImpl is a well known pattern for reducing dependencies in a C++ project. The classic implementation is:
#pragma once
#include <memory>
class HTTPServerImpl;
class HTTPServer
{
public:
HTTPServer(unsigned short port);
~HTTPServer();
unsigned short GetPort() const;
private:
std::unique_ptr<HTTPServerImpl> m_Impl;
};

#include "HTTPServerClassic.h"
#include "HTTPServerImpl.h"
HTTPServer::HTTPServer(unsigned short port)
: m_Impl(new HTTPServerImpl(port))
{
}
HTTPServer::~HTTPServer()
{
}
unsigned short HTTPServer::GetPort() const
{
return m_Impl->GetPort();
}

It has two drawbacks:
  • inhibits function inlining
  • has an extra heap allocation and pointer chase
The extra heap allocation leads to whole new set of drawbacks - creating an instance is more expensive, fragments the heap memory and the address space, has an extra pointer chase and reduces cache-locality.

This allocation can be avoided by a simple trade-off with the PImpl idiom. Why allocating the HTTPServerImpl instance, instead of storing it in the facade object? This is because C++ requires to see the declaration of HTTPServerImpl to allow it to be stored by value. But we can store a C++ object in every memory chunk large enough to hold the its data and respects its alignment requirements. So instead of storing HTTPServerImpl pointer in the facade, we can store a memory chunk that is interpreted as an instance of HTTPServerImpl.This concept can be easily generalized in an reusable template:
#pragma once
#include <new>
#include <utility>
// Visual Studio 2010 does not support std::max_align_t,
// so use an arbitrary default and leave it for clients to specify better value
template <typename T, size_t size, size_t alignment = sizeof(unsigned long long)>
class Impl
{
public:
Impl()
{
new (Get()) T;
}
template <typename Arg1>
Impl(Arg1&& arg1)
{
new (Get()) T(std::forward<Arg1>(arg1));
}
template <typename Arg1, typename Arg2>
Impl(Arg1&& arg1, Arg2&& arg2)
{
new (Get()) T(std::forward<Arg1>(arg1), std::forward<Arg2>(arg2));
}
Impl& operator=(const Impl& rhs)
{
*Get() = *rhs.Get();
}
Impl& operator=(Impl&& rhs)
{
*Get() = std::move(*rhs.Get());
}
~Impl()
{
static_assert(sizeof(T) <= size,
"Implementation instance does not fit in the buffer");
static_assert(alignment % std::alignment_of<T>::value == 0,
"Implementation instance has incompatible alignment requirements");
Get()->~T();
}
T* Get()
{
return reinterpret_cast<T*>(&m_Buffer);
}
const T* Get() const
{
return reinterpret_cast<const T*>(&m_Buffer);
}
T* operator->()
{
return Get();
}
const T* operator->() const
{
return Get();
}
private:
typedef typename std::aligned_storage<size, alignment>::type AlignedStorage;
AlignedStorage m_Buffer;
};
view raw Impl.h hosted with ❤ by GitHub

And the HTTPServer becomes:
#include "Impl.h"
class HTTPServerImpl;
class HTTPServer
{
public:
HTTPServer(unsigned short port);
~HTTPServer();
unsigned short GetPort() const;
private:
Impl<HTTPServerImpl, 8> m_Impl;
};
view raw HTTPServer.h hosted with ❤ by GitHub

#include "HTTPServer.h"
#include "HTTPServerImpl.h"
HTTPServer::HTTPServer(unsigned short port)
: m_Impl(port)
{
}
HTTPServer::~HTTPServer()
{
}
unsigned short HTTPServer::GetPort() const
{
return m_Impl->GetPort();
}
view raw HTTPServer.cpp hosted with ❤ by GitHub
This is definitely not a new technique and it is declared "deplorable" in GotW #28. It has its drawbacks, but I consider some of them acceptable trade-offs. What is more:
  • The alignment problems are mitigated by C++11 support for alignment
  • Writing operator= is not harder than writing it in general
  • The extra memory consumption is acceptable for small number of instances, given the better cache coherency.
So, does this technique really eliminates the extra pointer chase?

The classical implementation looks like:
?GetPort@HTTPServer@@QBEGXZ PROC ; HTTPServer::GetPort, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx]
mov ax, WORD PTR [eax]
ret 0

And the "twisted" one:
?GetPort@HTTPServer@@QBEGXZ PROC ; HTTPServer::GetPort, COMDAT
; _this$ = ecx
mov ax, WORD PTR [ecx]
ret 0
view raw HTTPServer.asm hosted with ❤ by GitHub


Seems like it does.

Of course, this technique breaks the PImpl idiom and might be considered a hack. Every time the HTTPServerImpl grows beyond the hard-coded size or its alignment requirements change, we have to change the definition of the facade and recompile all the source files depending on the HTTPServer.h, but given the advantages, this is an acceptable trade-off for many situations.

Literature:
  • John Lakos; Large-Scale C++ Software Design; Addison-Wesley Longman, 1996
  • Herb Sutter; Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions; Addison-Wesley Longman, 2000

No comments:

Post a Comment