Gibt es Nachteile beim Übergeben von Strukturen nach Wert in C, anstatt einen Zeiger zu übergeben?

Lesezeit: 12 Minuten

Gibt es Nachteile beim Übergeben von Strukturen nach Wert in C, anstatt einen Zeiger zu übergeben?

Wenn die Struktur groß ist, gibt es offensichtlich den Leistungsaspekt des Kopierens vieler Daten, aber für eine kleinere Struktur sollte es im Grunde dasselbe sein, als würden mehrere Werte an eine Funktion übergeben.

Es ist vielleicht noch interessanter, wenn es als Rückgabewert verwendet wird. C hat nur einzelne Rückgabewerte von Funktionen, aber Sie brauchen oft mehrere. Eine einfache Lösung besteht also darin, sie in eine Struktur einzufügen und diese zurückzugeben.

Gibt es Gründe dafür oder dagegen?

Da es vielleicht nicht jedem klar ist, wovon ich hier spreche, gebe ich ein einfaches Beispiel.

Wenn Sie in C programmieren, werden Sie früher oder später anfangen, Funktionen zu schreiben, die so aussehen:

void examine_data(const char *ptr, size_t len)
{
    ...
}

char *p = ...;
size_t l = ...;
examine_data(p, l);

Das ist kein Problem. Das einzige Problem ist, dass Sie sich mit Ihrem Kollegen auf die Reihenfolge der Parameter einigen müssen, damit Sie in allen Funktionen dieselbe Konvention verwenden.

Aber was passiert, wenn Sie die gleiche Art von Informationen zurückgeben möchten? Sie erhalten normalerweise so etwas:

char *get_data(size_t *len);
{
    ...
    *len = ...datalen...;
    return ...data...;
}
size_t len;
char *p = get_data(&len);

Das funktioniert gut, ist aber wesentlich problematischer. Ein Rückgabewert ist ein Rückgabewert, außer dass dies in dieser Implementierung nicht der Fall ist. Aus dem oben Gesagten lässt sich die Funktion nicht ableiten get_data was nicht anschauen darf len verweist auf. Und es gibt nichts, was den Compiler dazu bringt, zu prüfen, ob ein Wert tatsächlich durch diesen Zeiger zurückgegeben wird. Wenn also im nächsten Monat jemand anders den Code modifiziert, ohne ihn richtig zu verstehen (weil er die Dokumentation nicht gelesen hat?), geht er kaputt, ohne dass es jemand merkt, oder er stürzt zufällig ab.

Die Lösung, die ich vorschlage, ist also die einfache Struktur

struct blob { char *ptr; size_t len; }

Die Beispiele können wie folgt umgeschrieben werden:

void examine_data(const struct blob data)
{
    ... use data.tr and data.len ...
}

struct blob = { .ptr = ..., .len = ... };
examine_data(blob);

struct blob get_data(void);
{
    ...
    return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();

Aus irgendeinem Grund denke ich, dass die meisten Menschen instinktiv machen würden examine_data Nehmen Sie einen Zeiger auf ein Struct-Blob, aber ich verstehe nicht, warum. Es bekommt immer noch einen Zeiger und eine Ganzzahl, es ist nur viel klarer, dass sie zusammenpassen. Und in der get_data In diesem Fall ist es unmöglich, auf die zuvor beschriebene Weise Fehler zu machen, da es keinen Eingabewert für die Länge gibt und eine Länge zurückgegeben werden muss.

  • Für was es wert ist, void examine data(const struct blob) ist falsch.

    – Chris Lutz

    26. September 2011 um 4:37 Uhr

  • Danke, habe es so geändert, dass es einen Variablennamen enthält.

    – dkagedal

    27. September 2011 um 11:47 Uhr

  • „Aus dem Obigen ist nicht ersichtlich, dass die Funktion get_data nicht nachsehen darf, worauf len zeigt. – das macht für mich überhaupt keinen Sinn (vielleicht weil Ihr Beispiel ungültiger Code ist, weil die letzten beiden Zeilen außerhalb einer Funktion erscheinen); kannst du das bitte näher erläutern?

