Forum: Compiler & IDEs Zeitkritisch und Speicherkritisch programmieren in C


von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

Hallo,
eh die ersten kommen und sagen C und Zeitkritisch beißt sich, ich weiß 
Assembler ist schneller.
Es geht darum, dass ich bis jetzt nur kleine Projekte bearbeitet habe, 
bei denen es weder auf das eine noch auf das andere ankam. Jetzt gibts 
ein größeres Projekt mit einer Ethernetverbindung. Der Datenaustausch 
soll natürlich so schnell wie möglich vor sich gehen. Deshalb die Frage, 
nimmt es groß Zeit weg, wenn ich zum Beispiel einen Wert erst in eine 
Variable schreibe, diese einer Funktion übergebe, die ihn dann erst in 
den Sendepuffer schreibt, oder schlimmsten Falls noch mal eine Funktion 
aufruft, die etwas damit tun soll? Ich kenne das z.B. von JAVA Projekten 
auf dem PC, dass man zu jedem "Schiss" ne Methode schreibt um das ganze 
so modular und lesbar wie möglich zu machen. C kommt mir bis jetzt immer 
noch recht kryptisch daher. Was denkt ihr dazu, wie programmiert ihr so, 
worauf sollte ich achten, außer natürlich, dass die File spezifisch der 
Aufgabe sein sollten.Danke für eure Antworten, Links sind natürlich auch 
sehr willkommen, lässt sich nur schlecht googlen das Ganze.

von Uhu U. (uhu)


Lesenswert?

> außer natürlich, dass die File spezifisch der Aufgabe sein sollten.

Was meinst du denn damit?

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

Damit meine ich eigentlich nur, das man Files spezifisch zu ihren 
Aufgaben erstellt und diese Konvention dann einhält um den Überblick zu 
behalten, bzw. nach Aufgaben zu trennen.

von Hugo M. (hmld)


Lesenswert?

Wenn man Protokolle in Hochsprachen schreibt und die Implementierung 
gleichzeitig schnell und modular sein soll, sollte man darauf achten, 
daß so wenig wie möglich umherkopiert werden muß. Das kann man in "C" 
oder "C++" dadurch erreichen, daß man Puffer präalloziert, die so groß 
sind, daß jede Schicht ihre Daten einfach dazupacken kann. Wenn man jede 
Schicht als Klasse sieht, kann eine Funktion in jeder Schicht darüber 
Auskunft geben, wieviel Platz ihre Verwaltungsdaten einnehmen; wie groß 
also ihre Header  und Trailer sind, und demnach wo die jeweils obere 
Schicht anfangen kann, ihre Daten zu schreiben. Das hat auch  den 
Vorteil, daß man mit Blick auf das OSI-Modell Schichten 
dazwischenschieben oder weglassen kann. Die jeweils oberste oder 
unterste Schicht präalloziert in der Initialisierungsphase mit diesen 
Daten dann einen oder mehrere Puffer. (Auf denglish nennt sich diese 
Technik "Overlapped-IO") Der Pufferaufbau sieht damit z.B. so aus:
1
S1:                  Header1 | Nutzdaten1 | Prüfsumme1
2
S2:         Header2 |--------- Nutzdaten2 -----------|
3
S3:Header3 |------------------ Nutzdaten3 -----------| Prüfsumme3

Abhängig von der Leistungsfähigkeit der Zielhardware kann es sinnvoll 
sein, nach der Hochsprachenimplementierung (wenn alles funktioniert) 
einzelne Routinen in Assembler zu schreiben. In diesem Zusammenhang 
bieten sich vor allem die kurzen Prüfsummen-Routinen an. Abhängig vom 
verwendeten Compiler kann man meiner Erfahrung nach einen Faktor von 2-5 
erreichen.

Speicherkritische Fälle, in denen der Speicher kaum für einen einzelnen 
Puffer reicht, habe ich selbst noch nicht in den Fingern gehabt. Aber es 
gibt für diese eine ganze Reihe freie, in "C" implementierte 
Mini/Micro-IP Stacks mit nur wenig eingeschränkter Funktionalität.

von Roland Praml (Gast)


Lesenswert?

Das kann man meist nur beurteilen wenn man das "Compilat" sieht.

über verschiedene Techniken (Call by referece, inline) kann man unnötige 
Kopiervorgänge oft reduzieren, bzw. der Compiler kann das 
"wegoptimieren"

Beim gcc gibts da immer die *.lss (oder warens .lst) Dateien in denen 
das vom Compiler produzierte Resultat steht.

hat man z.B. die definition "void func1(...)" und findet dann ein "RCALL 
func1" im asm wieder, macht man einfach ein "inline" vor "func1" und 
compilierts nochmal und hofft dass der Compiler was gescheites draus 
macht.

Ich geb zu hier muss man (ich) oft ein wenig rum probieren und oft hiflt 
dann auch nur inline assembler.

evtl schaust dir auch das mal an:
http://www.mikrocontroller.net/articles/AVR-GCC-Codeoptimierung

Gruß
Roland

von Uhu U. (uhu)


Lesenswert?

Also ich glaube nicht, daß ASM per se schneller ist, als gut optimierter 
C-Code - der ist nämlich häufig besser, als wenn man dieselbe Aufgabe in 
ASM löst.

Voraussetzung ist allerdings ein sehr guter Optimierer und natürlich mit 
Verstand entworfene Algorithmen.

von hendi (Gast)


Lesenswert?

Wow, danke Jungs, das hilft mir erst mal unheimlich weiter. Werd erst 
mal die mir nicht bekannten Begriffe nachschauen ;-).
@ Hugo: Genau so einen Stack will ich benutzen. Wenn ich das richtig 
verstanden habe fange ich bei der obersten Schicht an meine z.B. HTTP 
Daten zu verpacken und reiche sie dann Stück für Stück nach unten 
weiter, bis sie schließlich in den PHY geschrieben und damit versendet 
werden. Umgekehrt beim empfangen. Also muss ich dementsprechend den 
Sende-/Empfangspuffer groß genug wählen, um alle Daten mit einem Mal 
auszulesen/reinzuschreiben und dies jeweils hintereinander weg.
@Roland: Ja, die Pointersache, damit komm ich noch nicht 
hundertprozentig klar, aber ich denke das wird noch. Letztendlich ist 
Sinn und Zweck der Sache, das eine bestimmte Operation einen Wert im 
Speicher verändert und diese durch eine andere Operation genutzt werden, 
oder gibts da noch andere Anwendungen? Kann man, sollte man Pointer 
global definieren, oder übergibt man sie immer wieder? Also z.B. bei so 
einem Puffer könnte es ja Sinn machen, das man ständig darauf zugreifen 
kann, oder?
Die Assemblersache schiebe ich erst mal, bin froh, wenns in C klappt und 
wenn  die Geschwindigkeit ausreichend ist, dann komm ich vielleicht 
drumherum. Was bedeutet denn Inline? wird dann nur diese eine Funktion 
assembliert?
@Uhu: Ich war bis jetzt in dem Glauben, dass Assembler manche Sachen in 
kürzerer Zeit, weil weniger PCs, schafft. Da gabs hier irgendwo auch mal 
ne Diskussion um einen ARM, da hatten sich Leute ran gesetzt und das 
verglichen, wenn ich nicht irre.
So genug laut gedacht, werd jetzt mal noch n Entwurf machen und morgen 
hoffentlich was auf die Reihe bekommen, schönen Abend noch!

von Peter D. (peda)


Lesenswert?

Ich schließ mich mal an, es wird von nicht-C Programmierern oder 
Windows-C Programmierern total unterschätzt, wie effizient C auf nem MC 
sein kann.

Man muß halt ein bischen mitdenken und es dem Compiler nicht unnötig 
schwer machen.

Wenn ich Daten über größere Arrays bearbeiten muß, übergebe ich gerne 
nur den Pointer auf das Array und dann kann sich die Funktion effizient 
daruf austoben.

