Thursday, January 1, 2009

Working with other libraries, part 2: allocators

Much is often made of the supposedly still-born Allocator concept in the standard library. However, one very good use of them is in tracking memory.

At the moment I'm preparing some analyses of FastFormat's performance for an article I'm writing. One of the analyses conducting is to see how many memory allocations are involved in a formatting statement, for each of the comparison libraries. The standard way to achieve something like this is to overload the global operators new, as in:

// NOTE: this code is only valid for single-threaded operation

extern int s_nallocs = 0;

#ifdef OVERLOAD_OPNEW
void* counting_malloc(size_t cb)
{
  ++s_nallocs;

  return ::malloc(cb);
}

void counting_free(void* pv)
{
  ::free(pv);
}

void* operator new(size_t cb)
{
  return counting_malloc(cb);
}

void operator delete(void* pv)
{
  counting_free(pv);
}

void* operator new[](size_t cb)
{
  return counting_malloc(cb);
}

void operator delete[](void* pv)
{
  counting_free(pv);
}
#endif /* OVERLOAD_OPNEW */

Unfortunately, some components with some compilers - the exact permutations escape me at this point - don't go through operator new. This might be because they use a per-class operator new, or it might be because they use an allocator that doesn't use new. (Being under a publishing deadline, I didn't have the time - nor the inclination, if I'm honest - to find out which it was in each case.)

So, in order to get a fighting chance at an accurate depiction of how much memory each library is using I decided to force the issue, by requiring all the strings used to be an instance of the following specialisation, rather than std::string:

typedef std::basic_string<
  char
, std::char_traits
<char>
stlsoft::new_allocator<char>
>   string_t;

Of course, things aren't ever that simple. Such a string type is not compatible with the IOStreams default specialisations, requiring:

typedef std::basic_stringstream<
  char
, std::char_traits
<char>
stlsoft::new_allocator<char>
>   stringstream_t;

And the same thing applies for Boost.Format, requiring:

typedef boost::basic_format<
  char
, std::char_traits
<char>
stlsoft::new_allocator<char>
>   format_t;

Unfortunately, Loki's SafeFormat library does not allow for the specification of allocators (or character traits, for that matter), and only uses std::string. So a little horrifying trickery was required.

Step 1: Introduce string_t into the std namespace.

namespace std
{
  using ::string_t;
}

Now, if you've been paying attention this last decade or so you'll know that adding to the std namespace is strictly controlled. I won't go over the rules now; you can look it up. Suffice to say that this action is not allowed.

Of course, needs must, and in this case there's no choice. Since it's just a perf-test program, it's ok. Just don't go using this tactic in production code.

Step 2: Make Loki (and any other code, for that matter) think that std::string_t is std::string.

#define string string_t

I warned you it was horrid!

Step 3: #include the Loki.SafeFormat header

#include <loki/safeformat.h>

Obviously, this has to be done after steps 1 & 2, otherwise it won't work.


There were a few other dodgy things I had to do to get it to work with some really stupid compilers, but that'll have to wait until another day.

No comments: