0xBADF00
0xBADF00

Reputation: 1120

Benchmark variadic template function call

I used Quick bench (see link below) to measure the performance between decoding a buffer with call to a function with variadic template arguments versus decoding the same buffer with call to a function without variadic pack expansion.

Any idea on how to make the variadic implementation on par with the other implementation?

The result from the benchmark is a ratio of (CPU time / Noop time). The benchmark runs on a pool of AWS machines whose load is unknown. The purpose is to give a reasonnably good comparison between two snippet of code, run in the same conditions. The CPU time for the non variadic template function was 5.9 and for the variadic implementation it was 21.3. Compiler: Clang 5.0 with optimization level O3.

Benchmark

#include <cstdint>
#include <cstring>
#include <string>
#include <type_traits>

namespace core { namespace decoder 
{
   class LittleEndian
   {
   public:
      LittleEndian(const LittleEndian&) = delete;
      LittleEndian& operator=(const LittleEndian&) = delete;

   public:
      constexpr LittleEndian(const std::uint8_t* buffer, size_t size) noexcept
      : m_buffer(buffer),
        m_size(size)
      {}

      constexpr bool decodeU8(
        size_t& offset, std::uint8_t& decodedValue) const noexcept
      {
        if (offset >= m_size)
            return false;

        decodedValue = m_buffer[offset];
        offset += sizeof(std::uint8_t);
        return true;
      }

      constexpr bool decodeU16(
        size_t& offset, std::uint16_t& decodedValue) const noexcept
      {
        if (offset + sizeof(std::uint16_t) > m_size)
            return false;

        const uint8_t b0 = m_buffer[offset], b1 = m_buffer[offset + 1];
        decodedValue = (b0 << 0) | (b1 << 8);
        offset += sizeof(std::uint16_t);
        return true;
      }

      constexpr bool decodeU32(
        size_t& offset, std::uint32_t& decodedValue) const noexcept
      {
        if (offset + sizeof(std::uint32_t) > m_size)
            return false;

        const uint8_t b0 = m_buffer[offset], b1 = m_buffer[offset + 1], b2 = m_buffer[offset + 2], b3 = m_buffer[offset + 3];
        decodedValue = (b0 << 0) | (b1 << 8) | (b2 << 16) | (b3 << 24);
        offset += sizeof(std::uint32_t);
        return true;
      }

      constexpr bool decodeU64(
        size_t& offset, std::uint64_t& decodedValue) const noexcept
      {
        if (offset + sizeof(std::uint64_t) > m_size)
            return false;

        const uint8_t b0 = m_buffer[offset], b1 = m_buffer[offset + 1],
            b2 = m_buffer[offset + 2], b3 = m_buffer[offset + 3],
            b4 = m_buffer[offset + 4], b5 = m_buffer[offset + 5],
            b6 = m_buffer[offset + 6], b7 = m_buffer[offset + 7];

        decodedValue = (static_cast<std::uint64_t>(b0) << 0) |
            (static_cast<std::uint64_t>(b1) << 8) |
            (static_cast<std::uint64_t>(b2) << 16) |
            (static_cast<std::uint64_t>(b3) << 24) |
            (static_cast<std::uint64_t>(b4) << 32) |
            (static_cast<std::uint64_t>(b5) << 40) |
            (static_cast<std::uint64_t>(b6) << 48) |
            (static_cast<std::uint64_t>(b7) << 56);

        offset += sizeof(std::uint64_t);
        return true;
      }

  private:
    const std::uint8_t* m_buffer;
    const size_t m_size;
  };

  template<typename EndianDecoderT>
  class ByteDecoder
  {
  public:
    ByteDecoder(const ByteDecoder&) = delete;
    ByteDecoder& operator=(const ByteDecoder&) = delete;

  public:
    constexpr ByteDecoder(const std::uint8_t* buffer, size_t size)
      : m_buffer(buffer),
        m_size(size),
        m_endianDecoder(buffer, size)
    {}

    template<typename ...Args>
    constexpr bool decode(size_t offset, Args&... args) const noexcept
    {
        bool success = true;

        using expand_type = int[];
        expand_type
        {
            ([&success] (auto result) noexcept
            {
                success = (!success || !result) ? false : true;

            } (decodeValue(offset, args)), 0)...
        };

        return success;

      }

      template<typename T>
      constexpr bool decode(size_t offset, T& decodedValue) const noexcept
      {
        return decodeValue(offset, decodedValue);
      }