    – Adam Spires

    11. April 2013 um 10:36 Uhr

  • Die beiden Zeilen unter der Funktion sollen veranschaulichen, wie die Funktion aufgerufen wird. Die Funktionssignatur gibt keinen Hinweis darauf, dass die Implementierung nur in den Zeiger schreiben soll. Und der Compiler hat keine Möglichkeit zu wissen, dass er überprüfen sollte, ob ein Wert in den Zeiger geschrieben wird, daher kann der Rückgabewertmechanismus nur in der Dokumentation beschrieben werden.

    – dkagedal

    28. Juli 2013 um 18:59 Uhr


  • Der Hauptgrund, warum Leute dies in C nicht häufiger tun, ist historisch. Vor C89, Sie konnte nicht Strukturen nach Wert übergeben oder zurückgeben, sodass alle Systemschnittstellen, die älter als C89 sind und dies logischerweise tun sollten (wie z gettimeofday) verwenden Sie stattdessen Zeiger, und die Leute nehmen das als Beispiel.

    – zol

    10. Dezember 2017 um 2:42 Uhr


Roddys Benutzeravatar
Roddy

Für kleine Strukturen (z. B. point, rect) ist die Wertübergabe durchaus akzeptabel. Aber abgesehen von der Geschwindigkeit gibt es noch einen weiteren Grund, warum Sie beim Übergeben/Rückgeben großer Strukturen nach Wert vorsichtig sein sollten: Stapelplatz.

Ein Großteil der C-Programmierung ist für eingebettete Systeme gedacht, bei denen der Speicher sehr wichtig ist und die Stapelgröße in KB oder sogar Bytes gemessen werden kann … Wenn Sie Strukturen nach Wert übergeben oder zurückgeben, werden Kopien dieser Strukturen platziert den Stack, was möglicherweise die Situation verursacht, nach der diese Site benannt ist …

Wenn ich eine Anwendung sehe, die eine übermäßige Stack-Nutzung zu haben scheint, sind Structs, die als Wert übergeben werden, eines der Dinge, nach denen ich zuerst suche.

  • “Wenn Sie Strukturen nach Wert übergeben oder zurückgeben, werden Kopien dieser Strukturen auf dem Stapel abgelegt”, würde ich anrufen hirntot jede Toolchain, die dies tut. Ja, es ist traurig, dass so viele es tun, aber es ist nichts, was der C-Standard verlangt. Ein vernünftiger Compiler wird alles optimieren.

    – Kuba hat Monica nicht vergessen

    1. Februar 2015 um 18:05 Uhr

  • @KubaOber Deshalb wird das nicht oft gemacht: stackoverflow.com/questions/552134/…

    – Roddy

    1. Februar 2015 um 22:15 Uhr

  • Gibt es eine definitive Linie, die eine kleine Struktur von einer großen Struktur trennt?

    – Josie Thompson

    16. Januar 2019 um 19:52 Uhr

  • Was ist mit den Auswirkungen auf die Leistung, wenn auf struct per Referenz innerhalb der Funktion zugegriffen werden muss, im Vergleich zum direkten Zugriff (ohne Referenz), wenn es als Wert übergeben wird? Ich meine, es sollte Leistungsvorteile geben, relativ kleine Strukturen nach Wert zu übergeben.

    – Illya S

    21. August 2020 um 10:46 Uhr

Benutzeravatar von tonylo
Toni

Ein Grund, dies nicht zu tun, der nicht erwähnt wurde, ist, dass dies ein Problem verursachen kann, bei dem die Binärkompatibilität von Bedeutung ist.

Je nach verwendetem Compiler können Strukturen je nach Compileroptionen/Implementierung über den Stack oder Register übergeben werden

Sehen: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-struct-return

-freg-struct-return

Wenn zwei Compiler sich nicht einig sind, können die Dinge explodieren. Unnötig zu erwähnen, dass die Hauptgründe dafür, dies nicht zu tun, Stapelverbrauchs- und Leistungsgründe sind.

