Gibt es eine Dokumentation, in der C ++ - Standardbibliotheksimplementierungen verglichen / gegenübergestellt werden? [geschlossen]

16

(Dies ist per se keine Spielprogrammierung, aber ich bin mir sicher, wenn ich dies auf SO frage, würde ich angewiesen, nicht vorzeitig zu optimieren, obwohl uns die Geschichte sagt, dass sich jedes große Spiel um diese Dinge Sorgen macht.)

Gibt es irgendwo ein Dokument, in dem die Unterschiede in der Leistung und insbesondere in der Speichernutzung zwischen verschiedenen C ++ - Standardbibliotheksimplementierungen zusammengefasst sind? Die Details einiger Implementierungen sind durch NDA geschützt, aber ein Vergleich zwischen sogar STLport vs. libstdc ++ vs. libc ++ vs. MSVC / Dinkumware (vs. EASTL?) Scheint immens nützlich zu sein.

Insbesondere suche ich nach Antworten auf Fragen wie:

  • Wie viel Speicheraufwand ist mit den Standardcontainern verbunden?
  • Welche Container führen dynamische Zuweisungen nur durch, indem sie deklariert werden?
  • Kann std :: string beim Schreiben kopiert werden? Kurzstringoptimierung? Seile?
  • Verwendet std :: deque einen Ringpuffer oder ist es Mist?

quelle
Ich hatte den Eindruck, dass dequein der AWL immer mit einem Vektor implementiert wurde.
Tetrad
@Tetrad: Bis vor ein paar Wochen war ich es auch, aber dann habe ich gelesen, dass es oft von einer seilartigen Struktur implementiert wurde - und das scheint das zu sein, was in STLport steckt.
Die STL verfügt über einen offenen Arbeitsentwurf , anhand dessen Informationen zu den verschiedenen Datenstrukturen (sequentiell und assoziativ), zu den implementierten Algorithmen und zu den implementierten Hilfsklassen abgerufen werden können. Es scheint jedoch der Fall zu sein, dass der Speicher-Overhead implementierungsspezifisch und nicht spezifikationsdefiniert ist.
Thomas Russell
3
@Duck: Die Spieleentwicklung ist der einzige Ort, von dem ich weiß, dass er regelmäßig C ++ - Funktionen auf hoher Ebene verwendet, jedoch die Speicherzuweisungen akribisch nachverfolgen muss, da er auf Systemen ohne virtuellen Speicher mit geringem Arbeitsspeicher ausgeführt wird. Jede einzelne Antwort auf SO würde lauten: "Nicht vorzeitig optimieren, die STL ist in Ordnung, benutze sie!" - 50% der Antworten hier sind bisher die folgenden - und dennoch zeigt Maiks Test eindeutig, dass Spiele, die std :: map verwenden möchten, und Tetrads Verwirrung und meine Verwirrung über gängige std :: deque-Implementierungen ebenfalls ein großes Problem darstellen.
2
@Joe Wreschnig Ich möchte nicht wirklich stimmen, um zu schließen, weil ich am Ergebnis interessiert bin. : p
Die kommunistische Ente

Antworten:

6

Wenn Sie eine solche Vergleichstabelle nicht finden, können Sie den betreffenden STL-Klassen auch einen eigenen Allokator hinzufügen und eine Protokollierung hinzufügen.

Die von mir getestete Implementierung (VC 8.0) verwendet keine Speicherzuordnung, indem lediglich ein String / Vektor / Deque deklariert wird, sondern listet und ordnet dies zu. Die Zeichenfolge hat eine Kurzzeichenfolgenoptimierung, da das Hinzufügen von 3 Zeichen keine Zuordnung ausgelöst hat. Die Ausgabe wird unterhalb des Codes hinzugefügt.

// basic allocator implementation used from here
// http://www.codeguru.com/cpp/cpp/cpp_mfc/stl/article.php/c4079

#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
#include <deque>
#include <list>
#include <map>

template <class T> class my_allocator;

// specialize for void:
template <> 
class my_allocator<void> 
{
public:
    typedef void*       pointer;
    typedef const void* const_pointer;
    // reference to void members are impossible.
    typedef void value_type;
    template <class U> 
    struct rebind 
    { 
        typedef my_allocator<U> other; 
    };
};

#define LOG_ALLOC_SIZE(call, size)      std::cout << "  " << call << "  " << std::setw(2) << size << " byte" << std::endl

