Showing posts with label x86. Show all posts
Showing posts with label x86. Show all posts

Monday, June 19, 2017

A look at range-v3 code generation

I recently saw a Stack Overflow post that compared the speed of std::find_if on a vector vec
auto accumulated_length = 0L;
auto found = std::find_if(vec.begin(), vec.end(),
                          [&](auto const &val) {
                              accumulated_length += val;
                              return to_find < accumulated_length;
                          });
auto const found_index = std::distance(vec.begin(), found);

and the equivalent code using the range-v3 library
auto const found_index = ranges::distance(vec
    | ranges::view::transform(ranges::convert_to<long>{})
    | ranges::view::partial_sum()
    | ranges::view::take_while([=](auto const i) {
          return !(to_find < i);
      }));
Measuring the performance on an Intel Broadwell CPU using the Google benchmark library and this code compiled with the options
-O3 -march=native -std=c++14 -DNDEBUG
gives me the result
Benchmark                Time           CPU Iterations
------------------------------------------------------
BM_std/1024            311 ns        311 ns    2248354
BM_range/1024         2102 ns       2102 ns     332711
for gcc 7.1.0 and
BM_std/1024            317 ns        317 ns    2208547
BM_range/1024          809 ns        809 ns     864328
for clang 4.0.0. There are two obvious questions
  • Why is range-v3 slower than the STL?
  • Why is the difference so much bigger for GCC than for LLVM?

I also wanted to see if the STL added overhead, so I tried a simple C-style for-loop
long i, acc = 0;
for (i = 0; i < len; i++) {
    acc += p[i];
    if (to_find < acc)
        break;
}
found_index = i;
This runs in 439 ns – 40% slower than the STL version! – which adds the question
  • Why is the for-loop slower than the STL version?

Why is the for-loop slower?

GCC is generating the obvious assembly for the for-loop
.L4:
        movslq  (%r8,%rax,4), %rcx
        addq    %rcx, %rdx
        cmpq    %rsi, %rdx
        jg      .L7
.L3:
        addq    $1, %rax
        cmpq    %rdi, %rax
        jl      .L4
.L7:
        ...
I had expected the compiler to generate similar code for std::find_if, and that is what happens if it is used with an input iterator, but libstdc++ has an overload for random-access iterators which partially unrolls the loop
template<typename _RandomAccessIterator, typename _Predicate>
  _RandomAccessIterator
  __find_if(_RandomAccessIterator __first, _RandomAccessIterator __last,
            _Predicate __pred, random_access_iterator_tag)
  {
    typename iterator_traits<_RandomAccessIterator>::difference_type
      __trip_count = (__last - __first) >> 2;

    for (; __trip_count > 0; --__trip_count)
      {
        if (__pred(__first))
          return __first;
        ++__first;

        if (__pred(__first))
          return __first;
        ++__first;

        if (__pred(__first))
          return __first;
        ++__first;

        if (__pred(__first))
          return __first;
        ++__first;
      }

    switch (__last - __first)
      {
      case 3:
        if (__pred(__first))
          return __first;
        ++__first;
      case 2:
        if (__pred(__first))
          return __first;
        ++__first;
      case 1:
        if (__pred(__first))
          return __first;
        ++__first;
      case 0:
      default:
        return __last;
      }
  }
This partial unrolling gets rid of a large fraction of the comparisons and branches, which makes a big difference for this kind of micro-benchmark.

Why does GCC generate slow code for range-v3?

The range-v3 code generated by GCC have a few objects placed on the stack which adds some (useless) memory operations. The reason they are not optimized has to do with how GCC are optimizing structures and the order the optimization passes are being run.

The GCC “Scalar Replacement of Aggregates” (SRA) optimization pass splits structures into their elements. That is,
struct S {
  int a, b, c;
};

struct S s;

s.a = s.b = s.c = 0;
...
is transformed to the equivalent of
int a, b, c;

a = b = c = 0;
...
and the variables are then optimized and placed in registers in the same way as normal non-structure variables.

The compiler cannot split structures that have their address taken as it would then need to do expensive pointer tracking to find how each element is used, so such structures are kept on the stack. The GCC SRA pass is conservative and does not split a structure if any part of it has been captured by a pointer, such as
struct S s;

s.a = s.b = s.c = 0;
int *p = &s.a;
...
that could be split into
int a, b, c;