  • Das war die Art von Antwort, nach der ich gesucht hatte.

    – dkagedal

    3. Oktober 2008 um 22:13 Uhr

  • Stimmt, aber diese Optionen beziehen sich nicht auf Pass-by-Value. sie beziehen sich auf Rückkehr Strukturen, was eine ganz andere Sache ist. Das Zurückgeben von Dingen als Referenz ist normalerweise ein sicherer Weg, sich selbst in beide Füße zu schießen. int &bar() { int f; int &j(f); return j;};

    – Roddy

    8. Dezember 2011 um 10:17 Uhr

Zu Ja wirklich Um diese Frage zu beantworten, muss man tief ins Montageland graben:

(Das folgende Beispiel verwendet gcc auf x86_64. Jeder kann gerne andere Architekturen wie MSVC, ARM usw. hinzufügen.)

Nehmen wir unser Beispielprogramm:

// foo.c

typedef struct
{
    double x, y;
} point;

void give_two_doubles(double * x, double * y)
{
    *x = 1.0;
    *y = 2.0;
}

point give_point()
{
    point a = {1.0, 2.0};
    return a;
}

int main()
{
    return 0;
}

Kompilieren Sie es mit vollständigen Optimierungen

gcc -Wall -O3 foo.c -o foo

Betrachten Sie die Baugruppe:

objdump -d foo | vim -

Das bekommen wir:

0000000000400480 <give_two_doubles>:
    400480: 48 ba 00 00 00 00 00    mov    $0x3ff0000000000000,%rdx
    400487: 00 f0 3f 
    40048a: 48 b8 00 00 00 00 00    mov    $0x4000000000000000,%rax
    400491: 00 00 40 
    400494: 48 89 17                mov    %rdx,(%rdi)
    400497: 48 89 06                mov    %rax,(%rsi)
    40049a: c3                      retq   
    40049b: 0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

00000000004004a0 <give_point>:
    4004a0: 66 0f 28 05 28 01 00    movapd 0x128(%rip),%xmm0
    4004a7: 00 
    4004a8: 66 0f 29 44 24 e8       movapd %xmm0,-0x18(%rsp)
    4004ae: f2 0f 10 05 12 01 00    movsd  0x112(%rip),%xmm0
    4004b5: 00 
    4004b6: f2 0f 10 4c 24 f0       movsd  -0x10(%rsp),%xmm1
    4004bc: c3                      retq   
    4004bd: 0f 1f 00                nopl   (%rax)

Ausgenommen die nopl Pads, give_two_doubles() hat dabei 27 Bytes give_point() hat 29 Bytes. Auf der anderen Seite, give_point() ergibt eine Anweisung weniger als give_two_doubles()

Interessant ist, dass wir feststellen, dass der Compiler optimieren konnte mov in die schnelleren SSE2-Varianten movapd und movsd. Außerdem, give_two_doubles() verschiebt tatsächlich Daten in den und aus dem Speicher, was die Dinge langsam macht.

Anscheinend ist vieles davon in eingebetteten Umgebungen nicht anwendbar (wo das Spielfeld für C heutzutage meistens ist). Ich bin kein Montage-Zauberer, daher wären Kommentare willkommen!

  • Das Zählen der Anzahl der Anweisungen ist nicht so interessant, es sei denn, Sie können einen großen Unterschied aufzeigen oder interessantere Aspekte wie die Anzahl schwer vorhersehbarer Sprünge usw. zählen. Die tatsächlichen Leistungseigenschaften sind viel subtiler als die Anzahl der Anweisungen .

    – dkagedal

    8. August 2010 um 21:45 Uhr