Wenn man Parameter über mehrere Funktionen übergibt, sollte man nicht 
unnötig die Reihenfolge ändern, dann entsteht auch kein unnützer Code.

Funktionsaufrufe, die die eigenen Argumente benötigen, schreibt man 
möglichst am Anfang. Funktionen, die den eigenen Returnwert liefern, 
möglichst ans Ende. Dann muß wenig zwischengespeichert werden.

Daß 8-Bitter lieber unsigned char, statt int verabeiten, dürfte auch 
kein Geheimnis mehr sein.


Peter

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

Naja, es handelt sich um einen Arm, da relativiert sich das glaub ich.

>Funktionsaufrufe, die die eigenen Argumente benötigen, schreibt man
>möglichst am Anfang. Funktionen, die den eigenen Returnwert liefern,
>möglichst ans Ende. Dann muß wenig zwischengespeichert werden.
Das verstehe ich nicht so ganz, wieso muss dann weniger 
zwischengespeichert werden?

von Uhu U. (uhu)


Lesenswert?

Wenn du schreibst:


   return f(x);

erhält die Funktion das Ergebnis von f(x) in einem Register; sie muß das 
Ergebnis nicht mehr umkopieren, weil sie es im selben Register 
zurückgibt.

von Andreas K. (a-k)


Lesenswert?

Nicht nur das, u.U. wird auch der Funktionsaufruf eingespart und die 
aufgerufene Funktion direkt angesprungen (JMP statt CALL+RET).

von Uhu U. (uhu)


Lesenswert?

Andreas Kaiser wrote:
> Nicht nur das, u.U. wird auch der Funktionsaufruf eingespart und die
> aufgerufene Funktion direkt angesprungen (JMP statt CALL+RET).

Nein, das ist nicht üblich, denn die Stackverwaltung wird damit gestört.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Peter Dannegger wrote:

> Wenn ich Daten über größere Arrays bearbeiten muß, übergebe ich gerne
> nur den Pointer auf das Array

Was denn auch sonst?  Ein Array wird in C immer über den Zeiger auf
das erste Element übergeben, ein Umkopieren gibt es nicht (außer, man
legt vorher explizit manuell eine Kopie an).

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

@Uhu:Ah, jetzt hat ers, danke.
@Jörg: Ich bin sicher Peter weiß das auch, aber ich wusste das vorher 
nicht unbedingt alles, weil ich mich grad erst tiefer mit C beschäftige, 
da bin ich auch für Hinweise auf Selbstverständlichkeiten dankbar, die 
mich schneller voran bringen.

von Andreas K. (a-k)


Lesenswert?

> Nein, das ist nicht üblich, denn die Stackverwaltung wird damit gestört.

Das ist durchaus üblich, aber natürlich nur wenn dadurch nichts 
durcheinander gebracht wird. Und bei registerorientierten Maschinen ist 
das nicht so selten. Nennt sich "tail call elimination" und ist in GCC 
drin (z.B. ARM).

von Uhu U. (uhu)


Lesenswert?

Geht aber nur, wenn die aufrufende Funktion keine lokalen Variablen hat 
und keine Framepointer auf dem Stack abgelegt werden.

von Andreas K. (a-k)


Lesenswert?

Uhu Uhuhu wrote:
> Geht aber nur, wenn die aufrufende Funktion keine lokalen Variablen hat
> und keine Framepointer auf dem Stack abgelegt werden.

Lokale Variablen darf sie haben, solange sie nicht auf dem Stack liegen 
sondern in nicht zu rettenden Registern. Aber lass das ruhig den 
Compiler entscheiden, der weiss sowas. Und Framepointer sind heutzutage 
in optimiertem Code nicht so häufig anzutreffen, vorausgesetzt der 
Stackpointer kann zur Adressierung genutzt werden.

von Uhu U. (uhu)


Lesenswert?

Andreas Kaiser wrote:
> Und Framepointer sind heutzutage in optimiertem Code nicht so häufig
> anzutreffen, vorausgesetzt der Stackpointer kann zur Adressierung
> genutzt werden.

Da irrst du, denn das Debuggen nicht nur von Dumps wird damit deutlich 
einfacher, weshalb viele Windows-Applikationen darauf nicht verzichten.

von Andreas K. (a-k)


Lesenswert?

> Da irrst du, denn das Debuggen nicht nur von Dumps wird damit deutlich
> einfacher, weshalb viele Windows-Applikationen darauf nicht verzichten.

In diesem Thread geht es um optimierten zeitkritischen Code. Ich weiss 
nicht, ob die Release-Versionen von Windows-Apps mit Framepointer 
übersetzt werden, aber wenn man es es auf zeitkritischen Code anlegt, 
sollte man darauf verzichten.

von Andreas K. (a-k)


Lesenswert?

Und im Debug-Code ist intensive Optimierung ohnehin wenig hilfreich.

von Uhu U. (uhu)


Lesenswert?

Andreas Kaiser wrote:
> Und im Debug-Code ist intensive Optimierung ohnehin wenig hilfreich.

Das stimmt so nicht. Ich habe sehr viel mit Speicherdumps zu tun, die 
über Windows Error Reporting zurückkommen. Das sind selbstverständlich 
alles Releaseversionen und dann muß eben der Post Mortem Dump analysiert 
werden. Dabei sind Stackframes - die übrigens in den MS-Release-Libs 
durchweg vorhanden sind, eine deutliche Arbeitserleichterung, zumal der 
optimierte Code häufig nicht gerade die Struktur des Quellcodes 
abbildet.

von Andreas K. (a-k)


Lesenswert?

>> Und im Debug-Code ist intensive Optimierung ohnehin wenig hilfreich.
>
> Das stimmt so nicht.

Dann gilt also umgekehrt: In Debug-Code ist Optimierung hilfreich??? 
Weiter unten schreibst du dann selber:

> zumal der optimierte Code häufig nicht gerade die Struktur des
> Quellcodes abbildet.

was ja der Punkt ist, auf den ich mich mit o.A. Aussage bezog. Was also 
ist an meinem obigen Satz falsch?

> Ich habe sehr viel mit Speicherdumps zu tun, die
> über Windows Error Reporting zurückkommen. Das sind selbstverständlich
> alles Releaseversionen und dann muß eben der Post Mortem Dump analysiert
> werden.

Hier beziehst du dich auf Release-Code. Mir ist klar, dass Framepointer 
für Analyse nützlich sein können. Aber was hat das mit meiner Aussage zu 
Debug-Code zu tun?

PS: Evtl. Missverständnis? Debug-Code = Debugversion eines Programms, 
Release-Code = Releaseversion.

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

Ich hab mich mal intensiv mit der Zeigerbenutzung auseinandergesetzt. Im 
Kernighan/Ritchie ist das eigentlich auch gut verständlich erklärt. 
Allerdings habe ich noch ein Problem:
1
double *dp, atof(char *)
Klar ist, dass der Zeiger dp ein double-Typ ist, also Adressen, auf die 
er zeigt auch nur double-Werte enthalten dürfen, oder? atof ist auch vom 
Typ double, auch klar. Dann steht aber weiter: "...das Argument von atof 
ein Zeiger auf char ist." Was heißt ein Zeiger auf char? Ein Zeiger auf 
den Datentyp direkt? Oder muss das Argument ein Zeiger sein, der vom Typ 
char ist? Soll atof hier eine Funktion sein, oder einfach irgendeine 
Variable?
Noch was, in C muss man doch glaub ich Zeiger wieder dereferenzieren, 
also irgendwie löschen, dass der Speicher nicht voll gemüllt wird, gibt 
ja keinen GarbageCollector. Wie mach ich das und wann?
Danke übrigens für die bisherigen Antworten, auch aus kleinen 
Streitgesprächen kann man viel lernen ;-)
EDIT: Hab ich ganz vergessen ein Zeiger auf void, heißt das einen Zeiger 
zu deklarieren, dessen Datentyp erst zur Laufzeit bekannt ist, bzw. dem 
dann ein  Zeiger mit bestimmtem Datentyp zugewiesen wird?