a = b = c = 0;
int *p = &a;
...
but that is not done by GCC.

It is usually not a problem that address-taking limits SRA as optimization passes such as constant propagation eliminates use of pointers when they are only used locally in a function, so code of the form
struct S s;

int *p = &s.a;
...
*p = 0;
is transformed to
struct S s;

...
s.a = 0;
which can then be optimized by SRA. But this requires that all paths to the use of p pass through the same initialization and that the compiler can see that they pass through the same initialization – we cannot easily eliminate the pointers for code such as
struct S s;
int *p;

if (cond)
    p = &s.a;
...
if (cond)
    *p = 0;
that need the compiler to track values to see that all executions of *p initializes p to &s.a.

And that is how the range-v3 code looks like after templates has been expanded and all functions inlined – the code does different initializations depending on if the range is empty or not and ends up with code segments of the form
if (begin != end) {
    // Initialize some variables
}

...

if (begin != end) {
    // Use the variables
}
I have a hard time trying to follow exactly what range-v3 is trying to do – the code expands to more than 700 functions, so I have only looked at the compiler’s IR after inlining and I do not know exactly how it look in the C++ source code – but the result is that the compiler fails to propagate some addresses due to this issue and three objects (one struct take_while_view and two struct basic_iterator) are still placed on the stack when the last SRA pass has been run.

GCC do eventually manage to simplify the code enough that SRA could eliminate all structures, but that is later in the optimization pipeline, after the last SRA pass has been run. I tested to add an extra late SRA pass – this eliminates the memory operations, and the function runs in 709 ns. Much better, but still only half the speed of the STL version.

Why is range-v3 slower than the STL?

Both GCC and LLVM generate the range-v3 code to something of the form
static long foo(const int *begin, const int *end, long to_find)
{
    long result = 0;
    const int *p = begin;
    if (begin != end) {
        result = *begin;
        while (1) {
            if (p == end)
                break;
            if (to_find < result)
                break;

            p++;
            if (p != end)
                result += *p;
        }
    }
    return p - begin;
}
that does one extra comparison in the loop body compared to the for-loop version. This kind of code is supposed to be simplified by the loop optimizers, but they are running relatively early in the optimization pipeline (partly so that later optimizations may take advantage of the improved loop structure, and partly as many optimizations makes life harder for the loop optimizer) so they are limited by the same issues mentioned in the previous section – that is, I assume the redundant comparison would be eliminated if the range-v3 library improved its handling of empty ranges etc.

Tuesday, May 16, 2017

Integer division is slow

The CppCon 2016 talk “I Just Wanted a Random Integer!” benchmarks randomization functionality from the C++ standard library (using GCC 5.1). There is one surprising result — the loop
std::random_device entropySource;
std::mt19937 randGenerator(entropySource());
std::uniform_int_distribution<int> theIntDist(0, 99);

for (int i = 0; i < 1000000000; i++) {
  volatile auto r = theIntDist(randGenerator);
}
need 23.4 seconds to run while
std::random_device entropySource;
std::mt19937 randGenerator(entropySource());

for (int i = 0; i < 1000000000; i++) {
  std::uniform_int_distribution<int> theIntDist(0, 99);
  volatile auto r = theIntDist(randGenerator);
}
run in 5.1 seconds. But the latter should intuitively be slower as it does more in the loop...

Code expansion

The functionality in the standard library is implemented using template magic, but the compiler’s view of the code after inlining and basic simplification is that
std::uniform_int_distribution<int> theIntDist(0, 99);
is just defining and initializing a structure
struct {
  int a, b;
} theIntDist;
theIntDist.a = 0;
theIntDist.b = 99;
while the call
volatile auto r = theIntDist(randGenerator);
is expanded to the equivalent of
uint64_t ret;
uint64_t urange = theIntDist.b - theIntDist.a;
if (0xffffffff > urange) {
  const uint64_t uerange = urange + 1;
  const uint64_t scaling = 0xffffffff / uerange;
  const uint64_t past = uerange * scaling;
  do {
    ret = mersenne_twister_engine(randGenerator);
  } while (ret >= past);
  ret /= scaling;
} else {
  ...
  uniform_int_distribution(&theIntDist, randGenerator);
  ...
  ret = ...
}
volatile int r = ret + theIntDist.a;
where I have used ... for code that is not relevant for the rest of the discussion.