template <class T> 
class my_allocator 
{
public:
    typedef size_t    size_type;
    typedef ptrdiff_t difference_type;
    typedef T*        pointer;
    typedef const T*  const_pointer;
    typedef T&        reference;
    typedef const T&  const_reference;
    typedef T         value_type;
    template <class U> 
    struct rebind 
    { 
        typedef my_allocator<U> other; 
    };

    my_allocator() throw() : alloc() {}
    my_allocator(const my_allocator&b) throw() : alloc(b.alloc) {}

    template <class U> my_allocator(const my_allocator<U>&b) throw() : alloc(b.alloc) {}
    ~my_allocator() throw() {}

    pointer       address(reference x) const                    { return alloc.address(x); }
    const_pointer address(const_reference x) const              { return alloc.address(x); }

    pointer allocate(size_type s, 
               my_allocator<void>::const_pointer hint = 0)      { LOG_ALLOC_SIZE("my_allocator::allocate  ", s * sizeof(T)); return alloc.allocate(s, hint); }
    void deallocate(pointer p, size_type n)                     { LOG_ALLOC_SIZE("my_allocator::deallocate", n * sizeof(T)); alloc.deallocate(p, n); }

    size_type max_size() const throw()                          { return alloc.max_size(); }

    void construct(pointer p, const T& val)                     { alloc.construct(p, val); }
    void destroy(pointer p)                                     { alloc.destroy(p); }

    std::allocator<T> alloc;
};

int main(int argc, char *argv[])
{

    {
        typedef std::basic_string<char, std::char_traits<char>, my_allocator<char> > my_string;

        std::cout << "===============================================" << std::endl;
        std::cout << "my_string ctor start" << std::endl;
        my_string test;
        std::cout << "my_string ctor end" << std::endl;
        std::cout << "my_string add 3 chars" << std::endl;
        test = "abc";
        std::cout << "my_string add a huge number of chars chars" << std::endl;
        test += "d df uodfug ondusgp idugnösndögs ifdögsdoiug ösodifugnösdiuödofu odsugöodiu niu od unoudö n nodsu nosfdi un abc";
        std::cout << "my_string copy" << std::endl;
        my_string copy = test;
        std::cout << "my_string copy on write test" << std::endl;
        copy[3] = 'X';
        std::cout << "my_string dtors start" << std::endl;
    }

    {
        std::cout << std::endl << "===============================================" << std::endl;
        std::cout << "vector ctor start" << std::endl;
        std::vector<int, my_allocator<int> > v;
        std::cout << "vector ctor end" << std::endl;
        for(int i = 0; i < 5; ++i)
        {
            v.push_back(i);
        }
        std::cout << "vector dtor starts" << std::endl;
    }

    {
        std::cout << std::endl << "===============================================" << std::endl;
        std::cout << "deque ctor start" << std::endl;
        std::deque<int, my_allocator<int> > d;
        std::cout << "deque ctor end" << std::endl;
        for(int i = 0; i < 5; ++i)
        {
            std::cout << "deque insert start" << std::endl;
            d.push_back(i);
            std::cout << "deque insert end" << std::endl;
        }
        std::cout << "deque dtor starts" << std::endl;
    }

    {
        std::cout << std::endl << "===============================================" << std::endl;
        std::cout << "list ctor start" << std::endl;
        std::list<int, my_allocator<int> > l;
        std::cout << "list ctor end" << std::endl;
        for(int i = 0; i < 5; ++i)
        {
            std::cout << "list insert start" << std::endl;
            l.push_back(i);
            std::cout << "list insert end" << std::endl;
        }
        std::cout << "list dtor starts" << std::endl;
    }

    {
        std::cout << std::endl << "===============================================" << std::endl;
        std::cout << "map ctor start" << std::endl;
        std::map<int, float, std::less<int>, my_allocator<std::pair<const int, float> > > m;
        std::cout << "map ctor end" << std::endl;
        for(int i = 0; i < 5; ++i)
        {
            std::cout << "map insert start" << std::endl;
            std::pair<int, float> a(i, (float)i);
            m.insert(a);
            std::cout << "map insert end" << std::endl;
        }
        std::cout << "map dtor starts" << std::endl;
    }

    return 0;
}

Bisher VC8 und STLPort 5.2 getestet, hier ist der Vergleich (im Test enthalten: String, Vektor, Deque, Liste, Karte)

                    Allocation on declare   Overhead List Node      Overhead Map Node

VC8                 map, list               8 Byte                  16 Byte
STLPort 5.2 (VC8)   deque                   8 Byte                  16 Byte
Paulhodge's EASTL   (none)                  8 Byte                  16 Byte