von Rolf Magnus (Gast)


Lesenswert?

> double *dp, atof(char *)
>
> Klar ist, dass der Zeiger dp ein double-Typ ist, also Adressen, auf die
> er zeigt auch nur double-Werte enthalten dürfen, oder?

Ja.

> atof ist auch vom Typ double, auch klar.

Der Rückgabetyp, ja.

> Dann steht aber weiter: "...das Argument von atof ein Zeiger auf char
> ist." Was heißt ein Zeiger auf char? Ein Zeiger auf den Datentyp
> direkt?

So wie dp ein Zeiger auf double ist, ist das Argument ein Zeiger auf 
char.

> Oder muss das Argument ein Zeiger sein, der vom Typ char ist?

Der Zeiger ist vom Typ "Zeiger auf char".

> Soll atof hier eine Funktion sein, oder einfach irgendeine
> Variable?

Eine Funktion. Obiges ist äquivalent zu:

double *dp;
double atof(char *);

> Noch was, in C muss man doch glaub ich Zeiger wieder dereferenzieren,
> also irgendwie löschen,

Dereferenzieren ist was ganz anderes. Dereferenzieren tut man, was man 
auf das, worauf er zeigt, zugreift.

> dass der Speicher nicht voll gemüllt wird, gibt ja keinen
> GarbageCollector. Wie mach ich das und wann?

Man "löscht" nicht "den Zeiger", sondern gibt dynamisch allokierten 
Speicher frei, sofern der Zeiger auf solchen zeigt. Das braucht man 
aber, weil der Speicher dynamisch allokiert wurde, nicht weil ein Zeiger 
darauf zeigt.

Beispiel:
1
void f()
2
{
3
    double d = 3;
4
    double *dp; = &d;
5
}

Hier zeigt dp auf d. d wird automatisch zerstört, wenn die Funktion 
beendet ist. Daran ändert auch die Tatsache, daß ein Zeiger darauf 
zeigt, nichts.
Wenn du stattdessen nun den Double-Wert dynamisch allokierst, muß er 
auch wieder freigegeben werden.
1
void f()
2
{
3
    double* dp = malloc(sizeof *dp);
4
    free(dp);
5
}

Hier muß free() aufgerufen werden, weil das, worauf dp zeigt, mit malloc 
dynamisch angelegt wurde.

> EDIT: Hab ich ganz vergessen ein Zeiger auf void, heißt das einen
> Zeiger zu deklarieren, dessen Datentyp erst zur Laufzeit bekannt ist,
> bzw. dem dann ein  Zeiger mit bestimmtem Datentyp zugewiesen wird?

void* ist ein Zeiger auf ein Objekt, dessen Typ an der Stelle, wo der 
Zeiger definiert wurde, nicht bekannt ist. Es ist im Prinzip eine Art 
generischer Zeiger, den man  erst wieder in den richtigen Zeigertyp 
konvertieren muß, um ihn für irgendetwas benutzen zu können.

von Michael G. (linuxgeek) Benutzerseite


Lesenswert?

> eh die ersten kommen und sagen C und Zeitkritisch beißt sich, ich weiß
> Assembler ist schneller.

Ich gehe mal davon aus, dass Du mit "zeitkritisch" jetzt Performace 
meinst und nicht auf das Zeit- bzw. Reaktionsverhalten des Prozesses 
hinaus willst?

In der Regel ist das ein Trugschluss. Moderne PC-CPUs sind so komplex, 
dass Du mit manuell geschriebenem Assembler wahrscheinlich eher bremst 
als beschleunigst. Was meinst Du wieviel know-how in den Compilern 
steckt, die mit allen Tricks versuchen, optimierten Code zu erzeugen, 
der auch tatsaechlich schnell ablaeuft?

Solange Du in C richtig programmierst, kannst Du schon sehr effiziente 
Programme schreiben. C ist ziemlich Low-Level und laesst Dir daher viele 
Eingriffsmoeglichkeiten -- natuerlich kann man auch viel falsch machen.

Wie wahrscheinlich schon mehrfach gesagt: Wenn Du schnellen Zugriff auf 
Daten haben willst, speicher sie irgendwo und arbeite nur mit Pointern. 
Wenn Du noch eine spezifische Frage hast lass hoeren ;)

Michael

von Stefan B. (stefan) Benutzerseite


Lesenswert?

Icke Muster wrote:
> Ich hab mich mal intensiv mit der Zeigerbenutzung auseinandergesetzt. Im
> Kernighan/Ritchie ist das eigentlich auch gut verständlich erklärt.
> Allerdings habe ich noch ein Problem:
>
1
> double *dp, atof(char *)
2
>
> Klar ist, dass der Zeiger dp ein double-Typ ist,

Nein. dp ist ein Zeiger auf Typ double

> also Adressen, auf die
> er zeigt auch nur double-Werte enthalten dürfen, oder?

Die Adressen auf die er zeigt, werden als double interpretiert. Wenn was 
anderes dort drinsteht, wird missinterpretiert.

> atof ist auch vom
> Typ double, auch klar.

Der Rückgabewert der Funktion atof ist vom Typ double.

> Dann steht aber weiter: "...das Argument von atof
> ein Zeiger auf char ist." Was heißt ein Zeiger auf char? Ein Zeiger auf
> den Datentyp direkt? Oder muss das Argument ein Zeiger sein, der vom Typ
> char ist?

Ein Zeiger auf den Typ char wird als Argument der Funktion atof 
übergeben.

> Soll atof hier eine Funktion sein, oder einfach irgendeine
> Variable?

Funktion.

> Noch was, in C muss man doch glaub ich Zeiger wieder dereferenzieren,
> also irgendwie löschen, dass der Speicher nicht voll gemüllt wird, gibt
> ja keinen GarbageCollector. Wie mach ich das und wann?

Fallspezifisch. Wenn du z.B. Speicher dynamisch alloziert hast mit 
malloc, gibst du den Speicher mit free wieder frei. Wenn dein Zeiger auf 
statischen Speicher zeigt (z.B. auf eine Variable), brauchst du nichts 
freizugeben.

> Danke übrigens für die bisherigen Antworten, auch aus kleinen
> Streitgesprächen kann man viel lernen ;-)
> EDIT: Hab ich ganz vergessen ein Zeiger auf void, heißt das einen Zeiger
> zu deklarieren, dessen Datentyp erst zur Laufzeit bekannt ist, bzw. dem
> dann ein  Zeiger mit bestimmtem Datentyp zugewiesen wird?

Ja, dafür kann man Void-Zeiger benutzen, wobei es sinnvoller ist Zeiger 
von bekannten Typen zu benutzen, wenn möglich, weil dann der Compiler 
Warnungen schmeissen kann, wenn inkompatible Zeiger vermischt werden.

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

Jungs, so langsam kommt Licht in den dunklen Wald, hurra.
Nun habe ich gelesen, dass malloc() Speicher vom OS fordert, das heißt, 
wenn ich kein OS habe, sondern direkt mit der Hardware arbeite kann ich 
diese Funktion nicht benutzen?! Was wäre denn so ein Anwendungsfall für 
malloc? Reserviere ich damit Speicher um z.B. einen Puffer anzulegen, 
könnte ich dann nicht auch in eine Variable schreiben, über Pointer und 
dadurch den gezielten Effekt erreichen? Oder benutzt man das um aus 
einem bestimmten Teil des Programms heraus Speicher dauerhaft verfügbar 
zu machen, den man vorher nicht anlegen braucht, sondern erst zu diesem 
Zeitpunkt anlegt , um ihn dann bei nicht weiterer Verwendung sofort 
freigeben zu können?

@Michael:Ja, ich meine Performance. Es geht ja hier um einen ARM(hab ich 
das schon mal erwähnt?) und da habe ich bis jetzt immer gehört, dass 
zeitkritische Operationen bei Mikrocontrollern wohl in Assembler besser 
wären, aber ich lasse mich natürlich gern belehren.