Optimization differences

It is now easy to see why the second case is faster — creating theIntDist in the loop makes it trivial for the compiler to determine that urange has the value 99, and the code simplifies to
uint64_t ret;
do {
  ret = mersenne_twister_engine(randGenerator);
} while (ret >= 4294967200);
ret /= 42949672;
volatile int r = ret;
This simplification is not possible when theIntDist is created outside of the loop — the compiler sees that the loop calls uniform_int_distribution with a reference to theIntDist, so it must assume that the value of theIntDist.a and theIntDist.b may change during the execution and can therefore not do the constant folding. The function does, however, not modify theIntDist, so both versions of the program do the same work, but the slow version needs to do one extra comparison/branch and a few extra arithmetic instructions for each loop iteration.

The cost of division

The mersenne_twister_engine is not a big function, but it is not trivial — it executes about 40 instructions — so it is surprising that adding a few instructions to the loop makes the program four times slower. I described a similar case in a previous blog post where the problem were due to branch mis-prediction, but the branch is perfectly predicted in  this example.

The reason here is that the slow loop need to do an integer division instruction when calculating scaling, and integer division is expensive — Agner Fog’s instruction tables says that the 64-bit division may need up to 103 cycles on the Broadwell microarchitecture! This usually does not matter too much for normal programs as as the compiler tries to move the division instructions so that they have as much time as possible to execute before the result is needed, and the CPU can in general continue executing other instructions out of order while waiting for the result of the division. But it does make a big difference in this kind of micro-benchmarks as the compiler cannot move the division earlier, and the CPU runs out of work to do out of order as the mersenne_twister_engine function executes much faster than the division.

Sunday, March 5, 2017

The cost of conditional moves and branches

The previous blog post contained an example where branching was much more expensive than using a conditional move, but it is easy to find cases where conditional moves reduce performance noticeably. One such case is in this stack overflow question (and GCC bug 56309) discussing the performance of a function implementing a naive bignum multiplication
static void inline
single_mult(const std::vector<ull>::iterator& data,
            const std::vector<ull>::const_iterator& rbegin,
            const std::vector<ull>::const_iterator& rend,
            const ull x)
{
    ull tmp=0, carry=0, i=0;
    for (auto rhs_it = rbegin; rhs_it != rend; ++rhs_it)
    {
        tmp = x * (*rhs_it) + data[i] + carry;
        if (tmp >= imax) {
            carry = tmp >> numbits;
            tmp &= imax - 1;
        } else { 
            carry = 0;
        }
        data[i++] = tmp;
    }
    data[i] += carry;
}

void
naive(std::vector<ull>::iterator data, 
      std::vector<ull>::const_iterator cbegin,
      std::vector<ull>::const_iterator cend,
      std::vector<ull>::const_iterator rbegin,
      std::vector<ull>::const_iterator rend)
{
    for (auto data_it = cbegin; data_it != cend; ++data_it)
    {
        if (*data_it != 0) {
            single_mult(data, rbegin, rend, *data_it);
        }
        ++data;
    }
}
Minor changes to the source code made the compiler use conditional moves instead of a branch, and this reduced the performance by 25%.

The difference between branches and conditional moves can be illustrated by
a = a + b;
if (c > 0)
    a = -a;
a = a + 1;
It is not possible to calculate the number of clock cycles for a code segment when working with reasonably complex CPUs, but it is often easy to get a good estimate (see e.g. this example for how to use such estimates when optimizing assembly code). The CPU converts the original instructions to micro-ops, and it can dispatch several micro-ops per cycle (e.g. 8 for Broadwell). The details are somewhat complicated,1 but most instructions in this blog post are translated to one micro-op that can be executed without any restrictions.

An assembly version using a branch looks like (assuming that the variables are placed in registers)
    addl    %edx, %eax
    testl   %ecx, %ecx
    jle     .L2
    negl    %eax
.L2:
    addl    $1, %eax
The CPU combines the testl and jle instructions to one micro-op by what is called “macro-fusion”, so both the addition and the test/branch instructions can be dispatched in the first cycle. It takes a while for the compare and branch to execute, but branch prediction means that the CPU can speculatively start executing the next instruction in the following cycle, so the final addl or the negl can be dispatched in the second cycle (depending on if the branch is predicted as taken or not). The result is that the code segment is done in 2 or 3 cycles, provided that the branch prediction was correct — a mispredict must discard the speculated instructions and restart execution, which typically adds 15–20 cycles.