  • @dkagedal: Stimmt. Im Nachhinein denke ich, dass meine eigene Antwort sehr schlecht geschrieben war. Obwohl ich mich nicht sehr auf die Anzahl der Anweisungen konzentriert habe (keine Ahnung, was Ihnen diesen Eindruck vermittelt hat :P), war der eigentliche Punkt, den es zu beachten gilt, dass die Übergabe von Strukturen nach Wert der Übergabe von Referenzen für kleine Typen vorzuziehen ist. Wie auch immer, die Übergabe nach Wert wird bevorzugt, weil es einfacher ist (kein lebenslanges Jonglieren, keine Notwendigkeit, sich Sorgen zu machen, dass jemand Ihre Daten ändert oder const die ganze Zeit) und ich habe festgestellt, dass es keine großen Leistungseinbußen (wenn nicht sogar einen Gewinn) beim Pass-by-Value-Kopieren gibt, im Gegensatz zu dem, was viele vielleicht glauben.

    – kizzx2

    9. August 2010 um 5:44 Uhr


Meckis Benutzeravatar
Mecki

Eine Sache, die die Leute hier bisher vergessen haben zu erwähnen (oder ich übersehen habe), ist, dass Strukturen normalerweise eine Polsterung haben!

struct {
  short a;
  char b;
  short c;
  char d;
}

Jedes Zeichen ist 1 Byte groß, jeder Kurzschluss 2 Byte. Wie groß ist die Struktur? Nein, es sind keine 6 Bytes. Zumindest nicht auf gängigen Systemen. Auf den meisten Systemen wird es 8 sein. Das Problem ist, dass die Ausrichtung nicht konstant ist, sondern systemabhängig ist, sodass dieselbe Struktur auf verschiedenen Systemen unterschiedliche Ausrichtung und unterschiedliche Größen hat.

Nicht nur, dass Padding Ihren Stack weiter auffrisst, es fügt auch die Unsicherheit hinzu, dass Sie das Padding nicht im Voraus vorhersagen können, es sei denn, Sie wissen, wie Ihr System polstert, und sehen sich dann jede einzelne Struktur an, die Sie in Ihrer App haben, und berechnen die Größe dafür. Das Übergeben eines Zeigers nimmt eine vorhersagbare Menge an Platz ein – es gibt keine Unsicherheit. Die Größe eines Zeigers ist dem System bekannt, sie ist immer gleich, egal wie die Struktur aussieht und Zeigergrößen werden immer so gewählt, dass sie ausgerichtet sind und kein Padding benötigen.

Benutzeravatar von Ilya
Ilja

Einfache Lösung wird ein Fehlercode als Rückgabewert und alles andere als Parameter in der Funktion zurückgegeben,
Dieser Parameter kann natürlich eine Struktur sein, aber ich sehe keinen besonderen Vorteil, wenn Sie dies als Wert übergeben, sondern nur einen Zeiger senden.
Das Übergeben von Strukturen nach Werten ist gefährlich. Sie müssen sehr vorsichtig sein, was Sie übergeben. Denken Sie daran, dass es in C keinen Kopierkonstruktor gibt. Wenn einer der Strukturparameter ein Zeiger ist, wird der Zeigerwert kopiert. Dies kann sehr verwirrend und schwierig sein pflegen.

Nur um die Antwort zu vervollständigen (voller Kredit an Roddy), ist die Stack-Nutzung ein weiterer Grund, warum die Struktur nicht nach Wert übergeben wird. Glauben Sie mir, das Debuggen des Stack-Überlaufs ist echte PITA.

Wiederholung zum Kommentieren:

Das Übergeben von Struct by Pointer bedeutet, dass eine Entität Eigentümer dieses Objekts ist und genau weiß, was und wann freigegeben werden sollte. Das Übergeben von Struct nach Wert erzeugt versteckte Verweise auf die internen Daten von Struct (Zeiger auf andere Strukturen usw.), da dies schwer zu pflegen ist (möglich, aber warum?).

