So, ich habe meinen Code noch mehr aufgeblasen, dass der nicht nur
FSIN gg. sin() vergleicht, sondern noch FTAN gg. tan() sowie FPREM
gg. remainder(). Unter Windows x64 ist es ja so, dass die FPU durch-
aus noch nutzbar ist, wie man ja an dem bisherigen Vergleich ja sehen
kann. Bei sin() und tan() war mir ja klar, dass das alles in Software
realisiert wird bzw. dass man da keine Umwege über die FPU nimmt,
aber bei remainder() dachte ich mir, dass die vielleicht doch die
x87-FPU nutzen.
Der aufgeblasene Code sieht jetzt so aus:
#define _USE_MATH_DEFINES
#include <random>
#include <cmath>
#include <chrono>
#include <atomic>
#include <chrono>
#include <iostream>
using namespace std;
using namespace chrono;
void deppenSin( double const &value, double &sin_ );
void deppenTan( double const &value, double &tan_ );
void deppenRem( double const &value, double &rem_ );
template<typename TransFn>
concept trans_concept = requires( TransFn transFn, double const &value,
double &ret ) { { transFn( value, ret ) }; };
int main()
{
size_t const
N_VALUES = 1000,
#if defined(NDEBUG)
ROUNDS = 100'000;
#else
ROUNDS = 1'000;
#endif
atomic_int64_t aISum;
vector<double> values( N_VALUES );
auto compareTrans = [&]<trans_concept Native, trans_concept Lib>( char
const *description, Native native, Lib lib )
{
auto transBench = [&]<trans_concept TransFn>( TransFn transFn ) -> double
{
auto start = high_resolution_clock::now();
double lastTrans;
atomic_int64_t aILastValue = 0;
for( size_t r = ROUNDS; r--; )
for( double &v : values )
{
int64_t iValue;
memcpy( &iValue, &v, 8 );
iValue ^= aILastValue.load( memory_order_relaxed );
memcpy( &v, &iValue, 8 );
transFn( v, lastTrans ),
memcpy( &iValue, &lastTrans, 8 );
aILastValue.store( iValue, memory_order_relaxed );
aILastValue.store( 0 );
}
aISum += aILastValue;
return (double)(int64_t)duration_cast<nanoseconds>(
high_resolution_clock::now() - start ).count() / ((double)ROUNDS *
N_VALUES);
};
double
tNative = transBench( native ),
tLib = transBench( lib ),
pct = trunc( tNative / tLib * 1'000.0 + 0.5 ) / 10.0;
cout << description << ": " << tNative << ", " << tLib << ", " << pct
<< "%" << endl;
};
mt19937_64 mt;
{
uniform_real_distribution<double> pmPi( -M_PI, M_PI );
for( double &v : values )
v = pmPi( mt );
}
compareTrans( "FSIN vs sin()", deppenSin, []( double const &value,
double &ret ) { ret = sin( value ); } );
compareTrans( "FTAN vs tan()", deppenTan, []( double const &value,
double &ret ) { ret = tan( value ); } );
{
uniform_real_distribution<double> rndValues( 0.0, ((uint64_t)1 << 53)
- 1 );
for( double &v : values )
v = rndValues( mt );
}
compareTrans( "FPREM vs remainder()", deppenRem, []( double const
&value, double &ret ) { ret = remainder( value, 2.0 ); } );
}
PUBLIC ?deppenSin@@YAXAEBNAEAN@Z
PUBLIC ?deppenTan@@YAXAEBNAEAN@Z
_DATA SEGMENT
?TWO@@3NC DQ 04000000000000000r
_DATA ENDS
_TEXT SEGMENT
?deppenSin@@YAXAEBNAEAN@Z PROC
fld qword ptr [rcx]
fsin
fstp qword ptr [rdx]
ret
?deppenSin@@YAXAEBNAEAN@Z ENDP
?deppenTan@@YAXAEBNAEAN@Z PROC
fld qword ptr [rcx]
fptan
fstp qword ptr [rdx]
ret
?deppenTan@@YAXAEBNAEAN@Z ENDP
?deppenRem@@YAXAEBNAEAN@Z PROC
fld qword ptr ?TWO@@3NC
fld qword ptr [rcx]
fprem
fxch st(1)
fstp st(0)
fstp qword ptr [rdx]
ret
?deppenRem@@YAXAEBNAEAN@Z ENDP
_TEXT ENDS
END
Wenn man das wirklich versteht, dann sieht man auch, was C++20 für
eine gigantisch mächtige Sprache ist. Das generische Lambda ließe
sich auch mit Makros realisieren, aber sowas will keiner der nicht
schlimme Dinge erlebt hat ernsthaft programmieren.
Auf jeden Fall sind das die Ergebnisse:
FSIN vs sin(): 29.2301, 13.4287, 217.7%
FTAN vs tan(): 22.0681, 16.8505, 131%
FPREM vs remainder(): 16.7135, 15.9596, 104.7%
Ich hatte bei meinem ersten Benchmark noch einen kleinen Bug drin wo ich
die Abhängigkeits-Ketten hergesellt haben wollte, aber dann im Endeffekt
keine zustandekamen, dass aus einem Unterschied zwischen FSIN und sin()
von 2,4 dann 3,55 wurde. Ich mein das war ein Fehler, aber ich find das
absolut enorm, dass da wo es dann tatsächlich keine Abhängigkeits-Ketten
gab die Pipe die längeren Instruktions-Sequenzen
in sin() mit unterschiedlichen Operanden so weit instruktions-parallel
ausführt, dass dabei so ein wahnsinns Gewinn zustandekommt.
Bei sin() ist es ja wirklich ausgeschlossen, dass der Code intern
entsprechend FSIN() nutz. Bei tan() und remainder() habe ich es nicht
ausschließen können bzw. dachte, auch wenn ich es für unwahrscheinlich
hielt, dass der Wrapping-Code den Unterschied macht.
Auf jeden Fall habe ich mir dann doch mal ein Mini-Programm geschrieben,
das die Funktionen im Release-Code im Debugger aufrief. In beiden Fällen
war es dann so, dass intern kein FRAN oder FPREM genutzt wurde, sondern
Code mit unzähligen SSE-Operationen.
So Helmut, wie war das nochmal mit 30 mal langsamer ?