VC8-Ausgabe-String / Vektor / Deque / Liste / Karte:

===============================================
my_string ctor start
my_string ctor end
my_string add 3 chars
my_string add a huge number of chars chars
  my_allocator::allocate    128 byte
my_string copy
  my_allocator::allocate    128 byte
my_string copy on write test
my_string dtors start
  my_allocator::deallocate  128 byte
  my_allocator::deallocate  128 byte

===============================================
vector ctor start
vector ctor end
  my_allocator::allocate     4 byte
  my_allocator::allocate     8 byte
  my_allocator::deallocate   4 byte
  my_allocator::allocate    12 byte
  my_allocator::deallocate   8 byte
  my_allocator::allocate    16 byte
  my_allocator::deallocate  12 byte
  my_allocator::allocate    24 byte
  my_allocator::deallocate  16 byte
vector dtor starts
  my_allocator::deallocate  24 byte

===============================================
deque ctor start
deque ctor end
deque insert start
  my_allocator::allocate    32 byte
  my_allocator::allocate    16 byte
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
  my_allocator::allocate    16 byte
deque insert end
deque dtor starts
  my_allocator::deallocate  16 byte
  my_allocator::deallocate  16 byte
  my_allocator::deallocate  32 byte

===============================================
list ctor start
  my_allocator::allocate    12 byte
list ctor end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list dtor starts
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte

===============================================
map ctor start
  my_allocator::allocate    24 byte
map ctor end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map dtor starts
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte

STLPort 5.2. Ausgabe mit VC8 kompiliert

===============================================
my_string ctor start
my_string ctor end
my_string add 3 chars
my_string add a huge number of chars chars
  my_allocator::allocate    115 byte
my_string copy
  my_allocator::allocate    115 byte
my_string copy on write test
my_string dtors start
  my_allocator::deallocate  115 byte
  my_allocator::deallocate  115 byte

===============================================
vector ctor start
vector ctor end
  my_allocator::allocate     4 byte
  my_allocator::deallocate   0 byte
  my_allocator::allocate     8 byte
  my_allocator::deallocate   4 byte
  my_allocator::allocate    16 byte
  my_allocator::deallocate   8 byte
  my_allocator::allocate    32 byte
  my_allocator::deallocate  16 byte
vector dtor starts
  my_allocator::deallocate  32 byte

===============================================
deque ctor start
  my_allocator::allocate    32 byte
  my_allocator::allocate    128 byte
deque ctor end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque insert start
deque insert end
deque dtor starts
  my_allocator::deallocate  128 byte
  my_allocator::deallocate  32 byte

===============================================
list ctor start
list ctor end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list dtor starts
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte

===============================================
map ctor start
map ctor end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map dtor starts
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte

EASTL- Ergebnisse, keine Deque verfügbar

===============================================
my_string ctor start
my_string ctor end
my_string add 3 chars
  my_allocator::allocate     9 byte
my_string add a huge number of chars chars
  my_allocator::allocate    115 byte
  my_allocator::deallocate   9 byte
my_string copy
  my_allocator::allocate    115 byte
my_string copy on write test
my_string dtors start
  my_allocator::deallocate  115 byte
  my_allocator::deallocate  115 byte

===============================================
vector ctor start
vector ctor end
  my_allocator::allocate     4 byte
  my_allocator::allocate     8 byte
  my_allocator::deallocate   4 byte
  my_allocator::allocate    16 byte
  my_allocator::deallocate   8 byte
  my_allocator::allocate    32 byte
  my_allocator::deallocate  16 byte
vector dtor starts
  my_allocator::deallocate  32 byte

===============================================
list ctor start
list ctor end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list insert start
  my_allocator::allocate    12 byte
list insert end
list dtor starts
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte
  my_allocator::deallocate  12 byte

===============================================
map ctor start
map ctor end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map insert start
  my_allocator::allocate    24 byte
map insert end
map dtor starts
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
  my_allocator::deallocate  24 byte
