Una soluzione completa e infallibile alla pianificazione dei thread, che dovrebbe produrre esattamente gli stessi tempi per ogni test, consiste nel compilare il programma in modo che sia indipendente dal sistema operativo e avviare il computer in modo da eseguire il programma in un ambiente privo di sistema operativo. Tuttavia, questo è in gran parte impraticabile e sarebbe difficile nella migliore delle ipotesi.
Un buon sostituto per liberarsi del sistema operativo è semplicemente impostare l'affinità del thread corrente su 1 core e la priorità sulla massima. Questa alternativa dovrebbe fornire risultati sufficientemente coerenti.
Inoltre dovresti disattivare le ottimizzazioni che interferirebbero con il debug, che per g ++ o gcc significa aggiungere -Ogalla riga di comando , per evitare che il codice in fase di test venga ottimizzato. Il -O0flag non dovrebbe essere usato perché introduce un overhead extra non necessario che verrebbe incluso nei risultati di temporizzazione, distorcendo così la velocità temporizzata del codice.
Al contrario, sia assumendo che si utilizzi -Ofast(o, come minimo, -O3) sulla build di produzione finale sia ignorando il problema dell'eliminazione "morta" del codice, vengono -Ogeseguite pochissime ottimizzazioni rispetto a -Ofast; quindi -Ogpuò travisare la velocità reale del codice nel prodotto finale.
Inoltre, tutti i test di velocità (in una certa misura) falsano: nel prodotto di produzione finale compilato -Ofast, ogni frammento / sezione / funzione di codice non è isolato; piuttosto, ogni frammento di codice scorre continuamente nel successivo, consentendo così al compilatore di unire, unire e ottimizzare insieme pezzi di codice da ogni parte.
Allo stesso tempo, se stai valutando uno snippet di codice che fa un uso pesante di realloc() intensivo, lo snippet di codice potrebbe essere eseguito più lentamente in un prodotto di produzione con una frammentazione della memoria sufficientemente elevata. Quindi, l'espressione "l'intero è più della somma delle sue parti" si applica a questa situazione perché il codice nella build di produzione finale potrebbe essere notevolmente più veloce o più lento del singolo frammento che stai testando.
Una soluzione parziale che può ridurre l'incongruenza è quella di utilizzare -Ofastper il test di velocità CON l'aggiunta di asm volatile("" :: "r"(var))alle variabili coinvolte nel test per evitare l'eliminazione di codice morto / loop.
Ecco un esempio di come confrontare le funzioni della radice quadrata su un computer Windows.
// set USE_ASM_TO_PREVENT_ELIMINATION to 0 to prevent `asm volatile("" :: "r"(var))`
// set USE_ASM_TO_PREVENT_ELIMINATION to 1 to enforce `asm volatile("" :: "r"(var))`
#define USE_ASM_TO_PREVENT_ELIMINATION 1
#include <iostream>
#include <iomanip>
#include <cstdio>
#include <chrono>
#include <cmath>
#include <windows.h>
#include <intrin.h>
#pragma intrinsic(__rdtsc)
#include <cstdint>
class Timer {
public:
Timer() : beg_(clock_::now()) {}
void reset() { beg_ = clock_::now(); }
double elapsed() const {
return std::chrono::duration_cast<second_>
(clock_::now() - beg_).count(); }
private:
typedef std::chrono::high_resolution_clock clock_;
typedef std::chrono::duration<double, std::ratio<1> > second_;
std::chrono::time_point<clock_> beg_;
};
unsigned int guess_sqrt32(register unsigned int n) {
register unsigned int g = 0x8000;
if(g*g > n) {
g ^= 0x8000;
}
g |= 0x4000;
if(g*g > n) {
g ^= 0x4000;
}
g |= 0x2000;
if(g*g > n) {
g ^= 0x2000;
}
g |= 0x1000;
if(g*g > n) {
g ^= 0x1000;
}
g |= 0x0800;
if(g*g > n) {
g ^= 0x0800;
}
g |= 0x0400;
if(g*g > n) {
g ^= 0x0400;
}
g |= 0x0200;
if(g*g > n) {
g ^= 0x0200;
}
g |= 0x0100;
if(g*g > n) {
g ^= 0x0100;
}
g |= 0x0080;
if(g*g > n) {
g ^= 0x0080;
}
g |= 0x0040;
if(g*g > n) {
g ^= 0x0040;
}
g |= 0x0020;
if(g*g > n) {
g ^= 0x0020;
}
g |= 0x0010;
if(g*g > n) {
g ^= 0x0010;
}
g |= 0x0008;
if(g*g > n) {
g ^= 0x0008;
}
g |= 0x0004;
if(g*g > n) {
g ^= 0x0004;
}
g |= 0x0002;
if(g*g > n) {
g ^= 0x0002;
}
g |= 0x0001;
if(g*g > n) {
g ^= 0x0001;
}
return g;
}
unsigned int empty_function( unsigned int _input ) {
return _input;
}
unsigned long long empty_ticks=0;
double empty_seconds=0;
Timer my_time;
template<unsigned int benchmark_repetitions>
void benchmark( char* function_name, auto (*function_to_do)( auto ) ) {
register unsigned int i=benchmark_repetitions;
register unsigned long long start=0;
my_time.reset();
start=__rdtsc();
while ( i-- ) {
auto result = (*function_to_do)( i << 7 );
#if USE_ASM_TO_PREVENT_ELIMINATION == 1
asm volatile("" :: "r"(
// There is no data type in C++ that is smaller than a char, so it will
// not throw a segmentation fault error to reinterpret any arbitrary
// data type as a char. Although, the compiler might not like it.
result
));
#endif
}
if ( function_name == nullptr ) {
empty_ticks = (__rdtsc()-start);
empty_seconds = my_time.elapsed();
std::cout<< "Empty:\n" << empty_ticks
<< " ticks\n" << benchmark_repetitions << " repetitions\n"
<< std::setprecision(15) << empty_seconds
<< " seconds\n\n";
} else {
std::cout<< function_name<<":\n" << (__rdtsc()-start-empty_ticks)
<< " ticks\n" << benchmark_repetitions << " repetitions\n"
<< std::setprecision(15) << (my_time.elapsed()-empty_seconds)
<< " seconds\n\n";
}
}
int main( void ) {
void* Cur_Thread= GetCurrentThread();
void* Cur_Process= GetCurrentProcess();
unsigned long long Current_Affinity;
unsigned long long System_Affinity;
unsigned long long furthest_affinity;
unsigned long long nearest_affinity;
if( ! SetThreadPriority(Cur_Thread,THREAD_PRIORITY_TIME_CRITICAL) ) {
SetThreadPriority( Cur_Thread, THREAD_PRIORITY_HIGHEST );
}
if( ! SetPriorityClass(Cur_Process,REALTIME_PRIORITY_CLASS) ) {
SetPriorityClass( Cur_Process, HIGH_PRIORITY_CLASS );
}
GetProcessAffinityMask( Cur_Process, &Current_Affinity, &System_Affinity );
furthest_affinity = 0x8000000000000000ULL>>__builtin_clzll(Current_Affinity);
nearest_affinity = 0x0000000000000001ULL<<__builtin_ctzll(Current_Affinity);
SetProcessAffinityMask( Cur_Process, furthest_affinity );
SetThreadAffinityMask( Cur_Thread, furthest_affinity );
const int repetitions=524288;
benchmark<repetitions>( nullptr, empty_function );
benchmark<repetitions>( "Standard Square Root", standard_sqrt );
benchmark<repetitions>( "Original Guess Square Root", original_guess_sqrt32 );
benchmark<repetitions>( "New Guess Square Root", new_guess_sqrt32 );
SetThreadPriority( Cur_Thread, THREAD_PRIORITY_IDLE );
SetPriorityClass( Cur_Process, IDLE_PRIORITY_CLASS );
SetProcessAffinityMask( Cur_Process, nearest_affinity );
SetThreadAffinityMask( Cur_Thread, nearest_affinity );
for (;;) { getchar(); }
return 0;
}
Inoltre, credito a Mike Jarvis per il suo Timer.
Si noti (questo è molto importante) che se si eseguono frammenti di codice più grandi, è necessario ridurre il numero di iterazioni per evitare che il computer si blocchi.