Socket-Buffer C vs C++



  • @It0101 Mach dir mal keine Sorgen um Arrays auf dem Stack. (Außer wenn sie wie hier 1 MB groß sind :D) Aber wenn dein Code C-Lastig ist dann solltest du einfach mal durch gehen, und an jeder Stelle an der ein Handle (z.B. FILE*, oder auch char* auf reservierten Speicher, oder auch SOCKET) wieder frei gegeben wird diese Freigabe in einen Destruktor schieben.



  • cooky451 schrieb:

    @It0101 Mach dir mal keine Sorgen um Arrays auf dem Stack. (Außer wenn sie wie hier 1 MB groß sind :D) Aber wenn dein Code C-Lastig ist dann solltest du einfach mal durch gehen, und an jeder Stelle an der ein Handle (z.B. FILE*, oder auch char* auf reservierten Speicher, oder auch SOCKET) wieder frei gegeben wird diese Freigabe in einen Destruktor schieben.

    Das mach ich sowieso schon. In der Hinsicht gibts doch auch gar keine Probleme.
    Ich habe mich nur deswegen mal damit auseinandergesetzt, weil hier im Forum immer gleich rumgeheult wird, von wegen: "du darfst das so nicht machen, dass ist kein C++", "new benutzt man nicht, weil man immer Container benutzt", usw... Genau deswegen hab ich mich hingesetzt und überlegt, wie ichs besser machen kann.



  • Wieso ist die Variante aus deiner Sicht schlechter als die vector<char>-Variante?

    Abhaengig vom Anwendungszweck. Allgemein beim Array konstanter Laenge als Member kann ich nicht "moven". Eine Kopie ist noetig, ob mit memcpy oder std::copy ist egal (ich nutze letzteres). Obwohl meist ausreichend flink, ist es kein Vergleich zum Verschieben eines Pointers.

    Ich arbeite halt viel mit Buffern und co. und da ist C meist näher als C++

    Ich auch.

    Benchmark?

    Hier bei mir, aus meiner Erfahrung, bei meinem Problem, ... wenn du Sourcecode sehen moechtest, hier:

    void label::buffer::extract(span::buffer const& buf, label l, span::buffer& out_buf) const
    {
        out_buf.lines_.resize(buf.lines().size());
        out_buf.spans_.clear();
    
        auto pline = buf.lines().data();
        auto pline_end = pline + buf.lines().size();
        auto pline_out = out_buf.lines_.data();
        auto pspan = buf.spans().data();
        auto plabel = labels().data();
    
        for (; pline < pline_end; ++pline)
        {
            auto old_size = out_buf.spans().size();
            auto const pspan_end = pspan + *pline;
            for (; pspan < pspan_end; ++pspan, ++plabel)
            {
                if (*plabel == l) out_buf.spans_.push_back(*pspan);
            }
            *pline_out++ = out_buf.spans().size() - old_size;
        }
    }
    

    Aber ich werde ihn nicht in ein nachvollziehbaren Benchmark aufbereiten.

    RAII bei Arrays auf dem Stack, klingt ja interessant.

    Da steht char-Array, ein Vergleich mit std::vector macht nur Sinn, wenn es dynamisch ist.



  • It0101 schrieb:

    ...

    Vermutlich mache ich ja etwas falsch... Bei mir läuft der C++ Code genauso schnell wie der C-Code. Und das auch wenn ich über einen Index zugreife (Getestet unter Visual Studio 2012).

    #include <iostream>
    #include <array>
    #include <windows.h>
    
    const unsigned Size = 1048576;
    const unsigned Cnt = 1000;
    
    void TestCPP()
    {
        std::array<char, Size> Buffer;
    
        long t1 = GetTickCount();
    
        for(unsigned k = 0; k < Cnt; ++k)
            for(unsigned i = 0; i < Size; ++i)
                Buffer[i] = (char)( i % 26 + 65 );
    
        long t2 = GetTickCount();
        std::cout << "C++: " << t2 - t1 << std::endl;
    }
    ...
    


  • It0101 schrieb:

    Das mach ich sowieso schon. In der Hinsicht gibts doch auch gar keine Probleme.

    Sehr gut.

    It0101 schrieb:

    Ich habe mich nur deswegen mal damit auseinandergesetzt, weil hier im Forum immer gleich rumgeheult wird, von wegen: "du darfst das so nicht machen, dass ist kein C++", "new benutzt man nicht, weil man immer Container benutzt",

    Ja, nur leider hast du hier nicht new mit vector verglichen (der Vergleich macht zumindest begrenzt Sinn), sondern ein Array auf dem Stack mit vector (der Vergleich macht keinen Sinn).



  • Gut, war mein Fehler. Ich hoffe trotzdem, dass ich den Unterschied RAII zwischen C Code in C++ und C++ Code vermitteln konnte. Gewiss gibt es mehr, aber fuer mich ist halt RAII der Knackpunkt. Ich kenne keine andere Sprache, die es aehnlich handhabt.

    Das Rumgeheule kommt daher, dass viele mit Java,C#,C, ... Hintergrund anfangen C++ zu programmieren. Was sie aber nicht tun, da nicht nur Sprachmittel (malloc gegen new) ausgetauscht werden muessen sondern auch Konzepte.



  • Gut, dann sind wir uns ja im Wesentlichen einig. Ich danke euch erstmal für eure Impressionen. Ich werde mal über den vector nachdenken.



  • It0101 schrieb:

    Gut, dann sind wir uns ja im Wesentlichen einig. Ich danke euch erstmal für eure Impressionen. Ich werde mal über den vector nachdenken.

    Wieso beißt du dich eigentlich am std::vector fest, wenn du ein std::array willst (Zumindest deinem C-Code nach, siehe auch dem Code von meinen vorherigen Kommentar)?



  • Da hab ich mich vielleicht etwas doof ausgedrückt.

    Ich wollte eher sagen: "Ich werde die C++-Variante in Zukunft stärker berücksichtigen". Ich beiß mich nicht an vector fest. Array hab ich auch im Hinterkopf. 😉



  • Du darfst natürlich auch in C++ lowlevel programmieren. Es gehört bei C++ zum guten Ton das Ganze wegzukapseln und aufzupassen dass es mit (erkennbar) falschem Input nicht merkwürdig crasht.



  • cooky451 schrieb:

    Ausgabe:

    vector: 1.06401
    vector: 1.06201
    vector: 1.06201
    malloc: 0.0220203
    malloc: 0.0210204
    malloc: 0.0210201
    raw new: 0.022021
    raw new: 0.0220209
    raw new: 0.0210198
    raii new: 0.0210201
    raii new: 0.0210202
    raii new: 0.0210198
    

    Der Unterschied ist, dass der std::vector den Speicher initialisiert, indem er für jedes Element seinen default-Konstruktor aufruft, was in der Regel ein sehr gutes Feature ist.

    Der std::vector führt 1048577 Zuweisungen pro Schleifendurchlauf aus. Die anderen Tests aber nur eine einzige. Ergänze ich im test_malloc eine Zuweisung für alle Elemente, dann wird der test_malloc sogar langsamer.

    Und ja, wie Ethon schon gesagt hat, darf man in C++ auch low level programmieren, wo es Sinn macht. Sinnvoll ist es aber, solche Allokationen in eine Klasse zu kapseln. Man könnte ja auch so einen "raw_vector" oder so was implementieren, mit dem man genauso sicher arbeiten kann, wie mit einem std::vector.

    Das Problem bei malloc/free oder new/delete ist, dass man free bzw. delete leicht vergessen oder versehentlich durch ein throw oder return umgehen kann. Bei malloc kommt noch hinzu, dass man es wie im Beispiel eindrucksvoll gezeigt vergessen kann zu prüfen, ob das überhaupt geklappt hat. Es kommt ja keine Exception.



  • dass man free bzw. delete leicht vergessen

    Das ist nicht der Hauptgrund. Wer die Anzahl seiner new/deletes nicht ausgleicht, ist einfach fahrlaessig, schludrig oder unwissend und hat auch kein Interesse daran. Desweiteren gibt es genug Tools, die solche Fehler melden.



  • Ich würd jetzt eher "reserve" und "push_back"/"emplace_back" verwenden, als einen vector zu schreiben, der bei "resize" seine Elemente nicht initialisiert...



  • tntnet schrieb:

    Der Unterschied ist, dass der std::vector den Speicher initialisiert, indem er für jedes Element seinen default-Konstruktor aufruft, was in der Regel ein sehr gutes Feature ist.

    Stimmt, diese total schwachsinnige Regel hatte ich wohl in der Hoffnung verdränkt, dass sie dann aufhören würde zu existieren. Das ist allerdings keine default-initialization (welche sinnvoll wäre, und hier eben genau nichts tun würde), sondern eine value-initialization, was in unserem Fall zu einer zero-initialization wird.



  • Kann mir jemand erklären, wieso das nicht den erhofften Speedup bringt?]cpp]template <typename T>
    struct skip_default_construct_allocator : std::allocator<T> {
    template <class U, class... Args>
    void construct (U* p, Args&&... args)
    {
    static_cast<std::allocator<T>&>(this).construct(p, std::forward<Args>(args)...);
    }
    template <class U>
    void construct (U
    p)
    {
    }
    };

    void test_uninitialized_vector(std::size_t size, unsigned count)
    {
    default_stopwatch sw;

    while (count--) {
    std::vector<char,
    skip_default_construct_allocator<char> > buffer(size);
    buffer[(count * 1234567) % size] = 77;
    }

    std::cout << "uninitialized vector: " << duration_cast<double>(sw.elapsed()) << '\n';
    }
    [/cpp]



  • Vermutlich weil der vector nicht allocator_type::construct verwendet sondern placement new.

    Du könntest dir ein uninitialized_value Wrapper-Template schreiben das im Default-Konstruktor garnichts macht und ein Bytearray verwendet um bei Bedarf ein Objekt konstruieren zu können. (std::aligned_storage)



  • cooky451 schrieb:

    hustbaer schrieb:

    Klar ist er richtig. Er ist halt nur unnötig.

    Nicht ganz. 1. ermöglicht es in vielen move-Fällen erst die Optimierung den Destruktor auszulassen (das habe ich in einem anderen C vs C++ Thread hier festgestellt), und 2. ist es generell performanter so oft wie möglich ein call auszulassen.

    In C++11 ist klar gestellt, dass der Compiler den Aufruf der Deallokationsfunktion unterlassen darf, wenn ein Nullzeiger vorliegt (das "no effect" aus C++03 war ja ziemlich unklar).
    Ich mag die Prüfung auch, aber aus logischen Gründen: was nicht existiert, kann auch nicht gelöscht werden. Der Code wäre auch dann noch korrekt, wenn Nullzeiger nicht mit delete verwendet werden dürften. Es ist nicht klar, wieso die bloße Tatsache, dass Nullzeiger mit delete doch benutzt werden dürfen, einen Imperativ dahin schafft, das die Abfrage plötzlos falsch ist (hat hustbaer auch nicht behauptet, aber das ist im Forum der allgemeine Trend bei diesem Thema).



  • knivil schrieb:

    dass man free bzw. delete leicht vergessen

    Das ist nicht der Hauptgrund. Wer die Anzahl seiner new/deletes nicht ausgleicht, ist einfach fahrlaessig, schludrig oder unwissend und hat auch kein Interesse daran. Desweiteren gibt es genug Tools, die solche Fehler melden.

    Wenn das nur immer so einfach wäre. Gerade bei der Fehlerbehandlung sehe ich dort häufig Unterlassungen. Zwischen new und delete werden Funktionen aufgerufen, die eine exception werfen könnten. Da wird leicht vergessen, das delete in einem catch Block zu machen. Unter Umständen muss man überhaupt erst mal ein try-catch in die Funktion einführen, um überhaupt einen catch Block zu haben. Und auch Tools muss man zum einen einsetzen und auch noch den Fall Testen, wo der Memoryleak überhaupt auftritt. Fehler sind ja in der Regel eher selten. Daher heißen sie ja auch "exception".

    char* buffer = new char[8192];
    unsigned count = socket.read(buffer, 8192);
    doSomethingWithReceivedData(buffer, count);
    delete[] buffer;
    

    Sieht sehr übersichtlich aus und der nicht so versierte Entwickler könnte meinen, das sei richtig. Aber ich muss euch wohl nicht sagen, was hier falsch gelaufen ist.



  • Nur Müslifresser benutzen Exceptions.



  • überlauf schrieb:

    Nur Müslifresser benutzen Exceptions.

    Entweder ca. 50% des Codes für Fehlerhochreichungen und passende close-Aufrufe selber coden ODER Exceptions benutzen. Mag jeder machen, wie er will. Exceptions sind zur Zeit auf PCs ein wenig schneller und brauchen nicht spürbar mehr RAM. Ist vollkommen egal, wenn ich mal anschaue, welchen schlechten Fremdcode ich auf dem Rechner habe und benutze, wo unpassende Algos verwendet werden.


Anmelden zum Antworten