Noch was zu den Rückgabewerten von Funktionen. Ich hab das jetzt so 
verstanden, dass dies eigentlich nur Statusmeldungen der Funktion sind. 
Also 1-->alles cool, hat geklappt, 0-->versuchs noch mal, es gab nen 
Fehler. Die eigentlichen Modofikationen, die ein Fkt. vornimmt werden ja 
direkt im jeweiligen Speicher abgelegt. Oder gibt es auch Funktionen 
nach dem Prinzip
1
int addiere(int a, int b)
2
{
3
int c=a+b;
4
return c;
5
}
? Wobei mir klar ist, dass so eine Operation Zeit frisst, nur zum 
Verständnis.

von Andreas K. (a-k)


Lesenswert?

> Nun habe ich gelesen, dass malloc() Speicher vom OS fordert, das heißt,
> wenn ich kein OS habe, sondern direkt mit der Hardware arbeite kann ich
> diese Funktion nicht benutzen?!

Die Laufzeitumgebungen von Controllern bieten diese Funktion teilweise 
auch. Nur würde ich empfehlen, bei Controllerprogrammen den Speicher 
soweit wie möglich statisch anzulegen. Gibt eine sehr hässliche 
Fehlerquelle weniger.

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

Die letzte Frage hab ich mir grad selber beantwortet, gibt es. die Frage 
ist nur wo besteht der Unterschied, bzw. wann bietet es sich an z.b. 
einen Zeiger direkt zurückzugeben.
1
unsigned short ReadFrame_EMAC(void)
2
  232 {
3
  233   return (*rptr++);
4
  234 }
Hier wird der Zeiger um eine Adresse weitergeschoben und die neue 
Adresse zurückgegeben. Würde es nicht reichen, wenn ich den Zeiger 
verschiebe und dann einfach von anderer Stelle aufrufe, oder kann ich so 
die Manipulation besser nachvollziehen und dadurch Fehler besser 
ausschließen? Diese Funktion ist Teil eines Keil Webserverbeispiels und 
soll einen Frame aus dem Ethernetcontroller lesen, wenn ich das richtig 
verstanden hab. Der Pointer wird anfangs so: static unsigned short 
*rptr; deklariert. Ihm wird während des Programms die Startdresse des 
eingehenden Verkehrs übergeben.

@Andreas: Ich glaube diesen Tip werde ich wohl so umsetzen, zumindest so 
lange wie mir der Rest noch nicht 100% klar ist...

von Uhu U. (uhu)


Lesenswert?

> Hier wird der Zeiger um eine Adresse weitergeschoben und die neue
> Adresse zurückgegeben.

Nein, es wird der Wert zurückgegeben, auf den der Zeiger vor dem 
Weiterschieben zeigte.

Zeiger gibt man genau dann zurück, wenn man die Adresse braucht und den 
Wert eben dann, wenn man nur am Wert interessiert ist.

von Karl H. (kbuchegg)


Lesenswert?

Icke Muster wrote:
> Jungs, so langsam kommt Licht in den dunklen Wald, hurra.
> Nun habe ich gelesen, dass malloc() Speicher vom OS fordert,

Ja und nein.

malloc() ist zunächst mal eine Funktion die dir die Laufzeitumgebung
deines C-Systems zur Verfügung stellt. Dein Programm existiert ja
nicht in einem Vakuum, sondern ist in eine (kleine) Laufzeitumgebung
eingebettet, die sich um die Routinearbeiten kümmert. Teil dieser
Umgebung ist auch die Verwaltung von dynamisch allokiertem Speicher.

Natürlich kann sich auch diese Laufzeitumgebung den Speicher nicht
aus den Fingern saugen, sondern muss irgendwo irgendwie zu einem
größeren Speicherblock kommen, den sie dann in die von malloc()
angeforderten Speicherbereiche zerlegen kann. Im Regelfall fodert
daher die Laufzeitumgebung vom Betriebssystem diesen größeren
Speicherblock an.

> das heißt,
> wenn ich kein OS habe, sondern direkt mit der Hardware arbeite kann ich
> diese Funktion nicht benutzen?!

Nein. Das heist nur, dass es kein Betriebssystem gibt, das man um die
Erlaubnis zur Benutzung eines Speicherblockes fragen muss. Die
Laufzeitumgebung krallt sich dann einfach allen Speicher und
behandelt diesen wie den Speicherblock der von einem OS angefordert
worden wäre.

> Was wäre denn so ein Anwendungsfall für
> malloc? Reserviere ich damit Speicher um z.B. einen Puffer anzulegen,

zum Beispiel, ja.

Oder: Stell dir vor du schreibst ein Programm welches Daten
sortieren soll. Um das zu tun, werden die Daten normalerweise
in einem Array gespeichert. Nur: Wie gross dimensionierst du denn
das Array? 100 Einträge, 200?
Egal welche Zahl du nimmst, es wird immer irgendwie falsch sein.
Entweder dimensionierst du zu klein, und kannst damit bestimmte
Datensätze nicht sortieren (weil zu gross) oder du dimensionierst
zu gross und hast damit eine Menge Speicher brachliegen.

> könnte ich dann nicht auch in eine Variable schreiben, über Pointer und
> dadurch den gezielten Effekt erreichen? Oder benutzt man das um aus
> einem bestimmten Teil des Programms heraus Speicher dauerhaft verfügbar
> zu machen, den man vorher nicht anlegen braucht, sondern erst zu diesem
> Zeitpunkt anlegt , um ihn dann bei nicht weiterer Verwendung sofort
> freigeben zu können?

Man benutzt dynamische Allokierung um damit eine Speicherfläche zu
schaffen, von der man bei der Programmierung nicht weiss bzw. nicht
abschätzen kann, wie gross sie werden wird.

> Noch was zu den Rückgabewerten von Funktionen. Ich hab das jetzt so
> verstanden, dass dies eigentlich nur Statusmeldungen der Funktion sind.
> Also 1-->alles cool, hat geklappt, 0-->versuchs noch mal, es gab nen
> Fehler. Die eigentlichen Modofikationen, die ein Fkt. vornimmt werden ja
> direkt im jeweiligen Speicher abgelegt. Oder gibt es auch Funktionen
> nach dem Prinzip
>
1
> int addiere(int a, int b)
2
> {
3
> int c=a+b;
4
> return c;
5
> }
6
>
> ? Wobei mir klar ist, dass so eine Operation Zeit frisst, nur zum
> Verständnis.

Wofür du den Returnwert benutzt, ist ganz alleine deine Sache.
Ja, oft benutzt man das für Statusmeldungen und ja oft benutzt
man das um ein Ergebnis einer Funktion zurückzugeben. Und ja,
oft genug wird beides miteinander vermischt. Nimm zb. die bereits
angesprochene Funktion malloc(). Normalerweise liefert die dir
die Adresse des frisch allokierten Speicherbereiches. Konnte kein
Speicher mehr reserviert werden, dann liefert die Funktion 0.
Hier hast du beides in einem: Einen Statuswert (nämlich 0) UND
einen Wert der als Ergebnis her halten muss.

von Rolf Magnus (Gast)


Lesenswert?

> Nun habe ich gelesen, dass malloc() Speicher vom OS fordert, das heißt,
> wenn ich kein OS habe, sondern direkt mit der Hardware arbeite kann ich
> diese Funktion nicht benutzen?!

Kommt drauf an, ob sie implementiert ist. malloc() allokiert dynamisch 
Speicher. Ob der von einem OS kommt oder von sonstwo, kann dir egal 
sein. Wichtig ist nur, ob dein Compiler ein malloc() mitbringt.

> Was wäre denn so ein Anwendungsfall für malloc? Reserviere ich damit
> Speicher um z.B. einen Puffer anzulegen, könnte ich dann nicht auch in
> eine Variable schreiben, über Pointer und dadurch den gezielten Effekt
> erreichen?

Ja, sofern du schon beim Programmieren die Größe für den Puffer kennst.

