Wann nehmt ihr lieber C und wann lieber C++?
-
c=µc schrieb:
Nathan schrieb:
Wenn man Exceptions und RTTI deaktiviert hat, gibts eigentlich kaum einen Unterschied zwischen C und C++, was Perfomance und Co angeht.
Templates erzeugen schwer trackbaren Binary-Bloat, das kann auf dem µC stören. Die Standardbibliothek wird deshalb auch deaktiviert.
Dieses eingeschränkte C++ hat dann keinerlei Vorteile gegenüber C, so dass man gleich C verwenden sollte, das ist viel einfacher und bringt den Programmierer nicht in Versuchung, doch ineffiziente C++-Features zu nutzen.
Es gibt so viele kostenlose C++-Features, auf die ich nicht verzichten möchte.
Zum Beispiel Typsicherheits-Wrapper um mache void*-Fumnktionen.
Auf den meisten µC würde ich auch die Standardbibliothek meiden, aber trotzdem C++ bevorzugen.
-
1. Wenn man auf kleinen µC programmieren muss.
2. Wenn man am Linux Kernel mitarbeiten will.
3. Wenn man einen Kernel selber schreibt.
4. Wenn man eine Lib schreibt und dafür möglichst viele Bindings zur Verfügung stellen möchte.
5. Wenn man so klug wie ich ist.
-
ich würde alleine schon C++ nehmen nur um destruktoren zu bekommen. der rest von c++ ist nicht soo wichtig. aber ohne destruktoren wird es übelst masochistisch.
-
volkard schrieb:
Es gibt so viele kostenlose C++-Features, auf die ich nicht verzichten möchte.
Vor allem kann man auch Performance berücksichtigen.
Templates sind zum Beispiel einfach unverzichtbar.
Dieses eingeschränkte C++ hat dann keinerlei Vorteile gegenüber C
Doch, es hat welche. Aber vor allem keine Nachteile.
-
Würde C nur nutzen wenn ich muss.
Ich sehe auch nicht warum man kein C++ für die Kernelentwicklung verwenden sollte. Kein new, keine Exceptions, kein RTTI und gut.
-
Kein new
Das entspricht "kein malloc" in C.
keine Exceptions
Das entspricht kein SJLJ in C.
Oder?
-
Ethon schrieb:
Ich sehe auch nicht warum man kein C++ für die Kernelentwicklung verwenden sollte. Kein new, keine Exceptions, kein RTTI und gut.
Dazu: Keine Vererbung1, keine Template-Übernutzung, kein RAII2
1: Das Problem ist der vptr. Der nimmt einen wichtigen Platz in der Cacheline weg. virtuelle Funktionen aufrufen ist langsam, erfordert 2 Indirektionen. Den vptr erstellen ist langsam, weil er mehrmals umgebogen wird, destructen genauso. Dann will man manchmal eine Instanz der Klasse ohne den vptr-Overhead und es geht nicht. Man erhält schnellere Klassen mit Devirtualisierung. Vererbung muss von Hand gecoded werden.
2: Ja, kein RAII. Begründung: RAII in manchen Klassen und und manchen Klassen nicht, ist schädlich. Entweder ganz oder gar nicht. RAII hat aber ein riesiges Problem und das ist Besitzübertragung. Ohne C++11 hat man verloren. Einen std::string zurückgeben ist nicht performant möglich bzw. nur mit NRVO/URVO, aber darauf zu vertrauen ist fragil. Mit C++11 hat man zwar move, aber der Destruktor wird trotzdem aufgerufen, und auch wenn der nichts tut, das ist unnötiger Code. Den Destruktor eines moved-from-Objektes auszulassen können moderne Compiler in vielen Fällen nicht. Also bleibt eine close()-Funktion, die man von Hand aufruft. Selbst BOOST_SCOPE_EXIT hat Overhead. Deshalb von RAII fernbleiben.
-
Sone schrieb:
Kein new
Das entspricht "kein malloc" in C.
Trotzdem ist new in C++ eingebaut (im Gegensatz zu malloc in C), deswegen sollte man schon explizit darauf verzichten.
Sone schrieb:
keine Exceptions
Das entspricht kein SJLJ in C.
Oder?
Nö, Longjumps sind okay. Die erfordern ja auch keine Compiler-Magie.
Im Prinzip werden da ja nur alle Register gesichert und wiederhergestellt.
Exceptionhandling erfordert einiges an Krempel um zu funktionieren.1: Das Problem ist der vptr. Der nimmt einen wichtigen Platz in der Cacheline weg. virtuelle Funktionen aufrufen ist langsam, erfordert 2 Indirektionen. Den vptr erstellen ist langsam, weil er mehrmals umgebogen wird, destructen genauso. Dann will man manchmal eine Instanz der Klasse ohne den vptr-Overhead und es geht nicht. Man erhält schnellere Klassen mit Devirtualisierung. Vererbung muss von Hand gecoded werden.
Baut nicht der Linux-Kernel an 100 verschiedenen Stellen virtuelle Funktionen nach?
Es muss doch nicht jede Zeile Code im Kernel 100% performant sein?
Das Problem ist doch eher die Wartbarkeit der monolithischen Monster. Da hilft Polymorphie garantiert.Ja, kein RAII. Begründung: RAII in manchen Klassen und und manchen Klassen nicht, ist schädlich. Entweder ganz oder gar nicht. RAII hat aber ein riesiges Problem und das ist Besitzübertragung. Ohne C++11 hat man verloren. Einen std::string zurückgeben ist nicht performant möglich bzw. nur mit NRVO/URVO, aber darauf zu vertrauen ist fragil. Mit C++11 hat man zwar move, aber der Destruktor wird trotzdem aufgerufen, und auch wenn der nichts tut, das ist unnötiger Code. Den Destruktor eines moved-from-Objektes auszulassen können moderne Compiler in vielen Fällen nicht. Also bleibt eine close()-Funktion, die man von Hand aufruft. Selbst BOOST_SCOPE_EXIT hat Overhead. Deshalb von RAII fernbleiben.
Mach mal ein paar Tests. Moderne Compiler hauen praktisch alles unnötige raus.
Ansonsten bleibt das Argument davor: Hin und wieder mal nen Takt zu verschwenden ist vermutlich insgesamt besser wenn dafür der Code um 50% schrumpft und deutlich bugresistenter wird.
-
Ethon schrieb:
Baut nicht der Linux-Kernel an 100 verschiedenen Stellen virtuelle Funktionen nach?
Es muss doch nicht jede Zeile Code im Kernel 100% performant sein?
Das Problem ist doch eher die Wartbarkeit der monolithischen Monster. Da hilft Polymorphie garantiert.Ja, der Kernel baut virtuelle Funktionen nach. Dagegen spricht auch nichts.
fs.h: /* * Keep mostly read-only and often accessed (especially for * the RCU path lookup and 'stat' data) fields at the beginning * of the 'struct inode' */ struct inode { umode_t i_mode; unsigned short i_opflags; kuid_t i_uid; kgid_t i_gid; unsigned int i_flags; #ifdef CONFIG_FS_POSIX_ACL struct posix_acl *i_acl; struct posix_acl *i_default_acl; #endif const struct inode_operations *i_op; struct super_block *i_sb; struct address_space *i_mapping;
Unter C++ hätte das so ausgesehen:
struct inode { const struct inode_operations *i_op; // das ist der vptr umode_t i_mode; unsigned short i_opflags; kuid_t i_uid; kgid_t i_gid; unsigned int i_flags; ...
Reihenfolge ist wichtig. Nur die ersten 64 Bytes liegen in der Cacheline. Deshalb wichtige Daten oben und unwichtige unten.
Und der Konstruktor von inode hätte i_op auf abstract_i_op gesetzt, der konkrete Konstruktor von concrete_inode hätte den Zeiger dann nochmals neu auf concrete_i_op gesetzt. Zu beweisen, dass der vptr nicht benutzt wird, können die Compiler nicht.
Mach mal ein paar Tests.
Ok. Weder g++ 4.8 (COW) noch clang++ 3.4 (SSO) schaffen es
std::string f(int i, const char *c) { if (i < 0) return std::string("nope"); std::string s; if (i--) s += c; return s; }
dahingehend zu optimieren, dass kein Destruktor von s aufgerufen wird. s wird gemoved, aber dann wird der nullpointer deleted.
Moderne Compiler hauen praktisch alles unnötige raus.
Manchmal ist man erstaunt, wie viel sie raushauen, aber manchmal ist man erstaunt, wie "dumm" sie sind.
Ansonsten bleibt das Argument davor: Hin und wieder mal nen Takt zu verschwenden ist vermutlich insgesamt besser wenn dafür der Code um 50% schrumpft und deutlich bugresistenter wird.
Ja, oft ist C++ ausreichend. Aber wenn es wie im Kernel praktisch überall um volle Geschwindigkeit geht, dann ist RAII halt nicht geeignet.
-
c=µc schrieb:
Den Destruktor eines moved-from-Objektes auszulassen können moderne Compiler in vielen Fällen nicht.
Hmm, wann denn genau? Ich muss sagen mich stört auch etwas dass sie ein neues Sprachkonzept mehr oder weniger hinter binding-rules versteckt haben, aber bisher scheinen Compiler da erstaunlich gut zu sein was das Optimieren angeht.
@Ethon Fix mal deinen Beitrag und mach den c++ zu einem quote Block.
Edit @c: Da godbolt irgendwie nicht will kann ich's nur mit MSVC testen, aber
std::string f(int i, const char *c) { 000007F7DE031270 push rbx 000007F7DE031272 sub rsp,50h 000007F7DE031276 mov qword ptr [rsp+28h],0FFFFFFFFFFFFFFFEh 000007F7DE03127F mov rbx,rcx 000007F7DE031282 mov dword ptr [rsp+20h],0 if (i < 0) return std::string("nope"); std::string s; 000007F7DE03128A mov qword ptr [rsp+48h],0Fh 000007F7DE031293 mov qword ptr [rsp+40h],0 000007F7DE03129C mov byte ptr [s],0 if (i--) s += c; 000007F7DE0312A1 mov r8d,0Dh if (i--) s += c; 000007F7DE0312A7 lea rcx,[s] 000007F7DE0312AC call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::append (07F7DE0314B0h) return s; 000007F7DE0312B1 mov qword ptr [rbx+18h],0Fh 000007F7DE0312B9 mov qword ptr [rbx+10h],0 000007F7DE0312C1 mov byte ptr [rbx],0 000007F7DE0312C4 mov rcx,qword ptr [rsp+48h] 000007F7DE0312C9 cmp rcx,10h 000007F7DE0312CD jae f+7Dh (07F7DE0312EDh) 000007F7DE0312CF mov r8,qword ptr [rsp+40h] 000007F7DE0312D4 inc r8 000007F7DE0312D7 je f+85h (07F7DE0312F5h) 000007F7DE0312D9 lea rdx,[s] 000007F7DE0312DE mov rcx,rbx 000007F7DE0312E1 call memcpy (07F7DE031C70h) 000007F7DE0312E6 mov rcx,qword ptr [rsp+48h] 000007F7DE0312EB jmp f+85h (07F7DE0312F5h) 000007F7DE0312ED mov rax,qword ptr [s] 000007F7DE0312F2 mov qword ptr [rbx],rax 000007F7DE0312F5 mov rax,qword ptr [rsp+40h] 000007F7DE0312FA mov qword ptr [rbx+10h],rax 000007F7DE0312FE mov qword ptr [rbx+18h],rcx 000007F7DE031302 mov rax,rbx } 000007F7DE031305 add rsp,50h 000007F7DE031309 pop rbx 000007F7DE03130A ret
Kein call zu operator delete. ?
-
http://gcc.godbolt.org/#{%22version%22%3A3%2C%22filterAsm%22%3A{%22labels%22%3Atrue%2C%22directives%22%3Atrue%2C%22commentOnly%22%3Atrue%2C%22intel%22%3Atrue}%2C%22compilers%22%3A[{%22source%22%3A%22%23include%20%3Clist%3E\nvoid%20feddisch%28%29%3B\nstd%3A%3Alist%3Cint%3E%20f%28int%20i%2C%20int%20x%29\n{\n%20%20if%20%28i%20%3C%200%29\n%20%20%20%20return%20std%3A%3Alist%3Cint%3E%281%2C1%29%3B\n%20%20std%3A%3Alist%3Cint%3E%20v%3B\n%20%20if%20%28i--%29\n%20%20%20%20v.push_back%28x%29%3B\n%20%20feddisch%28%29%3B\n%20%20return%20v%3B\n}\n%22%2C%22compiler%22%3A%22%2Fusr%2Fbin%2Fg%2B%2B-4.8%22%2C%22options%22%3A%22-O3%20-std%3Dc%2B%2B11%22}]}
Oder auch das std::move löschen, dann passiert das gleiche. Der ganze delete-Schrott ist völlig unnötig.
-
c=µc schrieb:
Reihenfolge ist wichtig. Nur die ersten 64 Bytes liegen in der Cacheline. Deshalb wichtige Daten oben und unwichtige unten.
Also wenn ich mich nicht verzählt habe, sollte der Pointer in beiden Fällen innerhalb der ersten 64 Byte sein. Und selbst wenn nicht: ich würde in der Regel den vptr als wichtiges Datum zählen. Es nützt ja nix, wenn man auf die Daten fix zugreifen kann, aber auf die Operationen auf diesen Daten nicht.
Sollte das dennoch ein Problem sein, kann man es immernoch mit einer solchen Struktur und einer Wrapper-Klasse lösen.
-
@c Ui, der Code den std::list generiert ist tatsächlich beängstigent. Zumindest std::forward_list sieht aber mehr oder weniger sauber aus:
std::forward_list<int> f(int i, int x) { 000007F617A51270 push rbx 000007F617A51272 sub rsp,40h 000007F617A51276 mov qword ptr [rsp+30h],0FFFFFFFFFFFFFFFEh 000007F617A5127F mov rbx,rcx 000007F617A51282 xor eax,eax 000007F617A51284 mov dword ptr [rsp+28h],eax 000007F617A51288 mov dword ptr [x],7 if (i < 0) return std::forward_list<int>(1, 1); std::forward_list<int> v; 000007F617A51290 mov qword ptr [v],rax if (i--) v.push_front(x); 000007F617A51295 lea r8,[x] 000007F617A5129A xor edx,edx 000007F617A5129C call std::_Flist_buy<int,std::allocator<int> >::_Buynode<int const & __ptr64> (07F617A51790h) return v; 000007F617A512A1 mov qword ptr [rbx],rax 000007F617A512A4 mov rax,rbx } 000007F617A512A7 add rsp,40h 000007F617A512AB pop rbx 000007F617A512AC ret
Also RAII nicht zu verwenden als Grundregel scheint mir auch im Kernel keine gute Idee. Allerdings gibt es da wohl leider noch einige Fallstricke, da muss ich dir recht geben.
-
Templates erzeugen schwer trackbaren Binary-Bloat, das kann auf dem µC stören. Die Standardbibliothek wird deshalb auch deaktiviert.
Dieses eingeschränkte C++ hat dann keinerlei Vorteile gegenüber C, so dass man gleich C verwenden sollte, das ist viel einfacher und bringt den Programmierer nicht in Versuchung, doch ineffiziente C++-Features zu nutzen.
Weist du, bei C bekomme ich regelmäßig die Krätze weil ich keine STL habe und so programmiere ich mir regelmäßig einen Wolf ab. X Implementierungen von std::list, X Fehlerquellen, X Testreihen... Nicht schön...
Klar, wenn man Low Level programmieren muss (Interrupt Funktion,...) sind so manche Dinge an C++ nicht praktikabel. Aber deswegen die Standardbibliothek verbieten finde ich suspekt, sehr suspekt...
Du scheinst deinen Anwendungsgebiet von C einfach mal so standardisieren wollen, und das geht nicht.Ich könnte genausogut argumentieren das die C Standardbibliothek nicht empfehlenswert ist da diese regelmäßig Buffer Overflows produziert, die Mutter aller Undefined Behaviors printf ist und Pointer öfters
ins Nirvana zeigen als man denkt,...Übrigens. Auch bei kleinere Prozessoren ändern sich die Aufgabenfeldern. Und so mancher Controller besitzt auch schon so einige MHz, auf denen man wunderbar dynamische Probleme wie kleine
Datenbanken, einfache Simulationen implementieren kann. Und da wird es mit C schnell unschön...Nur mal eine Frage: Was ist ein schwer trackbaren Binary-Bloat?
-
Das entspricht kein SJLJ in C.
SJLJ?
HIV!
SPD!!!
-
Bitte ein Bit schrieb:
Nur mal eine Frage: Was ist ein schwer trackbaren Binary-Bloat?
Das, was man bekommt, wenn man mit templates nicht umzugehen weiß...
-
Bitte ein Bit schrieb:
Nur mal eine Frage: Was ist ein schwer trackbaren Binary-Bloat?
Vermutlich wenn die resultierende Binärdatei dick ist weil der Compiler sehr viel Code aus Templates generiert hat.
Kann in C nicht passieren, da hat man nur schrecklich zu benutzende Interfaces die void* für den tollsten Datentyp überhaupt halten.Bitte ein Bit schrieb:
Das entspricht kein SJLJ in C.
SJLJ?
HIV!
SPD!!!
setjmp / longjmp
-
Bitte ein Bit schrieb:
Zitat:
Das entspricht kein SJLJ in C.
SJLJ?
HIV!
SPD!!!
setjmp / longjmp
Danke für die Info.
Passend dazu meine Erklärung zu den Abkürzungen:
SJLJ?HIV! Habe Ich Vergessen!
SPD!! Suche Passende Dokumentation!!!
Vermutlich wenn die resultierende Binärdatei dick ist weil der Compiler sehr viel Code aus Templates generiert hat.
Naja...
Ich fürchte die Programmiersprache kann nicht den Benutzer vor seiner eigenen Dummheit retten. Ich habe schon C Projekte erlebt, welche unnatürlich groß waren und wo man sich fragte wieso das Binary so groß ist. Im Nachhinein machte ich globale Variablen + schlechten Compiler hierfür verantwortlich.