Hi
Ich weiss nicht, ob dies eine Frage oder eine Aussage werden soll.
Also beginne ich einfach mal mit meinen Beobachtungen:
Ich schreibe an einem Simulator für den RPi 1 B.
Dazu gehört offensichtlicherweise das Lesen und Ausführen von ARM-Instruktionen, was auf x86-Chips relativ aufwendig ist (>100 Bytes an x86-Code PRO ARM-Instruktion (wobei natürlich die MMU-Simulation recht viel dazu beiträgt). Entsprechend komme ich gerade mal auf 0.4 MIPS bei der Simulation auf einem modernen Skylake-Prozessor @ 2.4 GHz, während der ARM1176-jzfs @ 700 MHz im RPi knapp 1000 MIPS schafft.
Um dies zu umgehen (und ein bisschen mehr Leistung rauszukitzeln), habe ich zusätzlich einen Umkompilierer geschrieben, der die Instruktionen im Voraus übersetzt und dadurch merklich (gefühlt 10x) schneller ist als die Instruktion-für-Instruktion-Variante. Nun kann (und will) ich nicht alles Umkompilieren, da ich später auch Einzelschritte durch den Code will machen können. Das führt aber zum Problem, dass Branches (Jumps) im Voraus nicht umkompiliert werden können, denn ein Jump könnte aus einem vorkompilierten Block herausspringen - wo dann die langsamere Emulation wieder einspringen muss. Allerdings führte das zu extremen Leistungseinbussen, da die virtuelle (vereinfachte) Pipeline wieder gefüllt werden musste, die Register wieder angepasst werden mussten (z.B. x86 Status Register musste zum CPSR kopiert werden) usw.
Daher fügte ich Code ein, der die ARM-Adressen auf die x86 Adressen ummappen sollte, sodass bei Branches sehr viel Overhead entfiel.
Ich realisierte das mit einer std::map von ARM-Adresse auf x86-Adresse, und freute mich ob der erhaltenen Performance.
Irgendwann mass ich die Geschwindigkeit, kam auf die 0.4 MIPS und war sehr beschämt, da ich auf 10x langsamer gehofft und 500x langsamer erwartet hatte, die Simulation tatsächlich aber 5000x langsamer war.
Also schmiss ich Callgrind an und erhielt folgendes Bild:

Darin als einziges die Funktion: __gnu_cxx::__normal_iterator.
Der (leicht vereinfachte) Code:
Ich machte daraufhin 2 Optimierungen:
1) Ich cache die Adressen in einem Vektor, da wahrscheinlich dieselbe Adresse oft neu gebraucht wird
2) Ich Cache pro umkompilierten Abschnitt (statt wie bisher global in allen Abschnitten nach der Adresse zu suchen), da ein Codeabschnitt wahrscheinlich nicht alle anderen Bereiche braucht.
Code:
Das Resultat: (TL;DR: Die Auflösung ist komplett von der ersten "Seite" an Kosten verschwunden, einzig das miss-resolve ist noch zu finden, allerdings verschwindend bei längerer Laufzeit und bei weitem nicht mehr so teuer wie zuvor).

