Consider using constexpr static function variables for performance in C++

When programming, we often need constant variables that are used within a single function. For example, you may want to look up characters from a table. The following function is efficient:

char table(int idx) {
  const char array[] = {'z', 'b', 'k', 'd'};
  return array[idx];
}

It gets trickier if you have constants that require initialization. For example, the following is terrible code:

std::string table(int idx) {
  const std::string array[] = {"a", "l", "a", "z"};
  return array[idx];
}

It is terrible because it is possible that the compiler will create all string instances each time you enter the function, and then throw them away immediately. To fix this problem, you may declare the array to be ‘static’. It tells the compiler that you want the string instances to be initialized just exactly once in C++11. There is a one-to-one map between the string instances and the function instances.

const std::string& table(int idx) {
  const static std::string array[] = {"a", "l", "a", "z"};
  return array[idx];
}

But how does the compiler ensures that the initialization occurs just once? It may do so by using a guard variable, and loading this guard variable each time the function is called. If the variable indicates that the strings may not have been instantiated yet, a thread-safe routine is called and such a routine proceeds with the initialization if needed, setting the guard variable to indicate that no initialization is required in the future.

This initialization is inexpensive, and the latter checks are inexpensive as well. But they are not free and they generate a fair amount of binary code (e.g., 60 instructions or more!). A better approach is to tell the compiler that you want the initialization to occur at compile time. In this manner, there is no overhead whatsoever when you call the function. There is no guard variable. You get direct access to your constants. Unfortunately, it is not generally possible to have C++ string instances be instantiated at compile time, but it is possible with the C++17 counterpart ‘string_view’. We can declare the constant variables with the attributes constexpr static. The attribute constexpr tells the compiler to do the work at compile time. The resulting code is most efficient:

std::string_view table(int idx) {
  constexpr static std::string_view array[] = {"a", "l", "a", "z"};
  return array[idx];
}

It may compile to just this assembly:

  movsx rdi, edi
  sal rdi, 4
  mov rax, QWORD PTR table(int)::array[rdi]
  mov rdx, QWORD PTR table(int)::array[rdi+8]

I wrote a little benchmark to illustrate the effect. Your results will vary depending on your system. I using an Apple M1 processing with LLVM 14 and an Ice Lake processor with GCC 12. My source code is available.

function Apple M1, LLVM 14 Intel Ice Lake, GCC 12
constexpr static string_view 0.9 ns/call 2.2 ns/call
static string 2.0 ns/call 2.3 ns/call
string 6.6 ns/call 16 ns/call

Though the performance difference between the static string approach and the constexpr static string_view is small and may not matter if the function is called often, the constexpr static string_view code will generate less bloat in general.

Daniel Lemire, "Consider using constexpr static function variables for performance in C++," in Daniel Lemire's blog, April 12, 2023.

Published by

Daniel Lemire

A computer science professor at the University of Quebec (TELUQ).

3 thoughts on “Consider using constexpr static function variables for performance in C++”

  1. Hi!

    I wonder if the benchmark between std::string and std::string_view is 100% fair.

    Your functions that return std::string need to explicitly copy the grabbed string. If you make your “static const std::string” version return by reference, it really makes it as fast as the “static std::string_view” version.

    About code bloat, I fully agree that the constexpr version is better.

    Anyway, thanks for sharing!

  2. The reason why static and constexpr perform similar is that, most likely, the compiler will detect that the array is a constant expression (even if not specified), and remove it. This can be observed with your example-code in Godbolt. This is true for most code – a lot of the time, an optimizing compiler can do the same work regardless of whether a function is marked constexpr or not (as long as it can see the definition).

    constexpr is mainly used to enforce this, even in a debug-build (aside from all the other uses where non-constexpr function cannot be used to). constexpr also forced the functions definition to be visible when used.

Leave a Reply

Your email address will not be published.

You may subscribe to this blog by email.