> Oder benutzt man das um aus einem bestimmten Teil des Programms heraus
> Speicher dauerhaft verfügbar zu machen, den man vorher nicht anlegen
> braucht, sondern erst zu diesem Zeitpunkt anlegt , um ihn dann bei
> nicht weiterer Verwendung sofort freigeben zu können?

Auch, ja.

> @Michael:Ja, ich meine Performance. Es geht ja hier um einen ARM(hab
> ich das schon mal erwähnt?) und da habe ich bis jetzt immer gehört,
> dass zeitkritische Operationen bei Mikrocontrollern wohl in Assembler
> besser wären, aber ich lasse mich natürlich gern belehren.

In manchen Fällen kann sich das lohnen, aber da es eben mit zusätzlicher 
Arbeit verbunden ist, sollte man es nur tun, wenn's auch nötig ist und 
eine signifikante Verbesserung der Performance überhaupt zu erwarten 
ist.

> Wobei mir klar ist, dass so eine Operation Zeit frisst,

Tut sie das?

> unsigned short ReadFrame_EMAC(void)
>  232 {
>  233   return (*rptr++);
>  234 }
>
> Hier wird der Zeiger um eine Adresse weitergeschoben und die neue
> Adresse zurückgegeben.

Nein. Erstens wird nicht die Adresse, sondern der Wert, der dort steht, 
zurückgegeben, zweitens ist es nicht die neue, sondern die alte Adresse, 
also die vor dem Inkrementieren.

> Würde es nicht reichen, wenn ich den Zeiger verschiebe und dann einfach
> von anderer Stelle aufrufe,

Sofern du die Daten über einen globalen Zeiger austauschen willst, ja. 
Das tut man aber eher selten. Normalerweise hat der Aufrufer von 
ReadFrame_EMAC() keinen Zugriff auf rptr oder sich zumindest nicht dafür 
zu interessieren. Das nennt man auch "Kapselung".

> oder kann ich so die Manipulation besser nachvollziehen und dadurch
> Fehler besser ausschließen?

Die obige Funktion erlaubt dem Aufrufer gar keine Manipulation, weil sie 
keine Adresse zurückgibt.

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

@ Uhu: Stimmt, auf die nächste Adresse würde ich mit ++*rptr zeigen oder 
mit *(rptr++)?, das macht im Context auch deutlich mehr Sinn, immer 
diese kleinen Fallstricke...
@Karl Heinz: Stimmt, das Bsp. ist sehr bezeichnend. Tja, darum braucht 
man sich halt bei JAVA keine Sorgen machen, aber ich werd mich schon 
dran gewöhnen.

von Uhu U. (uhu)


Lesenswert?

Icke Muster wrote:
> @ Uhu: Stimmt, auf die nächste Adresse würde ich mit ++*rptr zeigen, das
> macht im Context auch deutlich mehr Sinn, immer diese kleinen
> Fallstricke...

Das meinte ich nicht. Du schriebst:

> ... die neue Adresse zurückgegeben.

Das ist definitiv falsch, denn dann sähe der return-Befehl so aus:

   return ++rptr;

(Die Klammer um den gesamten Ausdruck braucht man nicht.)

Allerdings hätte sich der Compiler beschwert, wenn du ihn das vorgestzt 
hättest:

   unsigned short ReadFrame_EMAC(void);

kann garkeinen Pointer zurückgeben.

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

Stimmt * zeigt ja auf den Inhalt, oh man, danke für eure Geduld...

von Karl H. (kbuchegg)


Lesenswert?

Icke Muster wrote:
> @Karl Heinz: Stimmt, das Bsp. ist sehr bezeichnend. Tja, darum braucht
> man sich halt bei JAVA keine Sorgen machen, aber ich werd mich schon
> dran gewöhnen.

(nicht ganz ernst gemeint)
Tja, das ist halt der Unterschied zwischen Jungs und Männern.
Die Jungs veranstalten Chaos in ihren Zimmern und die Mutti
räumt hinterher auf. Die Männer haben sich daran gewöhnt selbst
Ordnung in ihrer Werkstatt zu halten.

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

Tja, aber der Weg zum Mann ist nun mal nicht der leichteste, wie wir ja 
wissen. Aber zumindest hat mein C-Wissen schon die präpubetäre Phase 
überwunden, ich werde zum Mann ouh, ouh, ouh...

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Uhu Uhuhu wrote:

> Das stimmt so nicht. Ich habe sehr viel mit Speicherdumps zu tun, die
> über Windows Error Reporting zurückkommen. Das sind selbstverständlich
> alles Releaseversionen und dann muß eben der Post Mortem Dump analysiert
> werden.

Die sind aber dann auch auf einem Prozessor entstanden, der für seinen
permanenten Registermangel bekannt ist.  Entsprechend kommt man auf
diesem in den meisten Fällen um einen Stackframe sowieso nicht drum
herum, und damit bringt das optionale Weglassen des Stackframes
nichts.  Dabei ist es völlig egal, welches Betriebssystem unter der
Anwendung werkelt -- das lässt sich auf meinem FreeBSD genauso
beobachten wie in deinen Windows-Dumps (außer dass FreeBSD auch auf
anderen CPUs läuft, Windows nicht).

Auf einem Richtigen Prozessor[tm] sieht das ganz anders aus.  Dort ist
es die Regel, dass die Argumentübergabe an viele Funktionen in
Registern erfolgt, und viele einfache Funktionen kommen mit den per
ABI zugewiesenen Arbeistregistern aus, müssen also nichts zusätzlich
retten.  Damit bringt das Weglassen des Stackframes durchaus einiges
an Gewinn.  Wenn du weiterhin noch in einer Umgebung arbeitest wie
einem AVR, in der man sowieso keinen post-mortem-Dump anlegen kann,
braucht man auf einen solchen auch keine Rücksicht weiter zu nehmen.
Klar wird das Debuggen (in diesem Falle dann in-circuit) damit
schwieriger, aber es spart eben im Gegenzug Ressourcen, die hier oft
wertvoll sind.

von Uhu U. (uhu)


Lesenswert?

> Die sind aber dann auch auf einem Prozessor entstanden, der für seinen
> permanenten Registermangel bekannt ist.  Entsprechend kommt man auf
> diesem in den meisten Fällen um einen Stackframe sowieso nicht drum
> herum, und damit bringt das optionale Weglassen des Stackframes
> nichts.

Auch auf diesen Prozessoren kann man den Code ohne Stackframe 
compilieren, ohne dafür eine Laufzeit- oder Speicherstrafe zahlen zu 
müssen.

Der Preis ist allerdings, daß sich die Stackoffsets dauernd ändern und 
eine Dumpanalyse ziemlich mühsam machen, vor allem, wenn man nicht auf 
allen Quelltext zugreifen kann.

Daß man auf einen AVR keinen PMD ziehen kann, glaube ich nicht - das 
macht das Speicherfenster jedes einigermaßen brauchbaren Debuggers. Du 
must nur dafür sorgen, daß die Daten nicht zerschossen werden, bevor du 
sie ansehen kannst.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Uhu Uhuhu wrote:

> Auch auf diesen Prozessoren kann man den Code ohne Stackframe
> compilieren, ohne dafür eine Laufzeit- oder Speicherstrafe zahlen zu
> müssen.

Es bringt dir aber keinen Vorteil mehr.  Der Framepointer wird ohnehin
benötigt, um die lokalen Variablen adressieren zu können, da der
Compiler (bis auf sehr, sehr einfache Funktionen) keine freien
Register hat, um lokale Variablen in diesen unterzubringen.  Gleiches
gilt für die Funktionsargumente: die werden immer auf dem Stack
übergeben, während das auf ,,richtigen'' Prozessoren die Ausnahme ist
(entweder sehr viele Argumente odere eine unbekannte Anzahl, also
"varargs").

> Daß man auf einen AVR keinen PMD ziehen kann, glaube ich nicht

