Eine vollständige Lösung für die Thread-Planung, die bei jedem Test genau die gleichen Zeiten liefern sollte, besteht darin, Ihr Programm so zu kompilieren, dass es vom Betriebssystem unabhängig ist, und Ihren Computer hochzufahren, um das Programm in einer betriebssystemfreien Umgebung auszuführen. Dies ist jedoch weitgehend unpraktisch und bestenfalls schwierig.
Ein guter Ersatz für die Betriebssystemfreiheit besteht darin, die Affinität des aktuellen Threads auf 1 Kern und die Priorität auf die höchste zu setzen. Diese Alternative sollte konsistent genug Ergebnisse liefern.
Außerdem sollten Sie Optimierungen deaktivieren, die das Debuggen beeinträchtigen würden, was für g ++ oder gcc das Hinzufügen -Og
zur Befehlszeile bedeutet , um zu verhindern, dass der getestete Code optimiert wird. Das -O0
Flag sollte nicht verwendet werden, da es zusätzlichen unnötigen Overhead verursacht, der in den Timing-Ergebnissen enthalten wäre, wodurch die zeitgesteuerte Geschwindigkeit des Codes verzerrt wird.
Im Gegenteil, sowohl unter der Annahme, dass Sie den endgültigen Produktionsbuild verwenden -Ofast
(oder zumindest verwenden -O3
) als auch das Problem der "toten" Code-Eliminierung ignorieren, werden im -Og
Vergleich zu nur sehr wenige Optimierungen durchgeführt -Ofast
. Dies -Og
kann die tatsächliche Geschwindigkeit des Codes im Endprodukt falsch darstellen.
Darüber hinaus sind alle Geschwindigkeitstests (bis zu einem gewissen Grad) gültig: In dem endgültigen Produktionsprodukt, mit dem kompiliert wurde -Ofast
, ist nicht jedes Snippet / jeder Abschnitt / jede Funktion des Codes isoliert. Vielmehr fließt jedes Codeausschnitt kontinuierlich in das nächste, sodass der Compiler potenzielle Codeteile von überall her zusammenfügen, zusammenführen und optimieren kann.
Wenn Sie ein Code-Snippet vergleichen, das stark genutzt wird realloc()
, wird das Code-Snippet in einem Produktionsprodukt mit ausreichend hoher Speicherfragmentierung möglicherweise langsamer ausgeführt. Daher gilt für diese Situation der Ausdruck "Das Ganze ist mehr als die Summe seiner Teile", da Code im endgültigen Produktionsbuild möglicherweise merklich schneller oder langsamer ausgeführt wird als das einzelne Snippet, das Sie auf Geschwindigkeit testen.
Eine Teillösung, die die Inkongruenz verringern kann, ist die Verwendung -Ofast
für Geschwindigkeitstests mit Hinzufügung asm volatile("" :: "r"(var))
zu den am Test beteiligten Variablen, um die Beseitigung von totem Code / Schleife zu verhindern.
Hier ist ein Beispiel für das Benchmarking von Quadratwurzelfunktionen auf einem Windows-Computer.
// 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;
}
Dank auch an Mike Jarvis für seinen Timer.
Bitte beachten Sie (dies ist sehr wichtig), dass Sie die Anzahl der Iterationen verringern müssen, wenn Sie größere Codefragmente ausführen möchten, damit Ihr Computer nicht einfriert.