Ao tentar medir o impacto da previsão de erros de ramificação, percebi que não há nenhuma penalidade na previsão de erros de ramificação.
Com base na famosa pergunta de estouro de pilha: Por que processar um array ordenado é mais rápido do que processar um array não ordenado?
Escrevi um trecho simples de código para medir a penalidade da previsão de ramificação.
- Preencha uma matriz com números aleatórios
- Conte os números acima de 5 (deve haver muitas previsões erradas) - meça-os
- Classificar a matriz
- Conte os números acima de 5 (deve haver poucas previsões erradas) - meça-os
Depois de executar o código, obtive praticamente os mesmos resultados para ambas as medições.
Testado em:
- Visual Studio 2017, lançamento (Otimização Máxima (Favorecer Velocidade) (/O2)), Windows.
- Linux, g++ -Ofast
Então, peguei o código original da pergunta que linkei acima e ainda não obtive nenhuma melhoria para o array ordenado. Por quê? Qual é a vantagem da previsão de ramificação?
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>
int main()
{
// Step 1: Allocate a vector of size 1 million
std::vector<int> vec(100'000'000);
// Step 2: Fill it with random numbers between 0 and 10
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 10);
for (auto& val : vec)
{
val = dis(gen);
}
// Step 3: Count numbers above 5 (and measure time)
auto start = std::chrono::high_resolution_clock::now();
int count_above_5 = 0;
for (size_t i = 0; i < vec.size(); i++)
{
if (vec[i] < 5)
{
++count_above_5;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration_before_sort = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
std::cout << "Count of numbers above 5 (before sorting): " << count_above_5 << std::endl;
std::cout << "Time taken (before sorting): " << duration_before_sort << " ns" << std::endl;
// Step 4: Sort the array
std::sort(vec.begin(), vec.end());
// Step 5: Count numbers above 5 in the sorted array (and measure time)
start = std::chrono::high_resolution_clock::now();
count_above_5 = 0;
for (size_t i = 0; i < vec.size(); i++)
{
if (vec[i] < 5)
{
++count_above_5;
}
}
end = std::chrono::high_resolution_clock::now();
auto duration_after_sort = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
std::cout << "Count of numbers above 5 (after sorting): " << count_above_5 << std::endl;
std::cout << "Time taken (after sorting): " << duration_after_sort << " ns" << std::endl;
return 0;
}
TL;DR: GCC, MSVC e Clang geram um código de montagem sem ramificação, portanto não há ramificação real e, portanto, não há impacto de previsão de (falha) de ramificação.
No Linux, com o GCC 14.2.0, na minha máquina, não há impacto devido à previsão de ramificação, pois, na verdade, não há ramificações . De fato, o GCC gera um código assembly sem ramificação com instruções SIMD:
No Godbolt , podemos ver que tanto o MSVC quanto o Clang também geram código sem ramificação. Aqui está o código produzido pelo MSVC (não usa instruções SIMD, mas
cmovge
sim, que devem ser menos eficientes):Aqui está o código produzido pelo Clang (usa a instrução SIMD e desenrola o loop 4 vezes):