Странное снижение производительности при итерации неполных строк в двумерных массивах - PullRequest
2 голосов
/ 01 апреля 2020

Во время оптимизации части кода для Cortex-A53 (aarch64) я столкнулся со странным поведением кэша.

В уменьшенном случае (см. Ниже) я читаю числа из двумерного массива, строки по ряду. Странно то, что если я не перебираю всю строку, но короткая заключительная часть опускается, время обработки увеличивается. Более того, при переходе от последней строки к первой этот эффект не наблюдается.

Следующий код демонстрирует это поведение с использованием каркаса google-benchmark.

#include <iostream>
#include <benchmark/benchmark.h>

constexpr int CACHE_LINE_SIZE = 64;

using ElementType = int32_t;
constexpr int BUFFER_H = 1024;
constexpr int BUFFER_W = 4096; // in bytes

template <int X_END_BYTES> void BM_go_forward(benchmark::State& state)
{
    static __attribute__((aligned(CACHE_LINE_SIZE)))
    ElementType data[BUFFER_H][BUFFER_W / sizeof(ElementType)] = {5};
    int X_END = X_END_BYTES / sizeof(ElementType);
    for (auto _: state)
    {
        auto tmp = 0;
        for (int yc = 0; yc < BUFFER_H; ++yc)
        {
            for (int x = 0; x < X_END; ++x)
            {
                tmp += data[yc][x];
                tmp /= 2;
            }
        }
        benchmark::DoNotOptimize(tmp);
    }
}

template <int X_END_BYTES> void BM_go_backward(benchmark::State& state)
{
    static __attribute__((aligned(CACHE_LINE_SIZE)))
    ElementType data[BUFFER_H][BUFFER_W / sizeof(ElementType)] = {5};
    int X_END = X_END_BYTES / sizeof(ElementType);
    for (auto _: state)
    {
        auto tmp = 0;
        int y = BUFFER_H-1;
        for (int yc = 0; yc < BUFFER_H; ++yc, --y)
        {
            for (int x = 0; x < X_END; ++x)
            {
                tmp += data[y][x];
                tmp /= 2;
            }
        }
        benchmark::DoNotOptimize(tmp);
    }
}


constexpr int ITERS = 100;

BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 1)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 2)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 4)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 6)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 8)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 10)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 12)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 14)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 16)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 20)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 24)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 28)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_forward, BUFFER_W - CACHE_LINE_SIZE * 32)->Iterations(ITERS);

BENCHMARK_TEMPLATE(BM_go_backward, BUFFER_W)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_backward, BUFFER_W - CACHE_LINE_SIZE * 1)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_backward, BUFFER_W - CACHE_LINE_SIZE * 2)->Iterations(ITERS);
BENCHMARK_TEMPLATE(BM_go_backward, BUFFER_W - CACHE_LINE_SIZE * 4)->Iterations(ITERS);

вывод на Raspberry PI 3B + выглядит следующим образом:

--------------------------------------------------------------------------------------------------------
Benchmark                                                              Time             CPU   Iterations
--------------------------------------------------------------------------------------------------------
BM_go_forward<BUFFER_W>/iterations:100                              5.67 ms         5.67 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 1>/iterations:100        6.53 ms         6.53 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 2>/iterations:100        6.46 ms         6.46 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 4>/iterations:100        7.22 ms         7.22 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 6>/iterations:100        7.02 ms         7.02 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 8>/iterations:100        6.77 ms         6.77 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 10>/iterations:100       6.51 ms         6.50 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 12>/iterations:100       5.94 ms         5.94 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 14>/iterations:100       4.69 ms         4.69 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 16>/iterations:100       4.50 ms         4.50 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 20>/iterations:100       4.17 ms         4.17 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 24>/iterations:100       3.84 ms         3.84 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 28>/iterations:100       3.48 ms         3.48 ms          100
BM_go_forward<BUFFER_W - CACHE_LINE_SIZE * 32>/iterations:100       3.14 ms         3.14 ms          100
BM_go_backward<BUFFER_W>/iterations:100                             6.00 ms         6.00 ms          100
BM_go_backward<BUFFER_W - CACHE_LINE_SIZE * 1>/iterations:100       5.81 ms         5.81 ms          100
BM_go_backward<BUFFER_W - CACHE_LINE_SIZE * 2>/iterations:100       5.75 ms         5.75 ms          100
BM_go_backward<BUFFER_W - CACHE_LINE_SIZE * 4>/iterations:100       5.53 ms         5.53 ms          100

Я попытался выполнить профилирование с помощью oprofile Я прикрепляю результаты для событий L1D_CACHE_REFILL и PREFETCH_LINEFIL:

CPU: ARM Cortex-A53, speed 1400 MHz (estimated)
Counted L1D_CACHE_REFILL events (Level 1 data cache refill) with a unit mask of 0x00 (No unit mask) count 10007
Counted PREFETCH_LINEFILL events (Linefill because of prefetch) with a unit mask of 0x00 (No unit mask) count 10007
samples  %        samples  %        image name               symbol name
31        1.6316  639       6.7214  all_benchmarks           void BM_go_forward<4096>(benchmark::State&)
121       6.3684  545       5.7326  all_benchmarks           void BM_go_forward<4032>(benchmark::State&)
123       6.4737  544       5.7221  all_benchmarks           void BM_go_forward<3968>(benchmark::State&)
213      11.2105  454       4.7754  all_benchmarks           void BM_go_forward<3840>(benchmark::State&)
207      10.8947  453       4.7649  all_benchmarks           void BM_go_forward<3712>(benchmark::State&)
200      10.5263  456       4.7965  all_benchmarks           void BM_go_forward<3584>(benchmark::State&)
193      10.1579  463       4.8701  all_benchmarks           void BM_go_forward<3456>(benchmark::State&)
147       7.7368  491       5.1646  all_benchmarks           void BM_go_forward<3328>(benchmark::State&)
48        2.5263  583       6.1323  all_benchmarks           void BM_go_forward<3200>(benchmark::State&)
50        2.6316  557       5.8588  all_benchmarks           void BM_go_forward<3072>(benchmark::State&)
46        2.4211  525       5.5222  all_benchmarks           void BM_go_forward<2816>(benchmark::State&)
46        2.4211  484       5.0910  all_benchmarks           void BM_go_forward<2560>(benchmark::State&)
39        2.0526  435       4.5756  all_benchmarks           void BM_go_forward<2304>(benchmark::State&)
40        2.1053  403       4.2390  all_benchmarks           void BM_go_forward<2048>(benchmark::State&)
61        3.2105  614       6.4584  all_benchmarks           void BM_go_backward<4096>(benchmark::State&)
59        3.1053  612       6.4374  all_benchmarks           void BM_go_backward<4032>(benchmark::State&)
60        3.1579  606       6.3743  all_benchmarks           void BM_go_backward<3968>(benchmark::State&)
52        2.7368  615       6.4689  all_benchmarks           void BM_go_backward<3840>(benchmark::State&)

Масштабирование процессора составляет отключен. Процесс был запущен в режиме реального времени во время профилирования. Код был скомпилирован с -O3. Я не наблюдал различий в коде сборки внутреннего l oop в разных экземплярах функции сравнения.

Я подозреваю, что это поведение связано с предварительным извлечением кеша L1, но я не эксперт в кешировании топи c. Кто-нибудь может объяснить причины такого снижения производительности, и почему оно наблюдается только при посещении строк в прямом порядке, а не в обратном?

...