Maik Semder
quelle
Dies ist nützlich, um die Details der zugrunde liegenden Zuordnungen abzurufen, sagt jedoch leider nichts über den Overhead und die erwartete Cache-Leistung aus.
@Joe richtig, es ist schwer, alle Ihre Fragen in einer Antwort zu beantworten. Ich bin mir nicht sicher, was genau du mit "Overhead" meinst und außerdem, verglichen mit was? Ich dachte mit Overhead meinen Sie den Speicherverbrauch.
Maik Semder
Mit "Overhead" meine ich mehr die Größe einer leeren Instanz der Strukturen und aller zugehörigen Iteratoren und wie die komplizierteren mit der Zuweisung umgehen - z. B. ordnet std :: list intern mehr als einen Knoten gleichzeitig zu, oder ich die Grundzuteilungskosten für jeden Knoten usw. bezahlen?
1
Die Frage ist nicht "Bitte mach diesen Vergleich", sondern "Wo ist eine Ressource für diesen Vergleich?" - Ich denke nicht, dass SO ein guter Ort ist, um ihn zu "pflegen". Vielleicht solltest du anfangen, es auf einer Google-Site oder einem Wiki oder so zu veröffentlichen.
1
@ Joe nun ist es da: p Ich bin nicht sehr daran interessiert, es auf eine andere Seite zu verschieben, ich war nur an den Ergebnissen interessiert.
Maik Semder
8

std::stringkopiert nicht beim schreiben. Früher war CoW eine Optimierung, aber sobald mehrere Threads in das Bild einfließen, ist eine Pessimisierung nicht mehr möglich - es kann den Code durch massive Faktoren verlangsamen. Es ist so schlimm, dass der C ++ 0x-Standard ihn aktiv als Implementierungsstrategie verbietet. Nicht nur das, sondern die Zulässigkeit std::string, veränderbare Iteratoren und Zeichenreferenzen auszuteilen, bedeutet, dass "Schreiben" für std::stringfast jede Operation erforderlich ist.

Die Optimierung für kurze Zeichenfolgen umfasst, glaube ich, etwa 6 Zeichen oder etwas in dieser Region. Seile sind nicht erlaubt. Sie std::stringmüssen zusammenhängenden Speicher für diec_str() Funktion . Technisch könnte man sowohl eine zusammenhängende Schnur als auch ein Seil in derselben Klasse halten, aber niemand hat es jemals getan. Darüber hinaus wäre es nach meinem Wissen über Seile unglaublich langsam, sie fadensicher zu manipulieren - vielleicht genauso schlimm oder schlimmer als CoW.

Keine Container weisen Speicher zu, indem sie in modernen STLs deklariert werden. Knotenbasierte Container wie list und map haben dies früher getan, jetzt haben sie eine eingebettete Endoptimierung und brauchen sie nicht mehr. Es ist üblich, eine Optimierung namens "Swaptimization" durchzuführen, bei der Sie mit einem leeren Container tauschen. Erwägen:

std::vector<std::string> MahFunction();
int main() {
    std::vector<std::string> MahVariable;
    MahFunction().swap(MahVariable);
}

Natürlich ist dies in C ++ 0x redundant, aber in C ++ 03, als dies üblicherweise verwendet wurde, verringert MahVariable die Effektivität, wenn Speicher bei der Deklaration zugewiesen wird. Ich weiß, dass dies für schnellere Neuzuordnungen von Containern wie vectorin der MSVC9-STL verwendet wurde, wodurch das Kopieren der Elemente überflüssig wurde.

dequeverwendet etwas, das als nicht aufgerollte verknüpfte Liste bezeichnet wird. Grundsätzlich handelt es sich um eine Liste von Arrays, in der Regel mit fester Knotengröße. Als solche für die meisten Anwendungen, behält es die Vorteile beider Zugang structures- Daten zusammenhängenden und fortgeführten Anschaffungs O (1) Entfernung und sowohl vorne hinzufügen zu können und zurück und besser Iterator Ungültigkeits als vector. dequekann aufgrund seiner algorithmischen Komplexität und der Garantie für die Ungültigmachung von Iteratoren niemals durch Vektoren implementiert werden.

Wie viel Speicheraufwand ist damit verbunden? Nun, ehrlich gesagt, ist das eine wertlose Frage. Die STL-Container sind so konzipiert, dass sie effizient sind, und wenn Sie ihre Funktionalität replizieren, erhalten Sie entweder eine schlechtere Leistung oder sie befinden sich wieder an derselben Stelle. Wenn Sie die zugrunde liegenden Datenstrukturen kennen, wissen Sie, wie viel Arbeitsspeicher sie verbrauchen, geben oder nehmen, und dies ist nur aus einem guten Grund, z. B. bei der Optimierung kleiner Zeichenfolgen, mehr.

