Wie kann ich meinen Code verbessern?
-
Hallo, ich würde gerne generelles Feedback und Kritik einfordern wie ich meinen Code verbessern kann.
Ich hab schon Erfahrung mit C gesammelt, bin aber fern davon ein Profi zu sein, möchte aber dazulernen.
Ich habe leider keine konkrete Frage zu einem speziellen Problem, würde aber generell mal eine Meinung einholen wollen zu Style/Variablen Benennung/Performance (Bottlenecks) Improvements/Memory Management/Handling und oder Optimierungen/Struktur und vielleicht auch andere Hinweise zum Code direkt, was euch sofort ins Auge fällt und ihr vielleicht anders lösen würdet und warum. Ein Hinweis auf die C Standardbibliothek darf es auch gerne sein wenn ich damit etwas mehr hätte arbeiten können.
Es handelt sich dabei um einen Parser (später soll daraus ein Compressor werden), allerdings speziell darauf abgerichtet mit Wikipedia Dumps zu hantieren in Anlehnung an den Hutter Prize, der hier auch im Forum erwähnt wurde und mich erst dazu bewogen hat das Projekt anzufangen. Allerdings ohne jetzt in Details zu dem Thema Komprimierung einzugehen bzw. findet sich dazu auch nichts im Code dazu.
So lange Rede kurzer Sinn, ich wollte das nur mal vorwegnehmen damit man ungefähr eine Ahnung bekommt um was es geht.
Da ich den Code nicht hier direkt einbinden kann, weil meiner Meinung nach zu umfangreich, hoffe ich das Links auf die Github Seite des Projekts/zum Code in Ordnung sind.
Ist auch nur ein Source-File, eine Trennung mit Header-Datei hat noch nicht stattgefunden.
Bitte lasst euch nicht durch den Umfang des Codes erschrecken, die größten Stellen sind vordefinierte HTML Entities, wie auch Einleseroutinen für Wikipedia Tags und oder Daten.
https://github.com/jrie/wicked ist die Projektseite mit mehr Informationen, der Quelltext findet sich hier: https://github.com/jrie/wicked/blob/master/wicked.c
Wäre für jedes Feedback dankbar.
-
Sortiere mal deine Structmember und mach das #pragma pack() weg, das braucht kein Mensch
-
Nur überflogen:
Warum #pragma pack ?
Sortiere die Member der Größe (sizeof) nach von oben nach unten.
Dann gibt es eigentlich nur am Ende Füllbytes.Müssen die vielen globalen Variablen sein?
Warum so viele Arrays?
Statt const char formatNames[_formats][15] geht *const char formatNames[_formats]
Warum math.h ?
-
bambi schrieb:
Sortiere mal deine Structmember und mach das #pragma pack() weg, das braucht kein Mensch
Also eigentlich habe ich schon versucht die Struct Member so gut es geht (für mich logisch) zu sortieren (nicht nach Datentyp aber nach Byte-Größen in Blöcke), so dass sie 2, 4 bzw. 8 Byte Blöcke bedecken, damit Speicher gespart wird.
Die Pragma pack()-Direktive habe ich deßhalb drin, siehe Makefile, um das Padding gering zu halten und Speicherplatz zu sparen. Ich hab unter Linux auch nicht gemerkt das es dadurch Performance Einbrüche gegeben hat, aber ein paar MB Speicher sparen konnte bei dem 100 MB Testfile.
Für Vorschläge wäre ich aber offen, wobei man dazu sagen muß, so ein Wikipedia Dump (Meta),eine Beispieldatei mit 635 MB Daten, nutzt circa die Hälfte mehr RAM, also 1,209 GB Speicher circa mit Valgrinds Massif ermittelt. Daher auch der Versuch das mit Pragma pack() zu optimieren.
Ein aktueller Wikipedia Dump ist mehr als 13 GB gepackt groß, Beispiel hierfür:
https://dumps.wikimedia.org/enwiki/20160720/Die erste Datei. Also dachte ich mir das es sinnvoll wäre zu versuchen so viel Speicher wie möglich zu sparen, auch jetzt in Betracht auf die Speichervorgaben für die Ausschreibung beim Hutter Prize, aber die Dumps sind halt sehr riesig und vermutlich auch der Speicherverbrauch für Wort-Daten, Entities, WikiTags und marginal die Keys und Key Values.
Aber wenn noch mehr die Meinung vertreten das structure packing nichts bring und das pragma pack() nur mehr Nachteile als Vorteile hat, kann man das ja leicht entfernen/auskommentieren.
DirkB schrieb:
Nur überflogen:
Müssen die vielen globalen Variablen sein?
Eigentlich habe ich diese nur und ausschließlich dafür drin um die "Statistiken", also wieviel Wörter, Entities, Wikitags enthalten sind und wieviel Bytes die eingelesenen Daten circa umfassen.
Allerdings bin ich unsicher ob diese als Parameter an die Funktionen übergeben soll, sind ja einige Werte.
Warum so viele Arrays?
Für Formate, Wikipedia Tag-Typen, Wikipedia Tag-Endings (ungenutzt), Wikipedia-Templates (ungenutzt) und wie gesagt die Entities.
Hast du einen alternativen Vorschlag wie ich es sonst lösen könnte?
Statt const char formatNames[_formats][15] geht *const char formatNames[_formats]
Okay, das ist eine Sache, diese Deklarationsweisen, die ich auch aus dem Main-Konstrukt und den Argumenten-Char-Array kenne aber noch nicht weiter hinterfragt habe.
Macht dies in der Ausführung Unterschiede bzw. wie Speicher/Zugriffe gemanaged bzw. freigegeben werden? Wobei ich mit der Sternchen-Deklaration ja an ein Pointer denken würde, bei dem dann die Längen der Elemente Variable sind aber schon vorher bekannt sind durch das Deklarieren?Warum math.h ?
Theoretisch wird dies nur für ein floor() inkludiert, um die die Laufzeit in Stunden, Minuten und Sekunden nach Ausführung des Parsens auszugeben. Ansonsten findet sich daraus nichts weiter in Gebrauch.
-
- alle globalen Variablen müssen weg
- Namenskonvention sollten unterschiedlich für Variablen/Funktionen, Typen und symbol. Konstanten sein (also z.B. lowerCamelCase, UpperCamelCase, UPPER)
- Namens niemals mit Unterstrich beginnen lassen
- unsignifikante Namen wie "debug" u.ä. vermeiden
- "!feof()" ist der Tod jeglicher Professionalität+Performanz
- zum XML-Parsen nimmt man fertige Bibliotheken
- extended ASCII / Unicode Zeichen/Literale gehören so nicht in den Quelltext
- Dateinamen+Pfade niemals als #define
- u.v.a.m.fachliche Optimierung:
überlege mal, ob du überhaupt die gesamten Daten gleichzeitig im Speicher halten musst
-
Wutz schrieb:
- alle globalen Variablen müssen weg
Okay, die globalen Variablen kann ich notfalls noch übergeben lassen, wird nur ein Rattenschwanz die Werte jeder Funktion zu übergeben anstelle dessen diese global zu haben.
- Namenskonvention sollten unterschiedlich für Variablen/Funktionen, Typen und symbol. Konstanten sein (also z.B. lowerCamelCase, UpperCamelCase, UPPER)
Puh, das wäre möglich, ich dachte es wäre leichter wenn es bei einer Namenskonvention bleibt.
- Namens niemals mit Unterstrich beginnen lassen
Was wäre der Grund hierfür?
- unsignifikante Namen wie "debug" u.ä. vermeiden
- Dateinamen+Pfade niemals als #defineDebug wie auch die Dateipfade sind später als Parameter zu übergeben, im Grunde ist das nur meiner Faulheit zu schulden, damit ich einen Ort habe an dem ich diese effektiv managen kann ohne im Code suchen zu müssen.
Also die würden dann wegfallen. Wobei, das "debug" kann man ja schnell abändern.- "!feof()" ist der Tod jeglicher Professionalität+Performanz
Was wäre denn eine Alternative hierzu, um zu Überprüfen ob eine Datei eingelesen wurde beziehungsweise das Flag gesetzt ist dass das Dateiende erreicht ist?
- zum XML-Parsen nimmt man fertige Bibliotheken
Das XML Parsen ist eigentlich keine Schwierigkeit, das was so umfangreich ist sind die Daten einzulesen.
Der eigentliche XML Parser ist ~60 bis 90 Zeilen lang, warum sollte ich dafür eine Bibliothek verwenden wollen? Jedenfalls für diesen Einsatzzweck? Macht für mich relativ wenig Sinn, ich sehe nicht zwingend die Vorteile, außer meine Implementierung würde nicht funktionierten - aber sie tut scheinbar das was sie soll, einlesen. Welchen Vorteil hätte es fremden Code ins Projekt zu holen?
Edit:
Wegen der Umlaute, wäre da diese Schreibweise "\xcfe...." usw. vorzuziehen?Edit2:
fachliche Optimierung:
überlege mal, ob du überhaupt die gesamten Daten gleichzeitig im Speicher halten musstEine Art SAX Parser wäre auch ideal, ich habe aber im Kopf so eine Blockade das ich unbedingt über den gesamten Dateiaufbau bescheid wissen muß, um damit dann später etwas optimiertes ausgeben zu können was die gleiche Struktur besitzt.
Aber das Problem ist, wenn ich es zu einem Compressor erweitere, muß ich die Datei quasi mehrmals durchgehen, nur mit verschiedenem Fokus.
Und etwas problematisch ist auch zu wissen wann ein Knoten endet bzw. ob. Aber dafür könnte ich mir auch etwas einfallen lassen.
-
Pack doch die globalen Variablen in eine
struct
. Dann hast du nur einen extra Parameter.Namen mit beginnenden Unterstrich sind für den Compiler/System reserviert.
-
theSplit schrieb:
- Namens niemals mit Unterstrich beginnen lassen
Was wäre der Grund hierfür?
Namen die mit einem Underscore und einem Uppercase Buchstaben folgen sind für den Compiler reserviert. Das gleiche gilt für Namen, die mit mehr als einem Underscore beginnen. In deinem Fall ist es also noch ok.
-
- viele FILE-orientierte Funktionen liefern die EOF Informationen im return-Wert mind. implizit mit
- der rohe realloc-Gebrauch ist bei häufigem Aufruf suboptimal
- insgesamt zuviel Code, wird unübersichtlich und damit fehleranfällig
- wiederholte Elemente in mehreren struct
- üblicherweise vermeidet man lange Parameterlisten durch ein structtypedef struct { char **argv; int bla, return; char filename[FILENAME_MAX]; ... } Konfiguration; void init(Konfiguration *k){...} void run(const Konfiguration *k){...} void done(const Konfiguration *k){...} int main(int argc,char**argv) { Konfiguration k={argv}; init(&k); run(&k); done(&k); return k.return; }
Du kannst so schon durch das Design entscheiden, dass der Compiler dich auf Fehler hinweist, hier also z.B. wenn du nach der Initialisierungsphase noch die Parameter veränderst, die aber in der Phase tabu sein sollen.
Außerdem lassen sich durch solche flexiblen Konfigurationsobjekte sehr viel besser Testfälle schreiben.
-
Hallo.
Ich bin jetzt noch ein paar eurer Vorschläge nachgegangen:
Die globalen Variablen für die Dateistatistik sind jetzt in einem Struct zu finden das an die Funktionen per Referenz übergeben wird.Die Entities sind jetzt in Escape Sequenzen und nicht mehr direkt als Unicode-Zeichen eingebunden.
Alle mit #define deklarierten Zähler für die konstanten Datentypen und Switches/Parameter werden in Uppercase geschrieben.
Die Switches sollen ja noch komplett verschwinden, aber zum aktuellen arbeiten mit dem Code ist das setzen mit #define erstmal sehr viel leichter, wenn auch nicht zwingen schön. Aber das würde ich aktuell vernachlässigen wollen.--
Ansonsten überlege ich gerade über die Wutz Vorschläge nach die Variablen besser zu benennen. Damit tue ich mir allerdings schwer und würde mal gerne in die Runde fragen:
Wie handhabt ihr in der Regel die Namens bzw. die Schreibweise (Uppercase, CamelCase, Lowercase) von Variablen, Funktionen und Konstanten? Gibt es dafür ein paar definierte Vorgaben? Gibt es da Coding Conventions in C denen man folgen kann bzw. die man sich mal anschauen kann?Zweiter Punkt an dem ich noch hadere, die Konfiguration des Programms als Struct zu übergeben.
Ich hab mal gelesen das man nicht mehr als 3 Parameter an eine Funktion übergeben lassen sollte, weil sonst so viel auf den Stack kopiert werden muß wenn die Funktion aufgerufen wird und das für die Performance nicht förderlich wäre.
Frage wäre an auch jetzt, ist das (immernoch) so? Habe ich etwas falsches gelesen? Ist das zu vernachlässigen oder dient es eigentlich mehr der Übersicht damit die Funktionsparameter innerhalb der "80 Zeichen" Platz finden und nicht die Ansicht sprengen (wie es momentan bei einigen Funktionen der Fall ist) ?
-
theSplit schrieb:
Wie handhabt ihr in der Regel die Namens bzw. die Schreibweise (Uppercase, CamelCase, Lowercase) von Variablen, Funktionen und Konstanten? Gibt es dafür ein paar definierte Vorgaben? Gibt es da Coding Conventions in C denen man folgen kann bzw. die man sich mal anschauen kann?
Solange es nur lesbar ist, ist es eigentlich ziemlich egal, glaube ich.
Persönlich habe ich das Pascal-Case der WinAPI (z.B.RegEnumValue
) immer als "ordentlich" empfunden, aber programmieren tue ich trotzdem in ... naja, ich weiß nicht, wie man das nennt. Ich programmiere einfach meiner Erfahrung nach.winapi_registry_process_stub
, sowas halt. Aber da denke ich nicht bewusst drüber nach, und das solltest du auch nicht, solange es keine Vorgabe ist.theSplit schrieb:
Ich hab mal gelesen das man nicht mehr als 3 Parameter an eine Funktion übergeben lassen sollte, weil sonst so viel auf den Stack kopiert werden muß wenn die Funktion aufgerufen wird und das für die Performance nicht förderlich wäre.
Halte ich für Unsinn. Funktionen mit vielen Parametern können dir helfen, deinen Code unglaublich allgemein und klein zu halten - als Beispiel eine Funktion, die in der Lage sein soll, auf Windows und Linux Verzeichnisse durchzugehen und Eintragstrings zwischenzuspeichern. Die hat elf Parameter und spart mir woanders einen Haufen explizit auf eine bestimmte Funktionalität ausgerichteten Code.
Problematisch wird es, wenn du spezifiziert, was für Parameter übergeben werden. Nur Zeiger auf Objekte? Am Besten noch als
const
deklariert, weil nur von diesen gelesen werden soll? Kein Problem, ein Zeiger ist verhältnismäßig klein (4 Bytes auf 32-Bit/8 Bytes auf 64-Bit-System). Anders schaut's aus, wenn du ganze Objekte auf den Stack legen willst - ein Objekt, welches in der Lage sein soll, alle (!) möglichen Socket-Adresstypen zu speichern? Da sind wir bei mindestens 128 Bytes pro Objekt. Da komm ich mit meinen elf Zeigern (88 Bytes) gar nicht an.Bei rekursiven Funktionen möchtest du den Footprint auf den Stack auch eher klein halten.
Wenn du's allgemeiner halten willst, kannst du immer noch Macros definieren, die im Hintergrund deine Funktion aufrufen, aber einen Teil der Parameter bereits so übergeben. Wie die Standardparameter in C++. Mach ich auch ständig. So häufig sogar, dass ich Macros habe, die mir Werte generieren, die ich dann in Macros verwende, damit diese dann Funktionen aufrufen. Am Ende hast du dann nur ein
http_stack_init_simple(hst)
und siehst den Aufruf vonhttp_stack_init
gar nicht:#define http_stack_init_simple(hst) \ http_stack_init((hst),0,NULL,NULL,FALSE,0,0,0)
Ein paar Programmierer sagen, dass man dann lieber eine Parameterstruktur bauen und davon ein Objekt anlegen soll, dem man dann die Parameter übergibt. Kann man machen. Aber in der Realität hatte ich noch keine Funktionen, wo dies den Code lesbarer gemacht hätte. Und es macht auf wunderbare Weise meine Standardparametermacros unnötig kompliziert, manchmal sogar kaputt [wie bekommt man den Rückgabewert eines compound statements in einem Macro zurück? Antwort: offiziell gar nicht. Inoffiziell können einige Compiler (GCC) das, aber es bricht dann mit anderen Compilern, und das ist blöd].
-
dachschaden schrieb:
theSplit schrieb:
Wie handhabt ihr in der Regel die Namens bzw. die Schreibweise (Uppercase, CamelCase, Lowercase) von Variablen, Funktionen und Konstanten? Gibt es dafür ein paar definierte Vorgaben? Gibt es da Coding Conventions in C denen man folgen kann bzw. die man sich mal anschauen kann?
Solange es nur lesbar ist, ist es eigentlich ziemlich egal, glaube ich.
Persönlich habe ich das Pascal-Case der WinAPI (z.B.RegEnumValue
) immer als "ordentlich" empfunden, aber programmieren tue ich trotzdem in ... naja, ich weiß nicht, wie man das nennt. Ich programmiere einfach meiner Erfahrung nach.winapi_registry_process_stub
, sowas halt. Aber da denke ich nicht bewusst drüber nach, und das solltest du auch nicht, solange es keine Vorgabe ist.Naja, die Idee feste Konventionen zu haben macht ja schon Sinn.
Wenn ich deine Schreibweisewinapi_registry_process_stub
für Funktionen verwenden würde, wäre sofort ersichtlich das jetzt eine Funktion kommt.Haben wir eine vordefinierte Konstate und diese wird in UPPERCASE geschrieben, wird dies sofort ersichtlich und man rührt diese erst gar nicht bzw. sieht dass man diese weiterverwenden kann.
Ansonsten durchzieht meinen Code bisher nur camelCase - das habe ich mir so angewöhnt.
Aber eine Trennung macht, schon irgendwo Sinn. Ist natürlich etwas doof wenn es dafür keine guten Coding-Standards gibt an denen man sich orientieren kann.
Ich glaube es gab da mal eine Seite Ninja-Coding oder Karate-Coding, irgend so etwas, auf denen wurde auch auf Conventions eingangen...Wenn man sowas durchgängig hält, kann ich mir schon vorstellen dass dies vieles vereinfacht als wenn man das dem Zufall überlässt.
Naming Conventions machen ja auch Sinn, so weiß ich gleich durch "isValid" oder "hasToken" das es sich zu 99,9 % um boolsche Werte handelt - ohne überhaupt nachvollziehen zu müssen wie und wo die Variable ihre Werte her bekommt, es wird aus dem Kontext eigentlich relativ klar.
Sollte man zwar auch prüfen, aber ich meine sowas ist gängige Praxis.
-
theSplit schrieb:
Wutz schrieb:
- zum XML-Parsen nimmt man fertige Bibliotheken
Das XML Parsen ist eigentlich keine Schwierigkeit, das was so umfangreich ist sind die Daten einzulesen.
Der eigentliche XML Parser ist ~60 bis 90 Zeilen lang, warum sollte ich dafür eine Bibliothek verwenden wollen? Jedenfalls für diesen Einsatzzweck? Macht für mich relativ wenig Sinn, ich sehe nicht zwingend die Vorteile, außer meine Implementierung würde nicht funktionierten - aber sie tut scheinbar das was sie soll, einlesen...
Eine Art SAX Parser wäre auch ideal, ich habe aber im Kopf so eine Blockade das ich unbedingt über den gesamten Dateiaufbau bescheid wissen muß, um damit dann später etwas optimiertes ausgeben zu können was die gleiche Struktur besitzt.
Auch für SAX-Parser gibt es fertige Bibliotheken, sogar ziemlich viele.
Ich bleibe dabei, man sollte fertige und gut abgehangene Bibliotheken nehmen.
Und die sollten auch nicht gleich abstürzen, wenn man ihnen - wie du vorhast - XML-Dokumente mit fehlerhafter Baumstruktur vorsetzt, und daran wärst auch du gescheitert.Man sollte solchen Bibliotheken aber auch nicht kritiklos vertrauen, und mit einiger Erfahrung darf man dann die Quellcodes auch mal durchblättern.
Vor vielen Jahren habe ich mal xercesc verwendet, damals war es noch eine reine C-Bibliothek, heute mit einem häßlichen C++ Überbau. Keine Ahnung, warum das sein musste, schließlich werkeln in der Basis nach wie vor reine C-Codes.Nachdem ich dann zufällig auch noch mal in den gehypten libxml2 Code geschaut habe, und mir dabei schlecht wurde, hab ich mir gesagt: das kann ich besser.
Herausgekommen ist dann:
https://github.com/cdoyen/xmlcleanDabei sieht man dann: Portabilität und Performanz müssen sich nicht ausschließen.
Es müssen auch keine globalen Variablen, Makros und #ifdefs sein, und trotzdem läuft es überall und performant.
Und mit Callbacks kann man schon eine Menge machen, insbesondere die API einer Bibliothek übersichtlich und flexibel gestalten.
-
Mh, interessant.
Ich habe allerdings ziemliche Probleme deinen Quellcode nachzuvollziehen, weil mir die Variablen, alles so verdammt gekürzt, einfach nicht plausibel erscheinen und ich nicht verstehe was hier und dort angesprochen wird.
Ich finde da könntest du vielleicht nochmal etwas tun was die Benennung anbelangt, auch wenn der Code dann nicht so "super kompakt" ist...Was mir auch gerade fehlt, wie sehen die Daten aus, die deine Bibliothek zurück gibt an den Aufrufer?
Aus einer Readme deines Projekts:
Ausgehend vom Beispiel
<?xml version="1.0"?> <!--Introduction--> <foo> <bar>baz</bar> <baz foo/> </foo>Müll<Schrott
ergeben sich dann die Typen und Wertepaare wie folgt:
NORMALCLOSE_ : "baz" + "/bar" OPENTAG_ : "\n" + "foo" , "\n" + "bar" SELFCLOSE_ : "\n" +"baz foo/" FRAMECLOSE_ : "\n" + "/foo" COMMENT_ : "\n" +"!--Introduction--" PROLOG_ : "" + "?xml version="1.0"?" UNKNOWN_ : "Müll " + "Schrott"
Aber wie wäre jetzt der Anwendungsfall? - Bezieht sich das ausschließlich auf das "Bereinigen" von XML über die Callbacks? Oder erhalte ich Informationen über den Aufbau zurück?
Wäre nett wenn du mir meine (etwas naiven) Fragen beantworten könntest.
-
Der Anwendungsfall ist ganz einfach und z.B. im Code bei den vorgegebenen Default-Callbacks oder im Beispiel xmlclean:worker_own ablesbar:
#include "xmlclean.h" #include <stdio.h> /* fopen,fclose,fprintf,stderr */ int worker_own(int typ, const unsigned char *tag, size_t taglen, int out(), void* f, Parser *p) { switch (typ) { case NORMALCLOSE_: break; case OPENTAG_: break; case SELFCLOSE_: break; case FRAMECLOSE_: break; case COMMENT_: break; case PROLOG_: break; case UNKNOWN_: break; } printf("%d: \"%.*s\" + \"%.*s\"\n", typ, p->contentlen, p->content, taglen, tag); return XML_OK; } int main(int argc, char**argv) { FILE *f = fopen(argv[1], "rb"); if (!f) return perror(argv[1]), 1; { Parser p = { f, 0, worker_own }; { int r = parse(&p); if (r) { if (r == ERRMEM) fprintf(stderr, "Fehler Speichermangel"); else if (r == ERRHIERAR) fprintf(stderr, "Fehler Hierarchie"); else fprintf(stderr, "Fehler Output"); } fclose(f); done(&p); return r; } } }
Hier siehst du beispielhaft (zugegebenermaßen mit printf und somit an der Callback-API vorbei), wie o.g. Datenpaare zu verstehen und zu benutzen sind.
Im Gegensatz zum SAX-"Standard" gibt es hier bei xmlclean detailliertere Infos für jedes content+tag Paar, eben die Typen und auch die Möglichkeit, viá XPATH die Tags zu separieren.
Die Variablennamen werde ich sicher nicht umbenennen, da du als Anwender ja nicht im Bibliothekscode rumfrickeln sollst, sondern alles über die Initialisierung des Parserobjektes und in den eigenen Callbacks machen sollst, und dort ist alles extensiv benamst und auch dokumentiert.
-
Wutz schrieb:
- alle globalen Variablen müssen weg
ist nicht einsehbar, wenn du keine plausible begründung lieferst.
-
dachschaden schrieb:
Halte ich für Unsinn. Funktionen mit vielen Parametern können dir helfen, deinen Code unglaublich allgemein und klein zu halten ...
naja, ab 5 params würde ich ne struct dafür nehmen.
-
Depp. Klappe halten, weil du keine Ahnung hast.
-
Wutz schrieb:
Depp. Klappe halten, weil du keine Ahnung hast.
hört, hört. unser c-profi meldet sich zu wort.
-
Ja genau.
Ich hab Ahnung und du keine. Und deshalb trolle dich dahin, wo du herkamst.