Generating a version using a conditional move produces something like
    addl    %edx, %eax
    movl    %eax, %edx
    negl    %edx
    testl   %ecx, %ecx
    cmovg   %edx, %eax
    addl    $1, %eax
The first cycle will execute the first addition and the test instruction, and the following cycles will only be able to execute one instruction at a time as all of them depend on the previous instruction. The result is that this needs 5 cycles to execute.2

So the version with conditional moves takes twice the time to execute compared to the version using a branch, which is noticeable in the kind of short loops from single_mult. In addition, pipeline-restrictions on how instructions can be dispatched (such as only one division instruction can be dispatched each cycle) makes it hard for the CPU to schedule long dependency chains efficiently, which may be a problem for more complex code.


1. See “Intel 64 and IA-32 Architectures Optimization Reference Manual” and Agner Fog’s optimization manuals for the details.
2. This assumes that the cmovg instruction is one micro-op. That is true for some CPUs such as Broadwell, while others split it into two micro-ops.

Wednesday, February 22, 2017

Branch prediction

Branch misprediction is expensive: an example

The SciMark 2.0 Monte Carlo benchmark is calculating \(\pi\) by generating random points \(\{(x,y) \mid x,y \in [0,1]\}\) and calculating the ratio of points that are located within the quarter circle \(\sqrt{x^2 + y^2} \le 1\). The square root can be avoided by squaring both sides, and the benchmark is implemented as
double MonteCarlo_integrate(int Num_samples)
{
    Random R = new_Random_seed(SEED);
    int under_curve = 0;

    for (int count = 0; count < Num_samples; count++)
    {
        double x = Random_nextDouble(R);
        double y = Random_nextDouble(R);
        if (x*x + y*y <= 1.0)
            under_curve++;
    }

    Random_delete(R);
    return ((double) under_curve / Num_samples) * 4.0;
}
GCC used to generate a conditional move for this if-statement, but a recent change made this generate a normal branch which caused a 30% performance reduction for the benchmark due to the branch being mispredicted (bug 79389).

The randomization function is not inlined as it is compiled in a separate file, and it contains a non-trivial amount of loads, stores, and branches
typedef struct
{
    int m[17];
    int seed, i, j, haveRange;
    double left, right, width;
} Random_struct, *Random;

#define MDIG 32
#define ONE 1
static const int m1 = (ONE << (MDIG-2)) + ((ONE << (MDIG-2)) - ONE);
static const int m2 = ONE << MDIG/2;
static double dm1;

double Random_nextDouble(Random R) 
{
    int I = R->i;
    int J = R->j;
    int *m = R->m;

    int k = m[I] - m[J];
    if (k < 0)
        k += m1;
    R->m[J] = k;

    if (I == 0) 
        I = 16;
    else
        I--;
    R->i = I;

    if (J == 0) 
        J = 16;
    else
        J--;
    R->j = J;

    if (R->haveRange) 
        return  R->left +  dm1 * (double) k * R->width;
    else
        return dm1 * (double) k;
}
so I had expected the two calls to this function to dominate the running time, and that the cost of the branch would not affect the benchmark too much. But I should have known better — x86 CPUs can have more than 100 instructions in flight (192 micro-ops for Broadwell), and a mispredict need to throw away all that work and restart from the actual branch target.


Branch overhead and branch prediction

The cost of branch instructions differ between different CPU implementations, and the compiler needs to take that into account when optimizing and generating branches.

Simple processors with a 3-stage pipeline fetch the next instruction when previous two instructions are decoded and executed, but branches introduce a problem: the next instruction cannot be fetched before the address is calculated by executing the branch instruction. This makes branches expensive as they introduce bubbles in the pipeline. The cost can be reduced for conditional branches by speculatively fetching and decoding the instructions after the branch — this improves performance if the branch was not taken, but taken branches need to discard the speculated work, and restart from the actual branch target.

Some CPUs have instructions that can execute conditionally depending on a condition, and this can be used to avoid branches. For example
if (c)
    a += 3;
else
    b -= 2;