Du kannst dir den Speicher `live' angucken, wenn du einen Emulator
hast (und wenn die Applikation nicht ohnehin sofort neu gestartet
werden muss, weil ihre Funktion benötigt wird), aber es gibt keinen
"post-mortem dump" im Sinne eines offline abgelegten Speicherabbildes,
mit dem man nachträglich (also nach dem CPU-Reset) noch eine Analyse
durchziehen könnte.

Ich habe auch schon `post mortem'-Analysen auf AVRs gemacht, aber das
ist zugegebenermaßen aufwändiger, als das auf einem IA32-System war.

von Michael G. (linuxgeek) Benutzerseite


Lesenswert?

> @Michael:Ja, ich meine Performance. Es geht ja hier um einen ARM(hab ich
> das schon mal erwähnt?) und da habe ich bis jetzt immer gehört, dass
> zeitkritische Operationen bei Mikrocontrollern wohl in Assembler besser
> wären, aber ich lasse mich natürlich gern belehren.

OK womit wir wieder zeitkritisch mit performant verwechselt haben. Wenn 
Du C programmieren kannst und eine Vorstellung davon hast, was hinter 
der Fassade passiert, ist das egal. Ich persoenlich finde C absolut 
passend fuer einen Mikrocontroller, weil es niedrig genug dafuer ist, 
aber komfortabel zu selben Zeit. Dafuer ist C ja gemacht worden, 
naemlich um Assembler weitgehend zu ersetzen. Glaube nicht, dass Dich 
der Assembler vor Fehlern bewahrt, die kannst Du in jeder "Sprache" 
machen.

Ach ja:
1
  return (*rptr++);

Sowas ist mistiger Code, da man hier die Auswertungsreihenfolge und 
Praezendenz der Operatoren beachten muss. Die Lesbarkeit erhoeht das 
nicht wirklich. Die Zeiten, wo sich ein guter Programmierer im Code 
profilieren muss nach dem Motto "meinen Code kann nur ich lesen" sind 
lange vorbei.

Noch eine Anmerkung zum Thema Performance:
1
 int addiere(int a, int b)
2
 {
3
   int c=a+b;
4
   return c;
5
 }

schreibst Du besser als
1
  int addiere(int a, int b)
2
  {
3
    return (a+b);
4
  }

Wahrscheinlich optimiert Dir das der Compiler sowieso weg aber unnoetige 
Operationen sollte man halt vermeiden. Noch performanter waere sicher 
folgendes:
1
  void addiere(int a, int b, int* result)
2
  {
3
    if (result != NULL)
4
    {
5
      *result = a+b;
6
    }
7
  }

Die Pruefung koenntest Du natuerlich auch weglassen aber das solltest 
vllt. eher nicht tun ;)

Und keine Angst mein Jung das kommt alles noch... :D

Michael

von Andreas K. (a-k)


Lesenswert?

Michael G. wrote:

> Ach ja:
>
1
>   return (*rptr++);
2
>

> Sowas ist mistiger Code, da man hier die Auswertungsreihenfolge und
> Praezendenz der Operatoren beachten muss.

Sowas ist nun wirklich ganz stinknormales C. Wie sieht deiner Ansicht 
nach die bessere Alternative aus?

> Noch performanter waere sicher folgendes:
>
1
>   void addiere(int a, int b, int* result)
2
>   {
3
>     if (result != NULL)
4
>     {
5
>       *result = a+b;
6
>     }
7
>   }
8
>

Im Gegenteil. Das ist viel schlechter als die vorherigen Versionen. 
Allein schon deshalb, weil das Resultat nun nicht in einem Register 
liegen kann, sondern nun im Speicher liegen muss, und die Optimierung 
des Compilers aus mehreren Gründen massiv ausgebremst wird. Und es für 
einen sonst oft überflüssigen Stackframe sorgt (was sich bei AVRs massiv 
bemerkbar macht).

von Rolf Magnus (Gast)


Lesenswert?

> Wahrscheinlich optimiert Dir das der Compiler sowieso weg

Mit an Sicherheit grenzender Wahrscheinlichkeit tut er das.

> aber unnoetige Operationen sollte man halt vermeiden.

Wenn sie für den Compiler so leicht zu optimieren sind, besteht dazu 
kein wirklicher Grund. In manchen Situationen kann es sogar den Code 
kleiner und schneller machen, wenn man solche eigentlich unnötigen 
Variablen einführt.

> Noch performanter waere sicher folgendes:
>
>  void addiere(int a, int b, int* result)
>  {
>    if (result != NULL)
>    {
>      *result = a+b;
>    }
>  }

Warum sollte das performanter sein? Man braucht eine zusätzliche Branch 
und einen zusätzlichen Parameter. Außerdem zwingst du den Complier, das 
Resultat in den Speicher zu schreiben statt effizient per Register zu 
übergeben. Das gilt alles natürlich nur, wenn kein inlining betrieben 
wird. Mit inlining werden in der Regel beide Varianten gleich schnell 
sein, aber dann hat deine Version den Nachteil, daß sie weniger lesbar 
und weniger intuitiv in der Benutzung ist.

> Und keine Angst mein Jung das kommt alles noch

Dito ;-)

von Karl H. (kbuchegg)


Lesenswert?

Noch was.

>
1
> return (*rptr++);
2
>

Wie ein Kollege von mir vor vielen, vielen Jahren sagte:
return ist kein Funktionsaufruf!

Die Klammern hier sind völlig unnötig.

Wenn du schon Klammern benutzt, dann benutze sie um anzuzeigen,
wie du die Auswertereihenfolge haben willst. Also entweder
1
return (*rptr)++;

oder
1
return *(rptr++);

aber einfach nur den Return-Wert einzuklammern bringt nichts. Da
kannst du die Klammern auch weglassen:
1
return *rptr++;

und ersparst den Leser die Fragestellung: "Warum um alles in der Welt
hat er hier Klammern eingesetzt? Wird da die Auswertereihenfolge
umgedreht, oder was?"

von Andreas K. (a-k)


Lesenswert?

> Die Klammern hier sind völlig unnötig.

Klar. Ist aber eine Stilfrage. Nicht wenige Leute schreiben return 
grundsätzlich mit Klammern, wahrscheinlich aus Analogie zu if() while() 
sizeof() ...

von Icke M. (Firma: my-solution) (hendi)


Lesenswert?

Leute, ihr machts mir nicht gerade einfach ;-). Ist es nun besser wenn 
ich das Ergebnis einer Funktion über einen Zeiger zurück gebe, oder 
direkt als return? Besser lesbar ist natürlich die zweite Variante, also 
wenn das keine Unterschiede bringt würde ich, wo es möglich ist eher die 
verwenden. Allerdings lese ich soetwas im Kernighan/Ritchie eher selten, 
da wird fast ausschließlich mit Zeigern gewerkelt. Versteht mich bitte 
richtig ich will mir nicht gleich am Anfang den Stil versauen ;-). 
Übrigens das Codebeispiel, dass ich hier hab ist von 2001, scheinbar gab 
es da die Regel meinen Code kann nur ich lesen zu diesem Zeitpunkt noch, 
zum Glück ist alles gut dokumentiert. Liegt wahrscheinlich aber auch 
daran, dass ich noch nicht so das Auge für manche Techniken habe.
@Michael: Ich dachte halt zeitkritische Abläufe bedingen performanten 
Code, vielleicht ist das aber auch ein Verständnisproblem.
Ansonsten wieder mal danke an alle die mich hier fit machen :-)

von Andreas K. (a-k)


Lesenswert?

> Ist es nun besser wenn
> ich das Ergebnis einer Funktion über einen Zeiger zurück gebe,

Bei Basisdatentypen wie int/long/double usw. ist ein Pointer für einen 
einzigen Returnwert schlicht und einfach Blödsinn. Bei Strukturen als 
Returnwert kann es hingegen sinnvoll sein.

von Rolf Magnus (Gast)


Lesenswert?

> Ist es nun besser wenn ich das Ergebnis einer Funktion über einen
> Zeiger zurück gebe, oder direkt als return? Besser lesbar ist
> natürlich die zweite Variante, also wenn das keine Unterschiede bringt
> würde ich, wo es möglich ist eher die verwenden.

