String-Klasse als Vorbild gesucht



  • Optimizer schrieb:

    Ja, try-catch sorgt für Overhead. Hat aber keiner gesagt, dass die Exception gefangen werden muss.

    Ja dann hat es doch aber auch keinen Sinn, dass ich sie werfe? Was passiert denn, wenn eine Exception geworfen wird und niemand fängt sie?



  • Das Programm kackt ab?



  • Nein. Das Programm wird *korrekt* beendet, mit allen Dtors...das ist IMHO schon ein Unterschied 😉

    Außerdem verwendet man natürlich manchmal try/catch, aber eben nicht oft und gerne ein paar Ebenen höher...



  • operator void schrieb:

    Nein. Das Programm wird *korrekt* beendet, mit allen Dtors...das ist IMHO schon ein Unterschied 😉

    Tatsache. Das sollte man nicht unerwähnt lassen. 🙂



  • operator void schrieb:

    Nein. Das Programm wird *korrekt* beendet, mit allen Dtors...das ist IMHO schon ein Unterschied 😉

    sind wir uns da ganz sicher, daß das programm korrekt beendet wird, wenn keiner die excpetion fängt?



  • Uh-oh, jetzt wo dus sagst...ja, der Standard ist da glaube ich etwas toleranter. Na dann braucht man immerhin ein einziges try/catch im main(), das den Fehler ausgibt (sowieso eine gute Idee). Dass das die Performance runterzieht, bezweifle ich einfach mal.



  • das war doch der sinn der exceptions das man die etliche ebene weiter abfangen kann und eine fehlermedlung oder was auch immer ausgibt. normalerweise steht das dann in der exception klasse welcher und wo der fehler ausgelöst wurde



  • Imho sorgt catch schon für Overhead. Und wie willst du denn ohne catch Exceptions fangen? Und was passiert, wenn ich kein try verwende?

    Ja, try-catch sorgt für Overhead. Hat aber keiner gesagt, dass die Exception gefangen werden muss.

    Ja bei den meisten Compilern produciert try/catch Overheat selbst wenn keine Exception geworfen wird. Wobei ich eigentlich nicht wirklich verstehe wieso man das nicht ohne Overheat hinbekommt.

    Die Dtoren in der Funktion in der throw aufgerufen wird aufzurufen und die catch Blocks zu durchsuchen ist ja wohl kein Problem. Wenn einmal der Stack von localen Daten geputzt ist bleibt immer noch die Returnaddress die man ja nicht nur zum Returnen benutzen kann. Per lookup Table kann man ja herausfinden von wo die Funktion aufgerufte worden ist also welche Dtoren und catch Blöcke gecheckt werden müssen. Wenn das fertig ist hat man wieder eine Return address... Könnte den Code ein wenig aufblähen aber sonst sehe ich eigentlich keine Probleme.

    Nein. Das Programm wird *korrekt* beendet, mit allen Dtors...das ist IMHO schon ein Unterschied

    Die Dtoren globaler Objekte ausgenommen. Wenn eine Exception aus main rausfliegt dann wird der "terminate" Handler aufgerufen. Den kann man mit set_terminate setzen. Bei default ruft der C's abort Funktion auf.

    Also man merke main sollte immer so aussehen:

    int main(){
      try{
        //...
      }catch(exception&except){
        cerr<<"Stopped on exception throw : "<<except.what()<<endl;
        return 1;
      }catch(...){
        cerr<<"Stopped on unknown exception throw!"<<endl;
        return 1;
      }
      return 0;
    }
    

    Dann ist man sicher, dass die Fehlermeldung ausgegeben wird und, dass alle Dtorn aufgerufen werden, da keine Exception aus main rausfliegen kann.



  • Es heißt Overhead. 😉
    Der kommt daher, dass das Programm jedesmal, wenn es in einen try-Block eintritt, das registrieren muss. I.d.R. haben die Programme einen Stack mit try-Eintritten, so dass immer der letzte die Exception fängt, oder eben der eins davor, wenn es kein passendes catch gibt...
    Dazu müssen jedesmal alle lokalen Destruktoren bekannt sein, das gibt es alles nicht umsonst.

    In Java wird das Problem über finally gelöst, bzw. kümmert sich um den Speicher schon mal die Garbage Collection. Ob die VM auch die try-Eintritte einzeln registriert, würd mich auch mal interessieren.



  • Wie ich seh hast du entweder meinen Post nicht gelesen oder nicht verstanden. Ich sehe teschnisch keine Probleme das ohne try Block registrirung zu machen.

    Beispiel:

    void foo(){
      string a;
      throw 4;
    }
    void foo2(){
      try{
        string b;
        foo();
      }catch(int){
      }
    }
    

    In foo selbst kann man den Cleanup von a inlinen. Somit ist keine Registrirung erforderlich. Wenn die Exception aus foo rausfliegt kann sie mit der Returnaddresse feststellen, wohin sie fliegen wird (ohne wirklich dochthin zu fliegen). Da die Returnaddesse ja bereits at Compiletime bekannt ist, kann der Compiler ein Lookup Table anlegen in dem gespeichert ist in welche Funktion die Exception fliegen wird und was da für Dtoren und catchs sind. Somit muss der try Block nicht at runtime registriert werden. Wahrscheinlich kann man damit Exceptions auch schneller fliegen lassen. Der grosse Nachteil ist natürlich, dass jeder Funktionsaufrufen in ein Lookup Table eingetragen werden muss und somit braucht der Code mehr Platz.

    PS: Wer Schreibfehler findet darf die behalten.



  • Das funktioniert nicht.

    void foo()
    {
    try...
    {
    }catch (a){}
    
    try...
    {
    }
    catch(a)
    {}
    catch(b)
    {}
    
    // zur abwechslung mal ein throw ohne umgebenden block in dieser funktion
    throw ThisCodeIsShitException();
    
    try...
    {
    }
    }
    

    Wie ich seh hast du entweder meinen Post nicht gelesen oder nicht verstanden.

    Nö, du glaubst es mir nur nicht, dass man um die Registrierung der einzelnen Eintritte nicht herum kommt.



  • Kannst du mal etwas genauer erklären was da nicht gehen soll? Wer weis von wo nach wo die Exception fliegen soll weis welche catchs und Dtoren auf dem Weg sind.

    Bei deinem foo läst sich das wunderbar machen. Weder catch noch Dtor sind auf der Flugbahn.



  • Es gibt da einige Punkte, an denen ich mich stoße:

    Da die Returnaddesse ja bereits at Compiletime bekannt ist

    Wieso ist sie das? Warum weißt du zur Kompilierzeit, ob foo() jetzt von bar1() oder bar2() aufgerufen wurde? Gehst du jetzt einfach mal von Inlining aus?

    Du gehst anscheindend auch immer davon aus, dass beim Werfen einer immer die gleichen Objekte zerstört werden. Was zerstört wird, kann jedoch sogar noch vom catch-Block abhängen.

    void foo()
    {
        string x;
        try   { throw BlubberException() }  // zerstör nichts
    catch( BlubberException& )
        {
            if( blubb )
                throw;  // zerstör x
            // zerstör nichts
        }
    
        foo2(y);
        if( bla )
            throw BlubberException();  // zerstör x und y
    
        // ...
    }
    

    Wie willst du diese ganzen Fälle unterscheiden? Du kannst nicht einfach sagen, wenn die BlubberException auftritt, musst du das und as zerstören. Du musst genau wissen, in welchem Block du bist und um was für eine Exception es sich handelt.

    Dann noch was:
    Angenommen throwMe() wirft ne Exception. catchMe() würde sie auffangen

    Exception at throwMe()
    at bla()
    at blubb()
    at main()

    Hier wird nichts gefangen. Das musst du wissen. Das weißt du aber nicht. Mit deinem System kennst du nur die Funktion wo du gerade bist und welche Exception jetzt anliegt.

    Exception at throwMe()
    at bla()
    at catchMe()
    at blubb()
    at main()

    Hier wird sie gefangen. Weil blubb() diesmal (d.h. weil rand()%2 diesmal 0 war) nicht direkt throwMe() aufgerufen hat sondern erst catchMe().

    Exception at throwMe()
    at hoi()
    at bla()
    at catchMe()
    at blubb()
    at catchMe()
    at main()

    Jetzt wirds noch schöner. Jetzt musst du wissen welches catchMe das auffängt. Das ist nicht unwesentlich. Was willst du machen?



  • Da die Returnaddesse ja bereits at Compiletime bekannt ist

    Wieso ist sie das? Warum weißt du zur Kompilierzeit, ob foo() jetzt von bar1() oder bar2() aufgerufen wurde? Gehst du jetzt einfach mal von Inlining aus?

    void foo(int a){
      if(a==1)
        throw 4;
    }
    void foo2(){
      foo(1);
    }
    
    foo_ret_table:
      cmp eax foo2.retaddress
      jne .next
        ;foo wurde as foo2 aufgerufen
      .next:
    
    foo:
      cmp [esp+4] 1
      jne .else
        pop eax    ;Return Address
        mov ebx 4  ;die Exception
        add esp 4  ;Mach den Stack sauber von Argumenten (Dtoren)
        jmp foo_ret_table
      .else:  
      ret 4
    
    foo2:
      push 1
      call foo
      .retaddress:
    

    call foo is eigentlich das gleiche wie:

    push .retaddress
    jmp foo
    

    Und das ret 4 in foo tut dies:

    pop eax
    add esp 4
    jmp eax
    

    (ohne eax zu verändern)

    foo und foo2 sind nur labels die an den Stellen wo man sie benutzt durch Numbern ersetzt werden, wie das jetzt genau lauffähig ist weis ich nur es sind immer die gleichen Numberen!

    Du gehst anscheindend auch immer davon aus, dass beim Werfen einer immer die gleichen Objekte zerstört werden. Was zerstört wird, kann jedoch sogar noch vom catch-Block abhängen.

    Ich hab nie gesagt, dass verschiedene throws in der gleichen Funktion nicht verschiedene Dtoren aufrufen können, sondern die die für ein bestimmt throw auf der Flugbahn liegen:

    void foo(){
      string a;
      {
        string b;
        throw 4;
      }
      throw 2;
    }
    
    foo:
      sub esp sizeof(string)   ;string a
      push esp
      call string::string
    
        sub esp sizeof(string) ;string b
        push esp
        call string::string
    
        push esp               ;throw 4
        call string::~string
        add esp sizeof(string) 
        push esp
        call string::~string
        add esp sizeof(string) 
        pop eax
        mov ebx 4
        jmp foo_ret_table
    
        push esp
        call string::~string
        add esp sizeof(string) 
    
      push esp               ;throw 2
      call string::~string
      add esp sizeof(string) 
      pop eax
      mov ebx 2
      jmp foo_ret_table  
    
      push esp
      call string::~string
      add esp sizeof(string) 
    
      ret
    

    Die throws blähen den Code zwar ein wenig auf aber die kann man ja auch noch auslagern wenn man Angst vor langen jmps im normalen Code hat.

    Du musst genau wissen, in welchem Block du bist und um was für eine Exception es sich handelt.

    Ich code jetzt nicht noch mal einen Beispiel zusammen aber im foo_ret_table stehten die Funktionsaufrufe (!=Funktion) und es ist at compile time klar in welchem Block ein Funktionsaufruf steht.

    Das musst du wissen. Das weißt du aber nicht. Mit deinem System kennst du nur die Funktion wo du gerade bist und welche Exception jetzt anliegt.

    Nein du weis auch noch wo du in der Funktion bist. Ich hab jetzt kein Beispiel mit einem catch Block gecodet, aber zwischen den Dtoren muss man nur die typeids vergleichen und man weis ob man weiter fliegen muss oder in den catch Block springen.

    Jetzt wirds noch schöner. Jetzt musst du wissen welches catchMe das auffängt. Das ist nicht unwesentlich. Was willst du machen?

    Das ist ein non Problem. Der einzige Unterschied zwischen 2 zweimal dem gleichen catch Block ist, dass noch ein paar mehr Daten auf dem Stack sind. Wenn man die nicht wegputzt ist alles ok, und ich putz sie ja nicht weg.



  • Irgendwer schrieb:

    foo und foo2 sind nur labels die an den Stellen wo man sie benutzt durch Numbern ersetzt werden, wie das jetzt genau lauffähig ist weis ich nur es sind immer die gleichen Numberen!

    Man, das ist doch nur in deinem speziellen Fall so. Ich kann foo() von überall aus aufrufen. Ich kann foo() sogar sich selber aufrufen lassen. Ich kann foo aus ein und der selben Funktion an 80 verschiedenen Stellen mehrmals aufrufen. Genausowenig wie du alles inlinen kannst, kannst du die Return Adresse immer zur Kompilierzeit wissen. Du bestimmst deine ret Adresse auch nur über 50 cmp-Aufrufe und branches. Das nenne ich nicht "zur Compilierzeit bekannt".

    Überlass das den Compiler. Der ist schon nicht so blöd wie du glaubst. Natürlich garantiere ich dir nicht, dass der Compiler nicht den Eintritt in einen try-Block nicht doch mal wegoptimieren kann, so wie er auch mal ne ganze Schleife wegoptimieren kann.
    Trotzdem ändert das nichts am allgemeinen Problem.

    Es ist _unmöglich_ für den Compiler, jedesmal, wenn ich noch einen Aufruf von foo() *irgendwo* im Code einbaue, dass er die foo_ret_table updates und noch ein 180stes cmp einfügt.
    Ich kann auch mal ne externe Funktion aufrufen. C++ übersetzt getrennt. Es geht nicht. Es spricht viel zu viel dagegen. Sogar die Logik. Wieso soll ich einen Funktion verändern (bzw. etwas, was dazugehört), weil ich sieo jetzt noch einmal mehr aufrufe??

    Das _jeder_ Eintritt und Austritt in einem try-Block registriert wird ist die einzige Möglichkeit, dass es immer geht. So teuer ist es doch gar nicht. Was ist denn ein push? Ein add auf den Zeiger und nochmal eine Handvoll bytes darauf kopieren.
    Sicherlich wird der Compiler auch ab und zu das wegoptimieren können. Ich würde mich da gar nicht einmischen. Solltest du weiter der festen Überzeugung sein (ich bin es nicht), dass dein System funktioniert, kannst du das ja mal den Compilerbauern erklären.



  • Vielleicht kann jemand eine string-Klasse posten, die als Vorbild tauglich und möglichst performant ist?



  • Es können sich auch ein paar zusammen tun und eine entwickeln.


Anmelden zum Antworten