can be compiled to the following straight line code on ARM (assuming that a, b, and c are placed in r1, r2, and r0 respectively)
cmp     r0, #0
addne   r1, r1, #3
subeq   r2, r2, #2
The cmp instruction sets the Z flag in the status register, and the addne instruction is treated as an addition if Z is 0, and as a nop instruction if Z is 1. subeq is similarly treated as a subtraction if Z is 1 and as a nop if Z is 0. The instruction takes time to execute, even when treated as a nop, but this is still much faster than executing branches.

This means that the compiler should structure the generated code so that branches are minimized (using conditional execution when possible), and conditional branches should be generated so that the most common case is not taking the branch.

Taken branches become more expensive as the CPUs get deeper pipelines, and this is especially annoying as loops must branch to the top of the loop for each iteration. This can be solved by adding more hardware to let the fetch unit calculate the target address of the conditional branch, and the taken branch can now be the cheap case.

It is, however, nice to have the “not taken” case be the cheap case, as the alternative often introduce contrived control flow that fragments the instruction cache and need to insert extra “useless” (and expensive) unconditional branches. The way most CPUs solve this is to predict that forward branches are unlikely (and thus speculatively fetch from following instructions), and that backward branches are likely (and thus speculatively fetch from the branch target).

The compiler should do similar work as for the simpler CPU, but structure the code so that conditional branches branching forward are not taken in the common case, and conditional branches branching backward are taken in the common case.

There are many branches that the compiler cannot predict, so the next step up in complexity is adding branch prediction to the CPU. The basic idea is that the CPU keeps a cache of previous branch decisions and use this to predict the current branch. High-end branch predictors look at the history of code flow, and can correctly predict repetitive patterns in how the branch behaved. Hardware vendors do not publish detailed information about how the prediction work, but Agner Fog’s optimization manuals contain lots of information (especially part 3, “The microarchitecture of Intel, AMD and VIA CPUs”, that also have a good overview of different ways branch prediction can be done).

Branch prediction in high-end CPUs is really good, so branches are essentially free, while conditional execution adds extra dependencies between instructions which constrain the out-of-order execution engine, so conditional execution should be avoided. This is essentially the opposite from how the simple CPUs should be handled. 😃

There is one exception — branches that cannot be predicted (such as the one in SciMark) should be generated using conditional instructions, as the branch will incur the misprediction cost of restarting the pipeline each time it is mispredicted.

The compiler should not use conditional execution unless the condition is unpredictable. The code should be structured as for the static prediction (this is not strictly necessary, but most CPUs use the static prediction first time a branch is encountered. And it is also slightly more efficient for the instruction cache).

So branches are free, except when they cannot be predicted. I find it amusing that many algorithms (balanced search trees, etc.) have the aim to make the branches as random as possible. I do not know how much this is a problem in reality, but Clang has a built-in function, __builtin_unpredictable, that can be used to tell the compiler that the condition is unpredictable.


Heuristics for estimating branch probabilities

The compiler estimates branch probabilities in order to generate the efficient form of the branch (there are more optimizations that need to know if a code segment is likely executed or not, such as inlining and loop unrolling). The general idea, as described in the PLDI ’93 paper “Branch Prediction for Free”, is to look at how the branches are used. For example, code such as
if (p == NULL)
    return -1;
comparing a pointer with NULL and returning a constant, is most likely error handling, and thus unlikely to execute the return statement.

GCC has a number of such predictors, for example
  • Branch ending with returning a constant is probably not taken.
  • Branch from comparison using != is probably taken, == is probably not taken.
  • Branch to a basic block calling a cold function is probably not taken.
Each predictor provides a probability (that has been set from branch frequencies observed by instrumenting real world code), and these probabilities are combined for a final result. This is one reason why __builtin_expect often does not make any difference — the heuristics are already coming to the same conclusion!

The predictors are defined in predict.def (some of the definitions seem reversed due to how the rules are implemented, e.g. PROB_VERY_LIKELY may mean “very unlikely”, but the comments describing each heuristic are correct). You can see how GCC is estimating the branch probabilities by passing -fdump-tree-profile_estimate to the compiler, which writes a file containing the output from the predictors for each basic block
Predictions for bb 2
  DS theory heuristics: 1.7%
  combined heuristics: 1.7%
  pointer (on trees) heuristics of edge 2->4: 30.0%
  call heuristics of edge 2->3: 33.0%
  negative return heuristics of edge 2->4: 2.0%
as well as (when using GCC 7.x) the IR annotated with the estimated probabilities.