ChatGPT-driven development

In the vast, often inexplicable universe of C++ programming, I found myself staring into the digital abyss of a peculiar problem: how does one effectively compare format_as with the venerable formatter specialization? This question was as intriguing as it was esoteric, much like pondering the number of Vogon ships in the Galactic Fleet. But fear not, for my quest was not solitary. I had a companion, an AI of improbable intelligence and wit: ChatGPT.

(As you might have guessed I got some help from ChatGPT in writing a catchy introductory paragraph as well.)

First, some background: the open-source {fmt} library provides two extension APIs to make a type formattable.

Let’s say you want to make the following enum formattable as the underlying type (int) with fmt::format:

enum class mode {read = 1, write = 2};

std::string s = fmt::format("{}", mode::read);
// s == "1"

To do this you can either provide a formatter specialization

template <> struct fmt::formatter<mode>: formatter<int> {
  auto fmt::formatter<mode>::format(mode m, format_context& ctx) const {
    return formatter<int>::format(std::to_underlying(m), ctx);
  }
};

or a format_as function:

auto format_as(mode m) { return std::to_underlying(m); }

Obviously the latter is much simpiler but I was wondering what is the performance difference between the two, if any.

Enter ChatGPT 4. I approached this AI with my query

Write a google benchmark comparing performance of format_as and formatter specialization

half-expecting generic responses. To my surprise, ChatGPT did a pretty good job at this task. The initial solution that it came up with was:

#include <benchmark/benchmark.h>
#include <fmt/format.h>

// Define a simple struct or enum
enum class Color { Red, Green, Blue };

// Define formatter specialization for Color
template <>
struct fmt::formatter<Color> {
    template <typename FormatContext>
    auto format(Color c, FormatContext& ctx) {
        std::string name;
        switch (c) {
            case Color::Red:   name = "Red"; break;
            case Color::Green: name = "Green"; break;
            case Color::Blue:  name = "Blue"; break;
        }
        return fmt::format_to(ctx.out(), "{}", name);
    }
};

// Define format_as for Color
constexpr auto format_as(const Color& color) -> std::string {
    switch (color) {
        case Color::Red:   return "Red";
        case Color::Green: return "Green";
        case Color::Blue:  return "Blue";
        default:           return "Unknown";
    }
}

// Benchmark for formatter specialization
static void BM_FormatterSpecialization(benchmark::State& state) {
    Color color = Color::Red; // or cycle through enum values
    for (auto _ : state) {
        std::string formatted = fmt::format("{}", color);
        benchmark::DoNotOptimize(formatted);
    }
}
BENCHMARK(BM_FormatterSpecialization);

// Benchmark for format_as
static void BM_FormatAs(benchmark::State& state) {
    Color color = Color::Red; // or cycle through enum values
    for (auto _ : state) {
        std::string formatted = fmt::format("{}", color);
        benchmark::DoNotOptimize(formatted);
    }
}
BENCHMARK(BM_FormatAs);

BENCHMARK_MAIN();

It had the correct structure but wouldn’t compile because of a conflict which I explained in my feedback:

This won’t compile because format_as and formatter specialization conflict with each other. To fix this introduce two enums with different names but otherwise identical, one for format_as, another for the formatter specialization.

Amazingly, ChatGPT had no problem in fixing this issue:

#include <benchmark/benchmark.h>
#include <fmt/format.h>

// Define two identical enums with different names
enum class ColorWithFormatAs { Red, Green, Blue };
enum class ColorWithFormatter { Red, Green, Blue };

// Define formatter specialization for ColorWithFormatter
template <>
struct fmt::formatter<ColorWithFormatter> {
    template <typename FormatContext>
    auto format(ColorWithFormatter c, FormatContext& ctx) {
        std::string name;
        switch (c) {
            case ColorWithFormatter::Red:   name = "Red"; break;
            case ColorWithFormatter::Green: name = "Green"; break;
            case ColorWithFormatter::Blue:  name = "Blue"; break;
        }
        return fmt::format_to(ctx.out(), "{}", name);
    }
};