Das würde ich so empfehlen.

> @Michael: Ich dachte halt zeitkritische Abläufe bedingen performanten
> Code, vielleicht ist das aber auch ein Verständnisproblem.

Nicht unbedingt. Manchmal muß der Code nicht unbedingt so schnell wie 
möglich sein, aber die Ausführungszeit muß sehr genau stimmen, wenn man 
z.B. ein Timing einer seriellen Kommunikation einhalten muß oder 
Impulszeiten sehr genau messen will. Da hat man in Assembler zumindest 
bei so einfachen Architekturen wie AVR die Möglichkeit, die Zeiten auf 
den Taktzyklus genau zu bestimmen. In C geht das nicht mehr so einfach, 
weil ja aus dem Quellcode alles mögliche werden kann, und dazu hängt es 
noch vom Compiler, dessen Version und den Optimierungseinstellungen ab. 
Da kann es durchaus auch passieren, daß der Code in C zu schnell wird, 
weil der Compiler einem was wegoptimiert.

von Uhu U. (uhu)


Lesenswert?

Michael G. wrote:
> Ich persoenlich finde C absolut
> passend fuer einen Mikrocontroller, weil es niedrig genug dafuer ist,
> aber komfortabel zu selben Zeit.

Ich pflege C als Highlevelassembler zu bezeichnen.

> Ach ja:
>
>  return (*rptr++);
>
> Sowas ist mistiger Code, da man hier die Auswertungsreihenfolge und
> Praezendenz der Operatoren beachten muss.

Sehe ich nicht so - ich mache das immer so (allerdings ohne die 
Klammern), weil es schön übersichtlich ist und Redundanz vermeidet. Zeig 
mir die Programmiersprache, bei der Ausführungsreihenfolge und 
Operatorpräzedenzen keine Rolle spielen...

Andreas Kaiser wrote:
> Bei Basisdatentypen wie int/long/double usw. ist ein Pointer für einen
> einzigen Returnwert schlicht und einfach Blödsinn. Bei Strukturen als
> Returnwert kann es hingegen sinnvoll sein.

Auch das kann man so einfach nicht sagen: Wenn man die Adresse braucht, 
um die referierte Variable verändern zu können, nützt einem deren Inhalt 
herzlich wenig.

von Uhu U. (uhu)


Lesenswert?

Jörg Wunsch wrote:
> Uhu Uhuhu wrote:
>
>> Auch auf diesen Prozessoren kann man den Code ohne Stackframe
>> compilieren, ohne dafür eine Laufzeit- oder Speicherstrafe zahlen zu
>> müssen.
>
> Es bringt dir aber keinen Vorteil mehr.

Es spart genau den Speicher, den die Stacklinks belegen und die Befehle, 
sie zu pflegen.

> Der Framepointer wird ohnehin
> benötigt, um die lokalen Variablen adressieren zu können, da der
> Compiler (bis auf sehr, sehr einfache Funktionen) keine freien
> Register hat, um lokale Variablen in diesen unterzubringen.

Nein. Das ebp-Register, das bei Speichermodellen mit Framepointer für
ebendiesen reserviert wird, steht frei zur Verfügung. Lokale Variable
werden über esp indiziert - deswegen ändern sich bei den 32-Bit-Modellen
auch dauernd die Offsets der lokalen Variablen, was die Analyse deutlich
erschwert.

(Die 64-Bit-Modelle wurden in dieser Beziehung etwas cleverer
konstruiert: Dort wird auf den lokalen Variablen beim Funktionseintritt
immer soviel Parameterspeicher allokiert, wie maximal benötigt werden.
=> konstante Offsets.)

> Gleiches
> gilt für die Funktionsargumente: die werden immer auf dem Stack
> übergeben, während das auf ,,richtigen'' Prozessoren die Ausnahme ist
> (entweder sehr viele Argumente odere eine unbekannte Anzahl, also
> "varargs").

Also die Idelologie von den ",,richtigen'' Prozessoren" kannst du
getrost in der Rumpelkammer lassen. Die ist ganz gewöhnlicher Blödsinn.

Ich habe von Intel alles von 8 bis 64 Bit schon mit ASM programmiert.
Lediglich bei den 8 und 16-Bit-Prozessoren hätte man sich über eine
handvoll zusätzliche Register und einen gleichförmigeren Befehlssatz
sehr gefreut.

Bei 32-Bit kann man auf mehr Register getrost verzichten. Die 
zusätzlichen Adressiermöglichkeiten bügeln die ererbten Mängel 
vollständig aus. Zudem ist es wirklich egal, ob du einen Parameter mit 
einem mov, oder einem push dorthin schiebst, wo er von der aufzurufenden 
Funktion erwartet wird. (Bei 2-3 Cachlevels über dem eigentlichen 
Arbeitsspeicher ist das Zeitverhalten auch nicht schlechter, als wenn 
alle lokalen Variablen in Prozessorregistern gehalten würden.)

Und wenn du auf einer Vielregistermaschine eine Funktion aufrufst, mußt
du auch entweder die aktuellen Register auf den Stack schieben, oder
hinterher wieder laden.

Mehr spaßeshalber habe ich auch schon den MSP430 mit ASM programmiert
und mich zunächst über die vielen Register gefreut - nur um hinterher
festzustellen, daß ich auch nicht mehr benutzt habe, als auf dem
Pentium.

Ich halte, offen gesagt, diese Glaubenskriege um ,,richtige'' und
,,falsche'' Prozessoren für Streitereien um Kaisers Bart - die überläßt
man besser den Haarespaltern.

