NytroX hat geschrieben: ↑29.01.2023, 13:08Also bei mir lief ersteres auf den ersten Blick auch wesentlich schneller:
https://quick-bench.com/q/bsfavhGm-f-L_OnayyjPjtX4zdo
Vielleicht habe ich irgendwo einen Fehler oder suboptimalen Code.
(vielleicht mal drüber schauen, habt ihr noch Optimierungs-Ideen?)
- Deine zweite Variante erzeugt zwei Strings – einmal "(unbekannter Wochentag)" und dann das tatsächliche Ergebnis. Korrigiert man das, ist der Unterschied 3-fach statt 8-fach.
- Die zweite Implementierung erzeugt einmal einen std::vector mit fertigen std::strings drin, während die erste Implementierung jedes Mal einen neues String aus den Literals erzeugt. Das ist ein unfairer Vorteil für die zweite Variante.
- Ich bin mir recht sicher, dass die Konstruktion von std::string samt Speicherallokation das Benchmark dominiert. Aber das ist wahrscheinlich gar nicht so schlimm, weil man es in den meisten echten Anwendungsfällen genauso machen würde.
- Zuletzt und am wichtigsten: Du testest nicht zufällige Zugriffe, sondern immer das selbe Muster – damit gibt es im ganzen Benchmark nur acht Branches in immer gleicher Reihenfolge, und die Branch Prediction der CPU freut sich natürlich :D
Danach ist die zweite Variante nur noch minimal langsamer:
Code: Alles auswählen
static std::string wochentagsname1(const int nummer)
{
switch (nummer)
{
case 1: return "Montag";
case 2: return "Dienstag";
case 3: return "Mittwoch";
case 4: return "Donnerstag";
case 5: return "Freitag";
case 6: return "Samstag";
case 7: return "Sonntag";
}
return "(unbekannter Wochentag)";
}
static std::string wochentagsname2(const int nummer)
{
static char const * const tage[]
{
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag",
"Sonntag"
};
if ((nummer >= 1) && (nummer <= std::size(tage)))
{
return tage[nummer - 1];
}
return "(unbekannter Wochentag)";
}
static void Wochentage1(benchmark::State& state) {
for (auto _ : state) {
std::string wo_lol = wochentagsname1(1 + rand() % 8);
benchmark::DoNotOptimize(wo_lol);
}
}
BENCHMARK(Wochentage1);
static void Wochentage2(benchmark::State& state) {
for (auto _ : state) {
std::string wo_lol = wochentagsname2(1 + rand() % 8);
benchmark::DoNotOptimize(wo_lol);
}
}
BENCHMARK(Wochentage2);
Das überrascht mich, aber kurzer Blick ins Disassembly zeigt, dass GCC acht verschiedene Konstruktoren von
std::string erzeugt hat, die jeweils die Länge und Adresse des Literals hard-coded haben. Ist natürlich eine riesen Inflation des Binaries und nicht so dolle, wenn die Caches kalt sind, aber da muss ich durchaus anerkennen, dass es schneller ist.
Der Blick zeigt allerdings auch, dass die erste Variante nur deshalb schneller ist, weil GCC sie automatisch in die zweite umgewandelt hat! Ich schaue mal kurz, ob ich das in C++ ausgedrückt kriege.
Edit: Hier – das baut GCC aus dem
switch:
Code: Alles auswählen
static std::string makeMO() {
return "Montag";
}
static std::string makeDI() {
return "Dienstag";
}
static std::string makeMI() {
return "Mittwoch";
}
static std::string makeDO() {
return "Donnerstag";
}
static std::string makeFR() {
return "Freitag";
}
static std::string makeSA() {
return "Samstag";
}
static std::string makeSO() {
return "Sonntag";
}
static std::string makeUnknown() {
return "Sonntag";
}
static std::string wochentagsname2(const int nummer)
{
using TagConstructor = std::string ();
static constexpr TagConstructor * tage[] = {
makeMO, makeDI, makeMI, makeDO, makeFR, makeSA, makeSO, makeUnknown
};
if ((nummer >= 1) && (nummer <= 7))
{
return tage[nummer - 1]();
}
return tage[7]();
}
Im Benchmark ist das sogar minimal schneller. Sehr interessant – die Optimierung kannte ich von noch nicht. Toll, dass das mittlerweile geht.
switch ist nur schnell, weil es intern auf die Tabellenversion umgestellt wird. Wenn das heutzutage möglich ist, kann man natürlich ohne Performance-Bedenken die
switch-Variante bevorzugen.