  • Aber das Übergeben eines Zeigers ist nicht “gefährlicher”, nur weil Sie ihn in eine Struktur einfügen, also kaufe ich ihn nicht.

    – dkagedal

    2. Oktober 2008 um 11:47 Uhr

  • Toller Punkt beim Kopieren einer Struktur, die einen Zeiger enthält. Dieser Punkt ist möglicherweise nicht sehr offensichtlich. Für diejenigen, die nicht wissen, worauf er sich bezieht, führen Sie eine Suche nach Deep Copy vs. Shallow Copy durch.

    – zooropa

    29. April 2009 um 12:21 Uhr

  • Eine der C-Funktionskonventionen besteht darin, Ausgabeparameter zuerst vor Eingabeparametern aufzulisten, z. B. int func(char* out, char *in);

    – zooropa

    29. April 2009 um 12:29 Uhr

  • Sie meinen, wie zum Beispiel getaddrinfo() den Ausgabeparameter zuletzt setzt? 🙂 Es gibt tausend Konventionen, und Sie können wählen, welche Sie wollen.

    – dkagedal

    28. Juli 2013 um 19:08 Uhr

Hier ist etwas, das niemand erwähnt hat:

void examine_data(const char *c, size_t l)
{
    c[0] = 'l'; // compiler error
}

void examine_data(const struct blob blob)
{
    blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime
}

Mitglieder einer const struct sind constaber wenn dieses Mitglied ein Zeiger ist (wie char *), es wird char *const eher als das const char * wir wollen wirklich. Natürlich könnten wir davon ausgehen, dass die const ist eine Dokumentation der Absicht, und dass jeder, der dagegen verstößt, schlechten Code schreibt (was sie sind), aber das ist für einige nicht gut genug (insbesondere für diejenigen, die gerade vier Stunden damit verbracht haben, die Ursache eines Absturzes aufzuspüren).

Die Alternative könnte sein, eine zu machen struct const_blob { const char *c; size_t l } und verwenden Sie das, aber das ist ziemlich chaotisch – es gerät in das gleiche Namensschema-Problem, mit dem ich habe typedefHinweise. Daher halten sich die meisten Leute daran, nur zwei Parameter zu haben (oder, was in diesem Fall wahrscheinlicher ist, eine String-Bibliothek zu verwenden).

  • Aber das Übergeben eines Zeigers ist nicht “gefährlicher”, nur weil Sie ihn in eine Struktur einfügen, also kaufe ich ihn nicht.

    – dkagedal

    2. Oktober 2008 um 11:47 Uhr

  • Toller Punkt beim Kopieren einer Struktur, die einen Zeiger enthält. Dieser Punkt ist möglicherweise nicht sehr offensichtlich. Für diejenigen, die nicht wissen, worauf er sich bezieht, führen Sie eine Suche nach Deep Copy vs. Shallow Copy durch.

    – zooropa

    29. April 2009 um 12:21 Uhr

  • Eine der C-Funktionskonventionen besteht darin, Ausgabeparameter zuerst vor Eingabeparametern aufzulisten, z. B. int func(char* out, char *in);

    – zooropa

    29. April 2009 um 12:29 Uhr

  • Sie meinen, wie zum Beispiel getaddrinfo() den Ausgabeparameter zuletzt setzt? 🙂 Es gibt tausend Konventionen, und Sie können wählen, welche Sie wollen.

    – dkagedal

    28. Juli 2013 um 19:08 Uhr

Darrons Benutzeravatar
Darron

Ich denke, dass Ihre Frage die Dinge ziemlich gut zusammengefasst hat.

Ein weiterer Vorteil der Übergabe von Strukturen nach Wert besteht darin, dass der Speicherbesitz explizit ist. Es muss nicht gefragt werden, ob die Struktur vom Heap stammt und wer die Verantwortung dafür trägt, sie zu befreien.

1425290cookie-checkGibt es Nachteile beim Übergeben von Strukturen nach Wert in C, anstatt einen Zeiger zu übergeben?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy