Para diferença de desempenho do loop e otimização do compilador

Eu escolhi a resposta de David porque ele foi o único a apresentar uma solução para a diferença nos for-loops sem sinalizadores de otimização. As outras respostas demonstram o que acontece ao ativar os sinalizadores de otimização.

A resposta de Jerry Coffin explicou o que acontece ao definir os sinalizadores de otimização para este exemplo. O que permanece sem resposta é por que o superCalculationA é executado mais lentamente que o superCalculationB, quando B executa uma referência de memória extra e uma adição para cada iteração. O post de Nemo mostra a saída do assembler. Eu confirmei essa compilação com o-S flag no meu PC, Sandy Bridge a 2,9 GHz (i5-2310), executando o Ubuntu 12.04 de 64 bits, conforme sugerido por Matteo Italia.

Eu estava experimentando o desempenho de for-loops quando me deparei com o seguinte caso.

Eu tenho o seguinte código que faz o mesmo cálculo de duas maneiras diferentes.

#include <cstdint>
#include <chrono>
#include <cstdio>

using std::uint64_t;

uint64_t superCalculationA(int init, int end)
{
    uint64_t total = 0;
    for (int i = init; i < end; i++)
        total += i;
    return total;
}

uint64_t superCalculationB(int init, int todo)
{
    uint64_t total = 0;
    for (int i = init; i < init + todo; i++)
        total += i;
    return total;
}

int main()
{
    const uint64_t answer = 500000110500000000;

    std::chrono::time_point<std::chrono::high_resolution_clock> start, end;
    double elapsed;

    std::printf("=====================================================\n");

    start = std::chrono::high_resolution_clock::now();
    uint64_t ret1 = superCalculationA(111, 1000000111);
    end = std::chrono::high_resolution_clock::now();
    elapsed = (end - start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    start = std::chrono::high_resolution_clock::now();
    uint64_t ret2 = superCalculationB(111, 1000000000);
    end = std::chrono::high_resolution_clock::now();
    elapsed = (end - start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    if (ret1 == answer)
    {
        std::printf("The first method, i.e. superCalculationA, succeeded.\n");
    }
    if (ret2 == answer)
    {
        std::printf("The second method, i.e. superCalculationB, succeeded.\n");
    }

    std::printf("=====================================================\n");

    return 0;
}

Compilando este código com

g ++ main.cpp -o output --std = c ++ 11

leva ao seguinte resultado:

=====================================================
Elapsed time: 2.859 s | 2859.441 ms | 2859440.968 us
Elapsed time: 2.204 s | 2204.059 ms | 2204059.262 us
The first method, i.e. superCalculationA, succeeded.
The second method, i.e. superCalculationB, succeeded.
=====================================================

Minha primeira pergunta é:por que o segundo loop está executando 23% mais rápido que o primeiro?

Por outro lado, se eu compilar o código com

g ++ main.cpp -o output --std = c ++ 11 -O1

Os resultados melhoram muito,

=====================================================
Elapsed time: 0.318 s | 317.773 ms | 317773.142 us
Elapsed time: 0.314 s | 314.429 ms | 314429.393 us
The first method, i.e. superCalculationA, succeeded.
The second method, i.e. superCalculationB, succeeded.
=====================================================

e a diferença no tempo quase desaparece.

Mas eu não podia acreditar nos meus olhos quando defini a bandeira -O2,

g ++ main.cpp -o output --std = c ++ 11 -O2

e entendi:

=====================================================
Elapsed time: 0.000 s | 0.000 ms | 0.328 us
Elapsed time: 0.000 s | 0.000 ms | 0.208 us
The first method, i.e. superCalculationA, succeeded.
The second method, i.e. superCalculationB, succeeded.
=====================================================

Então, minha segunda pergunta é:O que o compilador está fazendo quando eu defino sinalizadores -O1 e -O2 que levam a esse gigantesco aprimoramento de desempenho?

eu chequeiOpção otimizada - Usando a coleção GNU Compiler Collection (GCC), mas isso não esclareceu as coisas.

A propósito, estou compilando este código com o g ++ (GCC) 4.9.1.

EDIT para confirmar a suposição de Basile Starynkevitch

Eu editei o código, agoramain se parece com isso:

int main(int argc, char **argv)
{
    int start = atoi(argv[1]);
    int end   = atoi(argv[2]);
    int delta = end - start + 1;

    std::chrono::time_point<std::chrono::high_resolution_clock> t_start, t_end;
    double elapsed;

    std::printf("=====================================================\n");

    t_start = std::chrono::high_resolution_clock::now();
    uint64_t ret1 = superCalculationB(start, delta);
    t_end = std::chrono::high_resolution_clock::now();
    elapsed = (t_end - t_start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    t_start = std::chrono::high_resolution_clock::now();
    uint64_t ret2 = superCalculationA(start, end);
    t_end = std::chrono::high_resolution_clock::now();
    elapsed = (t_end - t_start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    std::printf("Results were %s\n", (ret1 == ret2) ? "the same!" : "different!");
    std::printf("=====================================================\n");

    return 0;
}

Essas modificações realmente aumentaram o tempo de computação, tanto para-O1 e-O2. Ambos estão me dando cerca de 620 ms agora.O que prova que -O2 estava realmente fazendo algum cálculo em tempo de compilação.

Ainda não entendo o que essas bandeiras estão fazendo para melhorar o desempenho e-Ofast faz ainda melhor, a cerca de 320ms.

Observe também que mudei a ordem na qual as funções A e B são chamadas para testar a suposição de Jerry Coffin. Compilar esse código sem sinalizadores otimizadores ainda me dá cerca de 2,2 segundos em B e 2,8 segundos em A. Então, acho que não é uma coisa de cache. Apenas reforçando que eu sounão falando sobre otimização no primeiro caso (aquele sem sinalizadores), eu só quero saber o que faz com que o loop dos segundos corra mais rápido que o primeiro.

questionAnswers(7)

yourAnswerToTheQuestion