ARM Exception Handling GCC 4 va GCC 8



  • Hallo zusammen,
    ich arbeite an einem ARM embedded projekt mit C++11.
    Da man bei embedded Projekte ja immer mit dem Speicher etwas vorsichtig sein muss wollte ich mir mal den Speicherbedarf einer Exception anschauen.
    Anbei findet ihr einen kleinen Beispielcode

    https://godbolt.org/z/pbUFzV

    Wenn man den gleichen Code nun mit ARM gcc 4.5.4 (linux) und ARM gcc 8.2 kompiliert sieht man das der Assembler Output von 150 Zeilen (gcc 4.5.4) auf 77 Zeilen schrumpft (gcc 8.2).

    Kann mir jemand sagen was sich hier geändert hat? Meine Assembler Kenntnisse sind etwas beschränkt. Meinem Kenntnissstand läuft das Exception Handling folgendermaßen ab:

    • Allokieren des Exeption Objekts auf dem Heap oder in einem statischen Speicher (__cxa_allocate_exception)

    • Zurückdrehen des Stack Pointers bis wir auf das erste try/catch stoßen

    • Nach Abhandlung des try/catch. Freigeben des Exception Objekts

    An dieser Grundlegenden Sache hat sich doch eigentlich nie was geändert? Wie können die Assembler Zeilen dann so arg weniger werden?



  • Mit -O3 sind es beim 8.2 74, beim 4.5 86 Zeilen.



  • Die Zeilen 29 - 76 beim "ARM gcc 4.5.4 (linux)"-Output stellen die Implementierung der beiden Kopierkonstruktoren für runtime_error::runtime_error(std::runtime_error const&) und std::exception::exception(std::exception const&) dar, welche beim anderen Build nicht explizit erzeugt werden (obwohl beide diese in Zeile 124, bzw. beim gcc 8.2 in Zeile 59, benutzen) - evtl. wird dieser dann beim gcc 8.2 dazugelinkt.



  • Besser: mit -O2 bauen und die lib.f Option anhakerln (entfernt Code für Funktionen von externen Libraries). Da ist zwar auch GCC 8 besser (weniger Code), aber der Unterschied ist nicht gross. Und dürfte einfach daran liegen dass GCC 8 generell besser optimiert. Wäre ja auch traurig wenn sich zwischen 4 und 8 nix getan hätte.



  • alles klar vielen Dank für euere schnellen Antworten.
    Ich versuche gerade zu prüfen ob es sich lohnt exceptions zu werfen oder nicht. (Wir verwenden zwar bereits eine dynamische allokierung, aber mir ging es in einem ersten Schritt um den zusätzlich generieren Code pro Exception, also der Teil vom Programm, der im Flash landet)



  • @J-heni

    mir ging es in einem ersten Schritt um den zusätzlich generieren Code pro Exception, also der Teil vom Programm, der im Flash landet

    Tipp: verwende mit "noinline" markierte Hilfsfunktionen um die Exceptions zu werfen.
    Also

    BOOST_NOINLINE void throwInvalidArgumentException(char const* detail) {
        throw InvalidArgumentException(detail);
    }
    
    void foo(int x, int y) {
        if (x < y)
            throwInvalidArgumentException("x < y");
    }
    


  • hey,
    was ist denn so schlecht am Inlinen von 1 Zeilern?

    BOOST_NOINLINE void throwInvalidArgumentException(char const* detail) {
        throw InvalidArgumentException(detail);
    }
    

    Ich dachte es wäre immer gut 1 Zeiler zu inleinen. (Die eine Zeile zusätzlich macht doch auch keinen Unterschied?)
    Oder verhindere ich so, dass, dass ganze Exception Handling inlined wird?



  • @J-heni sagte in ARM Exception Handling GCC 4 va GCC 8:

    Ich dachte es wäre immer gut 1 Zeiler zu inleinen.

    Aha. Und wie machst Du das?



  • @J-heni
    Wie fast immer beim Programmieren (und im Leben): es kommt drauf an 🙂

    Z.B. darauf wie der Konstruktor der Exception aussieht. Im besten Fall tut die eine Zeile nicht sehr weh. Aber angenommen die Exception nimmt einen std::string Parameter...
    Nun, std::string ist die Instanzierung eins Templates (std::basic_string<char>), und normalerweise auch "inline" Implementiert. Und üblicherweise auch ohne "Inline-Blocker". Dann kann es sein dass der Compiler meint es wäre furchtbar schlau auch noch den Konstruktor von std::string zu inlinen. Der muss dann erstmal noch die Länge des Strings bestimmen weil ja der const char* Konstruktor verwendet wird. Nehmen wir weiters an die Funktion mit der die Länge bestimmt wird (z.B. strlen) liegt auch inline vor, dann kann auch diese noch inlined werden. Im Endeffekt können da schnell ein paar hundert Byte Code draus werden.

    Mit einer "noinline" Hilfsfunktion zum Werfen der Exception verhinderst du diesen Wildwuchs.

    Aber selbst wenn die Exception trivial zu konstruieren ist, ist die Befehlssequenz die für throw generiert wird länger. Sieh selbst:

    https://godbolt.org/z/JDpLjJ

    Das Werfen der Exception ist hier

      mov edi, 8
      call __cxa_allocate_exception
      xor edx, edx
      mov esi, OFFSET FLAT:typeinfo for SimpleException
      mov QWORD PTR [rax], OFFSET FLAT:.LC0
      mov rdi, rax
      call __cxa_throw
    

    Und das Aufrufen der Hilfsfunktion ist

      mov edi, OFFSET FLAT:.LC2
      call throwSimpleException(char const*)
    

    Und BTW das Werfen eines std::runtime_error (nicht in dem verlinkten Beispiel) ist noch mehr Code, vermutlich da dieser einen Desturktor hat und daher noch zerstört werden muss:

      mov edi, 16
      call __cxa_allocate_exception
      mov esi, OFFSET FLAT:.LC4
      mov rdi, rax
      mov rbp, rax
      call std::runtime_error::runtime_error(char const*)
      mov edx, OFFSET FLAT:_ZNSt13runtime_errorD1Ev
      mov esi, OFFSET FLAT:_ZTISt13runtime_error
      mov rdi, rbp
      call __cxa_throw
      mov r12, rax
      mov rdi, rbp
      call __cxa_free_exception
      mov rdi, r12
      call _Unwind_Resume
    


  • ps

    @J-heni sagte in ARM Exception Handling GCC 4 va GCC 8:

    Oder verhindere ich so, dass, dass ganze Exception Handling inlined wird?

    Exception Handling passiert dort wo die Exception gefangen wird, nicht wo sie geworfen wird. Da kann man auch Hilfsfunktionen einsetzen, aber das ist wieder ein anderes Thema.
    Üblicherweise hat man aber viel mehr Stellen wo man Exceptions wirft als wo man welche fängt, daher ist das meist nicht so wichtig.



  • d.h. es wird nur dort "Zusatzcode" generiert wo die Exceptions aufgefangen werden?


Log in to reply