   private:
    template<typename T>
    constexpr bool decodeValue(
        size_t &offset, T& decodedValue) const noexcept
    {
        if constexpr (std::is_same< std::decay_t<T>, std::uint8_t>::value)
            return m_endianDecoder.decodeU8(offset, decodedValue);

        if constexpr (std::is_same< std::decay_t<T>, std::uint16_t>::value)
            return m_endianDecoder.decodeU16(offset, decodedValue);

        if constexpr (std::is_same< std::decay_t<T>, std::uint32_t>::value)
            return m_endianDecoder.decodeU32(offset, decodedValue);

        if constexpr (std::is_same< std::decay_t<T>, std::uint64_t>::value)
            return m_endianDecoder.decodeU64(offset, decodedValue);

        if constexpr (std::is_same<char *, typename std::decay<T>::type>::value ||
                      std::is_same<char const *, typename std::decay<T>::type>::value)
            return decodeCHR(offset, decodedValue);

        return false;
      }

      template<size_t SIZE>
      constexpr bool decodeCHR(
        size_t &offset, char (&buffer)[SIZE]) const noexcept
      {
        if (offset + SIZE > m_size)
            return false;

        memset(&buffer[0], 0x00, sizeof(char) * SIZE);
        memcpy(&buffer[0], &m_buffer[offset], sizeof(char) * (std::min)(SIZE, std::extent<decltype(buffer)>::value - 1));
        offset += SIZE;
        return true;
      }

  private:
    const std::uint8_t* m_buffer;
    const size_t m_size;
    EndianDecoderT m_endianDecoder;
  };

}} // namespace core::decoder
static void NonVariadicDecoding(benchmark::State& state) {
// Code inside this loop is measured repeatedly
constexpr std::uint8_t littleEndian[] = { 0x0D, 0x0C, 0x84, 0x03, 0x00, 0x00, 'H', 'e', 'l', 'l', 'o', '\0',  0x84, 0x03, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
core::decoder::ByteDecoder<core::decoder::LittleEndian> decoder(littleEndian, sizeof(littleEndian));


for (auto _ : state) {

  size_t offset = 0;

  struct DecodedValue
  {
      std::uint16_t v1_U16;
      std::uint32_t v2_U32;
      char v3_CHR[6];
      std::uint16_t v4_U16;
      std::uint64_t v5_U64;
  };
  DecodedValue dv;

  decoder.decode(offset, dv.v1_U16);  
  offset += sizeof(dv.v1_U16);
  decoder.decode(offset, dv.v2_U32);
  offset += sizeof(dv.v2_U32);
  decoder.decode(offset, dv.v3_CHR);
  offset += sizeof(dv.v3_CHR);
  decoder.decode(offset, dv.v4_U16);
  offset += sizeof(dv.v4_U16);
  decoder.decode(offset, dv.v5_U64);

  benchmark::DoNotOptimize(dv);
  }
}
// Register the function as a benchmark
BENCHMARK(NonVariadicDecoding);
static void VariadicDecoding(benchmark::State& state) {
// Code before the loop is not measured
constexpr std::uint8_t littleEndian[] = { 0x0D, 0x0C, 0x84, 0x03, 0x00, 0x00, 'H', 'e', 'l', 'l', 'o', '\0',  0x84, 0x03, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
core::decoder::ByteDecoder<core::decoder::LittleEndian> decoder(littleEndian, sizeof(littleEndian));

for (auto _ : state) {

  struct DecodedValue
  {
      std::uint16_t v1_U16;
      std::uint32_t v2_U32;
      char v3_CHR[6];
      std::uint16_t v4_U16;
      std::uint64_t v5_U64;
  };

  DecodedValue dv;
  decoder.decode(0, dv.v1_U16, dv.v2_U32, dv.v3_CHR, dv.v4_U16, dv.v5_U64);
  benchmark::DoNotOptimize(dv);
  }
}
BENCHMARK(VariadicDecoding);

Quick-Bench

Upvotes: 2

Views: 402

Answers (1)

piwi
piwi

Reputation: 5336

If you have recursive calls to your variadic implementation and use perfect forwarding then you can have better performance:

    template <typename Type>
    constexpr bool decode_impl(size_t offset, Type&& value) const noexcept
    {
      return decodeValue(offset, std::forward<Type>(value));
    }

    template <typename First, typename Second, typename... Other>
    constexpr bool decode_impl(size_t offset, First&& first, Second&& second, Other&&... others) const noexcept
    {
      return decode_impl(offset, std::forward<First>(first)) && decode_impl(offset, std::forward<Second>(second), std::forward<Other>(others)...);
    }

    template<typename ...Args>
    constexpr bool decode(size_t offset, Args&&... args) const noexcept
    {
      return decode_impl(offset, std::forward<Args>(args)...);
    }

See there.

Upvotes: 2

Related Questions