Nun zur eigentlichen Frage / Aussage:
Ja, es ist eine Optimierung, und nein, man kann vom Standard nicht wirklich "perfekte" Performance in jedem Fall erwarten. Aber sowas finde ich dann schon echt fragwürdig.
Wofür ist std::map dann noch gut?
1) Es ist langsamer als ein (mehr oder weniger klug angelegter) std::vector.
2) Für grosse Maps sollte man ohnehin optimierte Datenbanken wie sqlite, postgres, mysql und Co. verwenden.
3) const maps können locker als sortierte Arrays implementiert und damit sehr viel günstiger sein.
Also was heisst das nun?
Für mich heisst es: Finger weg von std::map.
Was heisst es für den Leser? Habe ich eine falsche Analyse gemacht, falsche Schlüsse gezogen oder mache ich etwas generell falsch? Kann ich noch mehr optimieren? Verwendet der Leser std::maps und wenn ja, warum?
Gruss
cwriter
Ich weiss nicht, ob dies eine Frage oder eine Aussage werden soll.
Also beginne ich einfach mal mit meinen Beobachtungen:
Ich schreibe an einem Simulator für den RPi 1 B.
Dazu gehört offensichtlicherweise das Lesen und Ausführen von ARM-Instruktionen, was auf x86-Chips relativ aufwendig ist (>100 Bytes an x86-Code PRO ARM-Instruktion (wobei natürlich die MMU-Simulation recht viel dazu beiträgt). Entsprechend komme ich gerade mal auf 0.4 MIPS bei der Simulation auf einem modernen Skylake-Prozessor @ 2.4 GHz, während der ARM1176-jzfs @ 700 MHz im RPi knapp 1000 MIPS schafft.
Um dies zu umgehen (und ein bisschen mehr Leistung rauszukitzeln), habe ich zusätzlich einen Umkompilierer geschrieben, der die Instruktionen im Voraus übersetzt und dadurch merklich (gefühlt 10x) schneller ist als die Instruktion-für-Instruktion-Variante. Nun kann (und will) ich nicht alles Umkompilieren, da ich später auch Einzelschritte durch den Code will machen können. Das führt aber zum Problem, dass Branches (Jumps) im Voraus nicht umkompiliert werden können, denn ein Jump könnte aus einem vorkompilierten Block herausspringen - wo dann die langsamere Emulation wieder einspringen muss. Allerdings führte das zu extremen Leistungseinbussen, da die virtuelle (vereinfachte) Pipeline wieder gefüllt werden musste, die Register wieder angepasst werden mussten (z.B. x86 Status Register musste zum CPSR kopiert werden) usw.
Daher fügte ich Code ein, der die ARM-Adressen auf die x86 Adressen ummappen sollte, sodass bei Branches sehr viel Overhead entfiel.
Ich realisierte das mit einer std::map von ARM-Adresse auf x86-Adresse, und freute mich ob der erhaltenen Performance.
Irgendwann mass ich die Geschwindigkeit, kam auf die 0.4 MIPS und war sehr beschämt, da ich auf 10x langsamer gehofft und 500x langsamer erwartet hatte, die Simulation tatsächlich aber 5000x langsamer war.
Also schmiss ich Callgrind an und erhielt folgendes Bild:

Darin als einziges die Funktion: __gnu_cxx::__normal_iterator.
Der (leicht vereinfachte) Code:
C++:
auto x86addr = ret->getInstMap().find(ARM-addr);
size_t retval = size_t(((uint8_t*)ret->getCodeBaseAddr()) + x86_addr->second);
return retval;
Ich machte daraufhin 2 Optimierungen:
1) Ich cache die Adressen in einem Vektor, da wahrscheinlich dieselbe Adresse oft neu gebraucht wird
2) Ich Cache pro umkompilierten Abschnitt (statt wie bisher global in allen Abschnitten nach der Adresse zu suchen), da ein Codeabschnitt wahrscheinlich nicht alle anderen Bereiche braucht.
Code:
C++:
size_t CodeChunk::cached_resolve(size_t ARM_addr)
{
//This function should be more efficient than the Recompiler::resolve function due to Chunk-local caches (it seems more likely that a function will call the same functions over and over)
if(m_resolve_cache.size() > 0)
{
auto rnd = m_resolve_cache_iterator;
do
{
if(std::get<0>(m_resolve_cache[m_resolve_cache_iterator]) == ARM_addr)
{
return std::get<1>(m_resolve_cache[m_resolve_cache_iterator]);
}
} while(rnd != (m_resolve_cache_iterator = (m_resolve_cache_iterator + 1) % m_resolve_cache.size()));
}
size_t resolv = m_recomp->resolve(ARM_addr);
m_resolve_cache.push_back(std::tuple<size_t, size_t>(ARM_addr, resolv));
return resolv;
}
//...
std::vector<std::tuple<size_t, size_t>> m_resolve_cache;
Das Resultat: (TL;DR: Die Auflösung ist komplett von der ersten "Seite" an Kosten verschwunden, einzig das miss-resolve ist noch zu finden, allerdings verschwindend bei längerer Laufzeit und bei weitem nicht mehr so teuer wie zuvor).

Nun zur eigentlichen Frage / Aussage:
Ja, es ist eine Optimierung, und nein, man kann vom Standard nicht wirklich "perfekte" Performance in jedem Fall erwarten. Aber sowas finde ich dann schon echt fragwürdig.
Wofür ist std::map dann noch gut?
1) Es ist langsamer als ein (mehr oder weniger klug angelegter) std::vector.
2) Für grosse Maps sollte man ohnehin optimierte Datenbanken wie sqlite, postgres, mysql und Co. verwenden.
3) const maps können locker als sortierte Arrays implementiert und damit sehr viel günstiger sein.
Also was heisst das nun?
Für mich heisst es: Finger weg von std::map.
Was heisst es für den Leser? Habe ich eine falsche Analyse gemacht, falsche Schlüsse gezogen oder mache ich etwas generell falsch? Kann ich noch mehr optimieren? Verwendet der Leser std::maps und wenn ja, warum?
Gruss
cwriter