// Define format_as for ColorWithFormatAs
constexpr auto format_as(const ColorWithFormatAs& color) -> std::string {
    switch (color) {
        case ColorWithFormatAs::Red:   return "Red";
        case ColorWithFormatAs::Green: return "Green";
        case ColorWithFormatAs::Blue:  return "Blue";
        default:                       return "Unknown";
    }
}

// Benchmark for formatter specialization
static void BM_FormatterSpecialization(benchmark::State& state) {
    ColorWithFormatter color = ColorWithFormatter::Red; // or cycle through enum values
    for (auto _ : state) {
        std::string formatted = fmt::format("{}", color);
        benchmark::DoNotOptimize(formatted);
    }
}
BENCHMARK(BM_FormatterSpecialization);

// Benchmark for format_as
static void BM_FormatAs(benchmark::State& state) {
    ColorWithFormatAs color = ColorWithFormatAs::Red; // or cycle through enum values
    for (auto _ : state) {
        std::string formatted = fmt::format("{}", color);
        benchmark::DoNotOptimize(formatted);
    }
}
BENCHMARK(BM_FormatAs);

BENCHMARK_MAIN();

However, the formatter specialization wasn’t quite right (missing parse) and also I wanted it to compare enums formatted as underlying types, not as strings. To be fair I didn’t specify this in my original prompt. After three more iterations ChatGPT produced a usable result:

#include <benchmark/benchmark.h>
#include <fmt/format.h>

// Define two identical enums with different names for separate formatting methods
enum class NumericEnumForFormatAs { One = 1, Two, Three };
enum class NumericEnumForFormatter { One = 1, Two, Three };

// Define formatter specialization for NumericEnumForFormatter to format as an integer
template <>
struct fmt::formatter<NumericEnumForFormatter> : fmt::formatter<int> {
    auto format(NumericEnumForFormatter e, fmt::format_context& ctx) {
        return fmt::formatter<int>::format(static_cast<int>(e), ctx);
    }
};

// Define format_as for NumericEnumForFormatAs to format as an integer
constexpr auto format_as(const NumericEnumForFormatAs& e) -> int {
    return static_cast<int>(e);
}

// Benchmark for formatter specialization
static void BM_FormatterSpecialization(benchmark::State& state) {
    NumericEnumForFormatter e = NumericEnumForFormatter::Two; // or cycle through enum values
    for (auto _ : state) {
        std::string formatted = fmt::format("{}", e);
        benchmark::DoNotOptimize(formatted);
    }
}
BENCHMARK(BM_FormatterSpecialization);

// Benchmark for format_as
static void BM_FormatAs(benchmark::State& state) {
    NumericEnumForFormatAs e = NumericEnumForFormatAs::Two; // or cycle through enum values
    for (auto _ : state) {
        std::string formatted = fmt::format("{}", e);
        benchmark::DoNotOptimize(formatted);
    }
}
BENCHMARK(BM_FormatAs);

BENCHMARK_MAIN();

There is still room for improvement but it’s a reasonable start and only required 5 iterations in total. Interestingly, it completely forgot about the conflict issue when switching from strings to integers so I had to remind it.

Running the benchmark confirmed the hypothesis that format_as is faster:

---------------------------------------------------------------------
Benchmark                           Time             CPU   Iterations
---------------------------------------------------------------------
BM_FormatterSpecialization       18.2 ns         18.1 ns     37152426
BM_FormatAs                      9.47 ns         9.46 ns     73865375

Conclusion

ChatGPT can save you some time when dealing with programming tasks that require fair amount of boilerplate like setting up a benchmark. However, to get decent results you still need to understand the nuances of the APIs and provide a lot of guidance. Otherwise you will likely get something that looks reasonable but is subtly wrong. If you are lucky it will be wrong enough to not compile like the first benchmark produced by ChatGPT here. But overall being able to meaningfullly iterate on a piece of code by conversing with an AI is an impressive feat of technology.


Last modified on 2023-12-14