DeadMG
quelle
"Es ist so schlimm, dass der C ++ 0x Standard es aktiv als Implementierungsstrategie verbietet." Und sie verbieten es, weil frühere Implementierungen es verwendeten oder versuchten, es zu verwenden. Sie leben anscheinend in einer Welt, in der jeder ständig die neueste, optimal implementierte STL verwendet. Diese Antwort ist überhaupt nicht hilfreich.
Ich bin auch neugierig, welche Eigenschaften von std :: deque Ihrer Meinung nach einen zusammenhängenden zugrunde liegenden Speicher verhindern - Iteratoren sind nur nach dem Entfernen am Anfang / Ende gültig, nicht in der Mitte oder nach irgendwelchen Einfügungen, die mit einem Vektor leicht durchgeführt werden können. Und die Verwendung eines Ringpuffers scheint alle algorithmischen Garantien zu erfüllen - amortisiertes Einfügen und Löschen von O (1) an den Enden, O (n) Löschen in der Mitte.
3
@ Joe: Ich denke, dass CoW seit den späten 90ern als eine schlechte Sache eingestuft wurde. Es gibt String-Implementierungen, die dies verwendeten - insbesondere CString -, aber das bedeutet nicht, dass dies zu dieser std::stringZeit der Fall war . Dafür müssen Sie nicht die neuesten und besten STL-Implementierungen verwenden. msdn.microsoft.com/en-us/library/22a9t119.aspx sagt "Wenn ein Element an der Vorderseite eingefügt wird, bleiben alle Referenzen gültig". Ich bin mir nicht sicher, wie ich das mit einem Umlaufpuffer umsetzen soll, da die Größe geändert werden muss, wenn der Puffer voll ist.
DeadMG
Ich werde COW mit Sicherheit nicht als Implementierungstechnik verteidigen, aber ich bin auch nicht naiv, wie oft Software weiterhin mit schlechten Techniken implementiert wird, lange nachdem sie als schlecht identifiziert wurden. Zum Beispiel zeigt der obige Maik-Test eine moderne stdlib, die bei der Deklaration zugeteilt wird. Vielen Dank für den Hinweis zur Gültigkeit von Deque-Referenzen. (Für Nitpick kann ein Vektor alle Garantien hinsichtlich der Ungültigkeit des Iterators und der Komplexität des Algorithmus erfüllen. Diese Anforderung ist keine.) Wenn überhaupt, sehe ich dies als weitere Notwendigkeit für ein Dokument, wie es meine Frage verlangt.
2

Die Frage ist nicht "Bitte mach diesen Vergleich", sondern "Wo ist eine Ressource für diesen Vergleich?"

Wenn das wirklich deine Frage ist (was mit Sicherheit nicht der Fall ist) die Antwort ist, die Sie in Ihrem eigentlichen Fragentext formuliert haben und die in 4 Fragen endete, von denen keine die Frage stellte, wo Sie eine Ressource finden könnten), lautet die Antwort einfach:

Es gibt keinen.

Die Mehrheit der C ++ - Programmierer muss sich nicht so sehr um den Overhead von Standardbibliotheksstrukturen und die Cache-Leistung von ihnen kümmern (was hoch ist) ohnehin Compiler abhängig ist) oder dergleichen kümmern. Ganz zu schweigen davon, dass Sie normalerweise nicht Ihre Standard-Bibliotheksimplementierung auswählen müssen. Sie verwenden, was mit Ihrem Compiler kommt. Selbst wenn es einige unangenehme Dinge tut, sind die Möglichkeiten für Alternativen begrenzt.

Es gibt natürlich Programmierer, die sich für so etwas interessieren. Aber alle haben vor langer Zeit geschworen, die Standardbibliothek zu benutzen.

Sie haben also eine Gruppe von Programmierern, die sich einfach nicht darum kümmern. Und eine andere Gruppe von Programmierern, denen es egal wäre, ob sie es verwenden, aber da sie es nicht verwenden, ist es ihnen egal. Da es niemanden interessiert, gibt es keine wirklichen Informationen über solche Dinge. Es gibt hier und da informelle Patches mit Informationen (Effective C ++ enthält einen Abschnitt über die Implementierung von std :: string und die großen Unterschiede zwischen diesen), aber nichts Umfassendes. Und auf jeden Fall nichts aktuelles.

Nicol Bolas
quelle
Spekulative Antwort. +1 für wahrscheinlich wahr, -1 für keinen Beweis.
verzögerte
Ich habe in der Vergangenheit viele sehr gute und detaillierte Vergleiche gesehen, aber sie sind alle veraltet. Alles vor der Einführung von move ist heutzutage so ziemlich irrelevant.
Peter - Unban Robert Harvey