Nel programma seguente mi aspetto che test1 funzioni più lentamente a causa delle istruzioni dipendenti. Un test con -O2 sembrava confermare questo. Ma poi ho provato con -O3 e ora i tempi sono più o meno uguali. Come può essere?Come mai questo ciclo viene eseguito ugualmente velocemente quando compilato con -O3, ma non quando è compilato con -O2?
#include <iostream>
#include <vector>
#include <cstring>
#include <chrono>
volatile int x = 0; // used for preventing certain optimizations
enum { size = 60 * 1000 * 1000 };
std::vector<unsigned> a(size + x); // `size + x` makes the vector size unknown by compiler
std::vector<unsigned> b(size + x);
void test1()
{
for (auto i = 1u; i != size; ++i)
{
a[i] = a[i] + a[i-1]; // data dependency hinders pipelining(?)
}
}
void test2()
{
for (auto i = 0u; i != size; ++i)
{
a[i] = a[i] + b[i]; // no data dependencies
}
}
template<typename F>
int64_t benchmark(F&& f)
{
auto start_time = std::chrono::high_resolution_clock::now();
f();
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start_time);
return elapsed_ms.count();
}
int main(int argc, char**)
{
// make sure the optimizer cannot make any assumptions
// about the contents of the vectors:
for (auto& el : a) el = x;
for (auto& el : b) el = x;
test1(); // warmup
std::cout << "test1: " << benchmark(&test1) << '\n';
test2(); // warmup
std::cout << "\ntest2: " << benchmark(&test2) << '\n';
return a[x] * x; // prevent optimization and exit with code 0
}
ottengo questi risultati:
g++-4.8 -std=c++11 -O2 main.cpp && ./a.out
test1: 115
test2: 48
g++-4.8 -std=c++11 -O3 main.cpp && ./a.out
test1: 29
test2: 38
Vedo come l'allocazione del registro potrebbe migliorare la situazione riducendo gli accessi alla memoria. Ma sembra esserci ancora una dipendenza: l'aggiornamento del registro deve precedere la scrittura in memoria. È davvero meglio "pipelinabile"? – StackedCrooked
@StackedCrooked Sì ma la scrittura può essere riordinata dopo il caricamento nella successiva iterazione (x86 può spostare i negozi dopo i carichi). Quindi penso che qui la CPU caricherà solo una linea di cache, eseguirà i calcoli, quindi aggiornerà quella linea di cache.Anche se il ciclo è pipeline, ci sono così poche istruzioni che il recupero dei dati è il collo di bottiglia in ogni caso, quindi il secondo ciclo sarà più lento. – sbabbi