> Ich habe auch schon `post mortem'-Analysen auf AVRs gemacht, aber das
> ist zugegebenermaßen aufwändiger, als das auf einem IA32-System war.

Was mich beim Itanium genervt hat, sind diese kleinen Fitzelbefehle, die
einzeln wenig tun.

von Andreas K. (a-k)


Lesenswert?

> Zudem ist es wirklich egal, ob du einen Parameter mit einem mov, oder
> einem push dorthin schiebst, wo er von der aufzurufenden Funktion
> erwartet wird.

Store to load latency bei Integer-Register: typ. 0-1 Takte.
Store to load latency bei Speicher: mind. 3-4 Takte (K8,Core2).
Gleichzeitige Lesezyklen Integer-Register: 9, Speicher: 2.

Doch, das macht sehr wohl einen Unterschied.

Und es machte bisher auch einen Unterschied, ob push oder mov im 
Speicher, weil die Resource ESP ausbremst. Kein Problem bei Registern. 
Erst ab Core2,K8L ist diese kritische Abhängigkeit weg.

> Und wenn du auf einer Vielregistermaschine eine Funktion aufrufst, mußt
> du auch entweder die aktuellen Register auf den Stack schieben, oder
> hinterher wieder laden.

Yep, aber es ist nicht unwichtig wann und wo. Entscheidend ist hier 
nämlich, dass die unterste Ebene von Funktionen, die üblicherweise die 
meistaufgerufene ist, wenig push/pop-traffic hat. Nötige pushs/pops des 
callers treten demgegenüber weniger in Erscheinung.

Und grad bei den leaf functions ist auch die oben skizzierte latency 
nicht unwichtig. Sollte ein C++ Compiler den "this" pointer auf dem 
stack übergeben, wird man das dank dort üblicher sehr kleiner leaf 
functions markant zu spüren kriegen.

von Andreas K. (a-k)


Lesenswert?

> Bei 32-Bit kann man auf mehr Register getrost verzichten.

Hast du schon mal mit Befehlen gearbeitet, die eine signifikante latency 
aufweisen, und versucht, das Problem mit loop unrolling anzugehen? Ist 
ja Standardtechnik für Optimierung. Nur wirst du dann merken, dass man 
dabei hässlich viele Register braucht.

Nicht dass das bei AVR,MSP430 irgendwie von Bedeutung wäre. Aber genau 
deshalb hat Itanium deren viele.

von Uhu U. (uhu)


Lesenswert?

Worauf ich hinaus will:

- Die Grabenkämpfe zwischen RISC und CISC sind überflüssig
- Die Grabenkämpfe zwischen Verfechtern von Viel- und
  Wenigregistermaschinen kann man sich schenken
- Die Grabenkämpfe zwischen Verfechtern unterschiedlicher
  Programmiersprachen sind überflüssig.

Das einzige, was in meinen Augen zählt, ist ein schlüssiges 
Gesamtkonzept und eine gute Implementierung.

Was zählt, ist die Gesamtleistung eines Systems - Hard- und Software 
zusammengenommen.

Vor Urzeiten, als der 8086 noch nicht auf dem Markt war, gab es bereits 
die ersten Motorola 68000. Wenn man deren Prozessormodelle und 
Befehlssätze vergleicht, kann man einfach nur einen Schluß ziehen: 
Bitte, bitte keinen 8086...

Und was hat sich durchgesetzt?

Nicht daß ich das toll fände, aber als glänzendes Beispiel für 
ingenieurmäßigen Pragmatismus kann man es allemal nehmen.

> Nicht dass das bei AVR,MSP430 irgendwie von Bedeutung wäre. Aber genau
> deshalb hat Itanium deren viele.

Und warum wird des schöne Itanium eingestampft?

Ich kenne die Dinger nur als heulende Heizöfen, die viel zu schwer und 
klobig für das sind, was sie bringen.

von Andreas K. (a-k)


Lesenswert?

> Was zählt, ist die Gesamtleistung eines Systems - Hard- und Software
> zusammengenommen.

Korrekt.

> Und was hat sich durchgesetzt?

Das hat nichts mit Qualität und viel mit Glück zu tun. Hätte IBM sich 
nicht für den 8088 entschieden, wären x86er heute grad so bekannt wie 
Z8000.

> Nicht daß ich das toll fände, aber als glänzendes Beispiel für
> ingenieurmäßigen Pragmatismus kann man es allemal nehmen.

Naja. Selbst Intel hatte 8086 nur als Übergangslösung bis zum 432 
vorgesehen, um 8080-Programme über die Zeit retten zu können. Nur hatten 
ebendiese "pragmatischen" Ingenieure mit dem 432 ein grausiges und elend 
langsames Monster hingesetzt, und so hatte Intel erstmal nichts besseres 
auf Lager.

> Und warum wird des schöne Itanium eingestampft?

Weil x86-Programme nicht wirklich gut darauf laufen. Das, und nur das, 
ist das effektive Kriterium im Massenmarkt.

Intel hatte ja selber versucht, den x86 allmählich loszuwerden. Und 
dabei AMD nicht beachtet, die mit AMD64 genau in diese Flanke stiessen 
und damit x86 ins 64bit Zeitalter retteten. Ohne AMD wäre das Ende von 
x86 bereits eingeleitet.

von Uhu U. (uhu)


Lesenswert?

Totgesagte leben bekanntlich länger...

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Uhu Uhuhu wrote:

> Also die Idelologie von den ",,richtigen'' Prozessoren" kannst du
> getrost in der Rumpelkammer lassen. Die ist ganz gewöhnlicher
> Blödsinn.

Du solltest deinen Humorsensor einschalten.  Da stehen doch nicht
umsonst Anführungszeichen drumrum.

Trotzdem ist das Konzept z. B. eines UltraSPARC hier deutlich
angenehmer zu benutzen (und einfach genial, da die tatsächliche Anzahl
der Register ein Feature einer aktuellen Implementierung ist, der Code
ist unabhängig davon).

Eine IA32 mit einem AVR zu Vergleichen, das ist eher Äpfel mit
Tomaten...

> Bei 32-Bit kann man auf mehr Register getrost verzichten. Die
> zusätzlichen Adressiermöglichkeiten bügeln die ererbten Mängel
> vollständig aus.

Sieht irgendwie alles aus, wie immer mehr Knöpfe an die Backe
genäht.

Dass sich technisch Gutes nicht immer durchsetzt, weil der Markt
einfach nicht so funktioniert, wissen wir nicht erst seit dem 8086 und
seit VHS-Video.  Das hat eher was mit Marketing, Schmiergeldern, und
den richtigen Partnern beim Golf spielen zu tun...

> Und wenn du auf einer Vielregistermaschine eine Funktion aufrufst,
> mußt du auch entweder die aktuellen Register auf den Stack schieben,
> oder hinterher wieder laden.

Nö.  Guck dir mal eine UltraSPARC an.  Da wird nur ein Registerzeiger
um 8 weitergerückt.  Speicheroperationen werden erst vonnöten, wenn
die Aufruftiefe die Anzahl der physisch vorhandenen Register
überschreitet.  (Das gibt dann einen Trap, und das OS kann
entscheiden, wie viele Registerblöcke jetzt ein- oder ausgelagert
werden.)

> Mehr spaßeshalber habe ich auch schon den MSP430 mit ASM
> programmiert und mich zunächst über die vielen Register gefreut -
> nur um hinterher festzustellen, daß ich auch nicht mehr benutzt
> habe, als auf dem Pentium.

Ich weiß nicht, der GCC hat auf dem AVR keine Probleme, die Register
zu benutzen. ;-)  Wie geschrieben, Parameterübergabe über den Stack
ist dort die absolute Ausnahme.

Wir sind aber jetzt hinreichend weit weg vom Ursprungsthema, ich würde
diese Diskussion jetzt von meiner Seite nicht weiter fortführen.

von Uhu U. (uhu)


Lesenswert?

> Nö.  Guck dir mal eine UltraSPARC an.  Da wird nur ein Registerzeiger
> um 8 weitergerückt.

Das ist eigentlich eine Dreiadressmaschine, die alle Operanden in einem 
Befehl aus dem Speicher liest und das Ergebnis wieder dort abspeichert. 
Alles wird über ein einziges Pointerregister indiziert. Alle anderen 
'Register' sind reine Fiktion.

Letztlich ist das nur ein weiteres Beispiel dafür, daß man nicht 
unbedingt viele Prozessorregister braucht...

von Andreas K. (a-k)


Lesenswert?

> Das ist eigentlich eine Dreiadressmaschine, die alle Operanden in einem
> Befehl aus dem Speicher liest und das Ergebnis wieder dort abspeichert.

Theoretisch richtig, praktisch als Argument absurd, weil in diesem Sinn 
jedes Register als Speicherstelle betrachtet werden kann (und in der 
Theorie als erste Speicherhierarchie ja auch ist). Komisch zudem dass du 
das erst jetzt erwähnst, immerhin arbeitet Itanium auch nicht anders.

von Uhu U. (uhu)


Lesenswert?

Andreas Kaiser wrote:
>> Das ist eigentlich eine Dreiadressmaschine, die alle Operanden in einem
>> Befehl aus dem Speicher liest und das Ergebnis wieder dort abspeichert.
>
> Theoretisch richtig, praktisch als Argument absurd, weil in diesem Sinn
> jedes Register als Speicherstelle betrachtet werden kann (und in der
> Theorie als erste Speicherhierarchie ja auch ist).

Wieso absurd? Sagte ich nicht, daß in dieser Architektur alle Register
außer dem Pointer-Register Fiktion - d.h. 'gewöhnliche' Speicherzellen -
sind?

Ich schrieb, daß das ein weiteres Argument dafür ist, daß man nicht
einen Haufen Register braucht, sondern im Extremfall eben mit einem
indizierbaren auskommt.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Uhu Uhuhu wrote:

> Das ist eigentlich eine Dreiadressmaschine, die alle Operanden in einem
> Befehl aus dem Speicher liest und das Ergebnis wieder dort abspeichert.

Es ist nicht nur eigentlich eine Dreiadressmaschine (aber das trifft
auf alle echten RISCs zu).  Und nein, die Register sind dort (bis auf
die spill/chill traps) eben kein Speicher, sondern erheblich
schneller.  Darin liegt der Unterschied begraben.

OK, gut für diesen Thread.  Tschüss.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.