Home Helping the compiler with [[assume]]
Post
Cancel

Helping the compiler with [[assume]]

I wrote once about std::unreachable (aka __builtin_unreachable before C++23): link to the post.

This command tells the compiler that the program will never get into this branch of execution (the programmer absolutely guarantees it), so the compiler may optimize this place knowing that.

A similar functionality was standardized in C++23: the [[assume(expr)]] attribute (aka __builtin_assume before C++23).

This thing tells the compiler that in this point of the program the expr expression should be considered equal to true, so that the compiler may make various optimizations based on this data. The expr expression will not be evaluated while the program is running, this is a compile-time hint.

There is little information on cppreference (the link above), so it is better to read the proposal: https://wg21.link/p1774r8.

The simplest example is a method that divides a number by 32:

1
2
3
int div32(int x) {
    return x / 32;
}

An obvious optimization is not to divide by 32, but to make a bit shift by 5 bits, right?

1
2
3
int div32(int x) {
    return x >> 5;
}

But it will not work correctly on negative numbers! The compiler should always take into account negative numbers, and because of this, the method is larger in size: link to godbolt.

If the programmer knows for sure that all numbers are going to be non-negative, then they need to do this:

1
2
3
4
int div32_2(int x) {
    [[assume(x >= 0)]]; // or __builtin_assume(x >= 0);
    return x / 32;
}

And then the code is optimized: link to godbolt.

In more complex examples, which are demonstrated in the proposal, it is possible to greatly reduce the number of assembler instructions, especially in computational programs.

Some assumes can be made common in the standard library (there is an example with smart pointers in the proposal), but in general this is a functionality for a narrow set of developers. There are several things needed to keep in mind:

  1. You really need to depend a lot on the speed of the code, for example, if you developing realtime programs. I once posted a video by Timur Dumler (by the way he is the author of the โ€œproposalโ€) on this topic.
  2. You need to understand how the instructions are going to be cut off. Example of a program that clarifies array values via std::clamp:
    1
    2
    3
    4
    5
    6
    7
    8
    
    void limiter(float* data, size_t size) {
     [[assume(size > 0)]];
     [[assume(size % 32 == 0)]];
     for (size_t i = 0; i < size; ++i) {
         [[assume(std::isfinite(data[i]))]];
         data[i] = std::clamp(data[i], -1.0f, 1.0f);
     }
    }
    

    Assuming that the buffer size is always greater than 0 and a multiple of 32, and the floats are normalized, the programmer puts assumes. The first and third assume does not allow you the compiler to generate unnecessary checks, and the second assume is probably somehow connected with the processor cache line size.

  3. You need to constantly go into the assembler of a compiled program and check whether the assumes did what you wanted. You even may need to write unit tests on the generated assembler (I would write it). After all, C++ compilers have a lot of tests checking the resulting assembler, so similar tests are needed for individual programs with assumes.
  4. The standard notes that compilers are free to optimize the code as they want, no requirements are imposed on them. So it is necessary to check how a separate compiler and even a separate version works on your assumes. For preserving the behaviour we can use unit tests (described in the 3rd paragraph).

We can make some fun things with assumes ๐Ÿ˜

This post is licensed under CC BY 4.0 by the author.

Branch tables for switch operators

The simplest std::function explanation in 15 minutes