Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modern C++: initialize constexpr tables

Tags:

Suppose I have a class X, which functionality requires a lot of constant table values, say an array A[1024]. I have a recurrent function f that computes its values, smth like

A[x] = f(A[x - 1]); 

Suppose that A[0] is a known constant, therefore the rest of the array is constant too. What is the best way to calculate these values beforehand, using features of modern C++, and without storaging file with hardcoded values of this array? My workaround was a const static dummy variable:

const bool X::dummy = X::SetupTables();  bool X::SetupTables() {     A[0] = 1;     for (size_t i = 1; i <= A.size(); ++i)         A[i] = f(A[i - 1]); } 

But I believe, it’s not the most beautiful way to go. Note: I emphasize that array is rather big and I want to avoid monstrosity of the code.

like image 509
Aleksandr Samarin Avatar asked Nov 14 '17 11:11

Aleksandr Samarin


People also ask

How do I declare constexpr?

constexpr variables A constexpr variable must be initialized at compile time. All constexpr variables are const . A variable can be declared with constexpr , when it has a literal type and is initialized. If the initialization is performed by a constructor, the constructor must be declared as constexpr .

When to use #define vs constexpr?

#define directives create macro substitution, while constexpr variables are special type of variables. They literally have nothing in common beside the fact that before constexpr (or even const ) variables were available, macros were sometimes used when currently constexpr variable can be used.

Does constexpr improve performance?

Using constexpr to Improve Security, Performance and Encapsulation in C++ constexpr is a new C++11 keyword that rids you of the need to create macros and hardcoded literals. It also guarantees, under certain conditions, that objects undergo static initialization.

Should you use constexpr everywhere?

Yes. I believe putting such const ness is always a good practice wherever you can. For example in your class if a given method is not modifying any member then you always tend to put a const keyword in the end. Now putting constexpr suggests that get() must also be a constexpr .


2 Answers

Since C++14, for loops are allowed in constexpr functions. Moreover, since C++17, std::array::operator[] is constexpr too.

So you can write something like this:

template<class T, size_t N, class F> constexpr auto make_table(F func, T first) {     std::array<T, N> a {first};     for (size_t i = 1; i < N; ++i)     {         a[i] = func(a[i - 1]);     }     return a; } 

Example: https://godbolt.org/g/irrfr2

like image 91
Bob__ Avatar answered Oct 10 '22 04:10

Bob__


I think this way is more readable:

#include <array>  constexpr int f(int a) { return a + 1; }  constexpr void init(auto &A) {   A[0] = 1;   for (int i = 1; i < A.size(); i++) {     A[i] = f(A[i - 1]);   } }  int main() {   std::array<int, 1024> A;   A[0] = 1;   init(A); } 

I need to make a disclaimer, that for big array sizes it is not guaranteed to generate array in constant time. And the accepted answer is more likely to generate the full array during template expansion.

But the way I propose has number of advantages:

  1. It is quite safe that the compiler will not eat up all your memory and fails to expand the template.
  2. The compilation speed is significantly faster
  3. You use C++-ish interface when you use an array
  4. The code is in general more readable

In a particular example when you need only one value, the variant with templates generated for me only a single number, while the variant with std::array generated a loop.

Update

Thanks to Navin I found a way to force compile time evaluation of the array.

You can force it to run at compile time if you return by value: std::array A = init();

So with slight modification the code looks as follows:

#include <array>  constexpr int f(int a) { return a + 1; }  constexpr auto init() {   // Need to initialize the array   std::array<int, SIZE> A = {0};   A[0] = 1;   for (unsigned i = 1; i < A.size(); i++) {     A[i] = f(A[i - 1]);   }   return A; }  int main() {   auto A = init();   return A[SIZE - 1]; } 

To have this compiled one needs C++17 support, otherwise operator [] from std::array is not constexpr. I also update the measurements.

On assembly output

As I mentioned earlier the template variant is more concise. Please look here for more detail.

In the template variant, when I just pick the last value of the array, the whole assembly looks as follows:

main:   mov eax, 1024   ret 

While for std::array variant I have a loop:

main:         subq    $3984, %rsp         movl    $1, %eax .L2:         leal    1(%rax), %edx         movl    %edx, -120(%rsp,%rax,4)         addq    $1, %rax         cmpq    $1024, %rax         jne     .L2         movl    3972(%rsp), %eax         addq    $3984, %rsp         ret 

With std::array and return by value the assemble is identical to version with templates:

main:   mov eax, 1024   ret 

On compilation speed

I compared these two variants:

test2.cpp:

#include <utility>  constexpr int f(int a) { return a + 1; }  template<int... Idxs> constexpr void init(int* A, std::integer_sequence<int, Idxs...>) {     auto discard = {A[Idxs] = f(A[Idxs - 1])...};     static_cast<void>(discard); }  int main() {     int A[SIZE];     A[0] = 1;     init(A + 1, std::make_integer_sequence<int, sizeof A / sizeof *A - 1>{}); } 

test.cpp:

#include <array>  constexpr int f(int a) { return a + 1; }  constexpr void init(auto &A) {     A[0] = 1;     for (int i = 1; i < A.size(); i++) {         A[i] = f(A[i - 1]);     } }  int main() {     std::array<int, SIZE> A;     A[0] = 1;     init(A); } 

The results are:

|  Size | Templates (s) | std::array (s) | by value | |-------+---------------+----------------+----------| |  1024 |          0.32 |           0.23 | 0.38s    | |  2048 |          0.52 |           0.23 | 0.37s    | |  4096 |          0.94 |           0.23 | 0.38s    | |  8192 |          1.87 |           0.22 | 0.46s    | | 16384 |          3.93 |           0.22 | 0.76s    | 

How I generated:

for SIZE in 1024 2048 4096 8192 16384 do     echo $SIZE     time g++ -DSIZE=$SIZE test2.cpp     time g++ -DSIZE=$SIZE test.cpp     time g++ -std=c++17 -DSIZE=$SIZE test3.cpp done 

And if you enable optimizations, the speed of code with template is even worse:

|  Size | Templates (s) | std::array (s) | by value | |-------+---------------+----------------+----------| |  1024 |          0.92 |           0.26 | 0.29s    | |  2048 |          2.81 |           0.25 | 0.33s    | |  4096 |         10.94 |           0.23 | 0.36s    | |  8192 |         52.34 |           0.24 | 0.39s    | | 16384 |        211.29 |           0.24 | 0.56s    | 

How I generated:

for SIZE in 1024 2048 4096 8192 16384 do     echo $SIZE     time g++ -O3 -march=native -DSIZE=$SIZE test2.cpp     time g++ -O3 -march=native -DSIZE=$SIZE test.cpp     time g++ -O3 -std=c++17 -march=native -DSIZE=$SIZE test3.cpp done 

My gcc version:

$ g++ --version g++ (Debian 7.2.0-1) 7.2.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
like image 42
mcsim Avatar answered Oct 10 '22 06:10

mcsim