Estou tentando aprender algumas otimizações em C++ e tentei usar __mm_prefetch
para somar um array. Os testes de benchmark para o meu código são:
#include <benchmark/benchmark.h>
#include <vector>
#if defined(__GNUC__) || defined(__clang__)
#define PREFETCH(addr, hint) __builtin_prefetch(addr, 0, hint)
#elif defined(_MSC_VER)
#include <xmmintrin.h>
#define PREFETCH(addr, hint) _mm_prefetch(reinterpret_cast<const char*>(addr), hint)
#else
#define PREFETCH(addr, hint)
#endif
class PrefetchBenchmark : public benchmark::Fixture {
public:
static constexpr size_t data_size = 1 << 20;
void SetUp(const benchmark::State& state) override {
data.resize(data_size, 1);
}
void TearDown(const benchmark::State& state) override {
}
std::vector<int> data;
};
BENCHMARK_F(PrefetchBenchmark, NoPrefetch)(benchmark::State& state) {
for (auto _ : state) {
long sum = 0;
for (const auto& i : data) {
sum += i;
}
benchmark::DoNotOptimize(sum);
}
}
BENCHMARK_F(PrefetchBenchmark, WithPrefetch)(benchmark::State& state) {
int prefetch_distance = 10;
for (auto _ : state) {
long sum = 0;
for (int i = 0; i < data.size(); i++) {
if (i + prefetch_distance < data.size()) {
PREFETCH(&data[i + prefetch_distance], 3);
}
sum += data[i];
}
benchmark::DoNotOptimize(sum);
}
}
No entanto, o benchmark é executado consistentemente lento com a pré-busca
PrefetchBenchmark/NoPrefetch 348484 ns 344905 ns 1948
PrefetchBenchmark/WithPrefetch 595119 ns 585938 ns 1120
Por que isso acontece e como eu poderia fazer um teste que aumentasse o desempenho usando __mm_prefetch
?
Meu repositório git para meus benchmarks para um exemplo completo está aqui
Primeiro, seu código está introduzindo uma ramificação desnecessária, o que muito provavelmente torna as coisas mais lentas e pode ser evitado:
Agora, analisando o código sem ramificação: a causa raiz do problema parece ser a incapacidade do compilador de vetorizar corretamente o loop com instruções SIMD quando
__builtin_prefetch()
usado em seu corpo. ONoPrefetch
código é vetorizado, mas não é pré-buscado explicitamente. OWithPrefetch
código é pré-perfeito explicitamente, mas não vetorizado. A lentidão causada pela vetorização perdida é muito mais severa do que a aceleração causada pela pré-busca explícita.Um relatório de bug interessante do GCC esclarece o problema: Bug 114061 - GCC falha na vetorização ao usar __builtin_prefetch . Pelo menos para o GCC, parece que o compilador assume que você
__builtin_prefetch(&data[i + x])
sobrecarrega a memória e faz uma chamada de função (faz sentido evitar a vetorização nesse caso), mesmo que a chamada seja para uma função interna que atua como uma operação não operacional.O GCC 15 deveria ter uma correção para superar essa limitação e permitir o built-in sem interromper a vetorização. No entanto, pelo que vejo no Godbolt.org , embora o trunk do GCC 16 vetorize o loop, ele ignora completamente a pré-busca, deixando-a completamente fora do loop. Portanto, ainda me parece quebrado.
Então, como você pode "consertar" isso, se necessário? Bem, infelizmente você não pode confiar no compilador para vetorizar corretamente com sua pré-busca explícita. Além disso, mesmo que um compilador específico seja inteligente o suficiente para lidar com isso (parece que nenhum dos principais é), parece improvável que todos os compiladores acertem em uma compilação multiplataforma. Isso significa que, como você já notou, você terá que aplicar a vetorização manualmente e inserir uma pré-busca conforme necessário.
Sim, a pré-busca de software pode ser benéfica, mas, em geral, é uma tarefa de otimização difícil, com muita tentativa e erro empíricos, principalmente porque a pré-busca de hardware já é muito boa hoje em dia. Veja a resposta em "Posso ler um sinalizador de CPU x86 para determinar se os dados pré-buscados chegaram ao cache L1?" . Se a pré-busca de software for muito cedo (longa distância), os dados serão removidos no momento em que você quiser usá-los. Se a pré-busca de software for muito tarde (pequena distância), a operação se torna inoperante, pois a pré-busca de hardware já ocorreu.
No código de exemplo abaixo , com
prefetch_disance
definido como um valor pequeno como8
, o desempenho diminui em vez de aumentar porque você está pré-buscando dados que já estavam no cache:Isso faz sentido, visto que uma linha de cache já cobre 64/4 = 16
int
valores contíguos (assumindo 32 bitsint
) e a pré-busca de hardware provavelmente já está carregando mais de uma linha de cache à frente. CPUs modernas [x86] já são muito boas em pré-busca de hardware para padrões simples de acesso à memória, como acesso sequencial.Aumentando
prefetch_disance
para um valor maior, como64
ou128
você pode começar a ver uma melhoria (já que estou no x86 Skylake):Código de exemplo: