Wie weist der Compiler Speicher zu, ohne die Größe zur Kompilierzeit zu kennen?

Lesezeit: 8 Minuten

Benutzeravatar von Rahul
Raul

Ich habe ein C-Programm geschrieben, das ganzzahlige Eingaben vom Benutzer akzeptiert, die als Größe eines ganzzahligen Arrays verwendet werden, und mit diesem Wert deklariert es ein Array mit einer bestimmten Größe, und ich bestätige dies, indem ich die Größe des Arrays überprüfe.

Code:

#include <stdio.h>
int main(int argc, char const *argv[])
{
    int n;
    scanf("%d",&n);
    int k[n];
    printf("%ld",sizeof(k));
    return 0;
}

und überraschenderweise ist es richtig! Das Programm ist in der Lage, das Array der erforderlichen Größe zu erstellen.
Aber die gesamte statische Speicherzuweisung erfolgt zur Kompilierzeit, und während der Kompilierzeit wird der Wert von n ist nicht bekannt, wie kommt es also, dass der Compiler Speicher der erforderlichen Größe zuweisen kann?

Wenn wir den erforderlichen Speicher einfach so zuweisen können, wozu dient dann die dynamische Zuweisung? malloc() und calloc()?

  • Warum würden Sie das anstelle des normalen “k = (int *) calloc (n, sizeof (int));” tun? Nur um Ihren Code zu verschleiern?

    – jamesqf

    24. September 2017 um 17:51 Uhr

  • @jamesqf Wie ist int k[n]; eine verschleierte Version von k = (int *) calloc (n, sizeof (int));? Ich denke, ersteres ist besser lesbar (wenn Sie wissen, dass VLAs existieren).

    – jcai

    24. September 2017 um 19:38 Uhr

  • @jamesqf: Leistung. Mit n Eingeladen in rsi (bereit, das 2. Argument für printf in der x86-64 SysV ABI zu sein), sub rsp, rsi (eine einfache asm-Anweisung) ist viel billiger als ein Funktionsaufruf calloc. Obwohl in diesem Fall k[] selbst wird nicht verwendet, nur sizeof(k)sodass ein guter Compiler sich nicht die Mühe macht, Stapelplatz vor dem Aufruf zu reservieren printf. Stapelspeicher ist im L1D-Cache und im TLB bereits heiß, daher ist dies ein guter Ort für kleine Puffer. Es ist auch extrem billig, es zu veröffentlichen, und Sie können nicht alles falsch machen, weil der Compiler es für Sie erledigt.

    – Peter Cordes

    24. September 2017 um 23:21 Uhr


  • @jamesqf: Es überprüft nicht die Größe und schlägt nicht ordnungsgemäß fehl. Es ist Sache des Programmierers, keine Programme zu schreiben, die eine VLA verwenden, die zu groß für die Implementierungen ist, auf denen sie ausgeführt werden sollen. (z.B 8 MB Stapelgröße in neuen User-Space-Threads unter Linux x86-64). Im Allgemeinen segfaulten Sie, wenn Sie den Speicher unterhalb des Stapels berühren und das Betriebssystem entscheidet, dass dies zu viel ist, und Ihre Stapelzuordnung nicht vergrößert. Es ist keine gute Idee, eine große VLA in einer Nicht-Blatt-Funktion mit Kindern zu verwenden, die möglicherweise auch VLAs verwenden.

    – Peter Cordes

    25. September 2017 um 12:52 Uhr

  • @jamesqf: Das klingt, als wäre es viel schlimmer als new / delete, aber mit modernen Betriebssystemen, die Speicher überlasten, ist es kaum noch schlimmer. Sie können viel mehr RAM zuweisen, als das Betriebssystem über physischen RAM + Swap-Speicher verfügt, und wenn Sie alles berühren, kann dies dazu führen, dass der Kernel entscheidet, Ihren Prozess zu beenden. (Linux nennt dies den OOM-Killer). linuxdevcenter.com/pub/a/linux/2006/11/30/…. Sie können jedoch dafür sorgen, dass die Zuordnung ordnungsgemäß fehlschlägt, indem Sie Grenzen für die Menge an virtuellem Speicher festlegen, die ein Prozess zuweisen kann malloc wird tatsächlich NULL zurückgeben, aber dies ist nicht die Standardeinstellung.

    – Peter Cordes

    25. September 2017 um 13:02 Uhr

AnT steht mit Russlands Benutzer-Avatar
AnT steht zu Russland

Dies ist keine “statische Speicherzuweisung”. Ihr Array k ist ein Array mit variabler Länge (VLA), was bedeutet, dass Speicher für dieses Array zur Laufzeit zugewiesen wird. Die Größe wird durch den Laufzeitwert von bestimmt n.

Die Sprachspezifikation schreibt keinen bestimmten Zuordnungsmechanismus vor, aber in einer typischen Implementierung Ihren k wird in der Regel ein einfaches sein int * Zeiger mit dem tatsächlichen Speicherblock, der zur Laufzeit auf dem Stapel zugewiesen wird.

Für ein VLA sizeof -Operator wird auch zur Laufzeit ausgewertet, weshalb Sie in Ihrem Experiment den richtigen Wert daraus erhalten. Benutz einfach %zu (nicht %ld), um Werte vom Typ zu drucken size_t.

Der Hauptzweck von malloc (und andere dynamische Speicherzuweisungsfunktionen) besteht darin, die bereichsbasierten Lebensdauerregeln zu überschreiben, die für lokale Objekte gelten. Dh Speicher mit zugewiesen malloc bleibt “für immer” zugewiesen oder bis Sie die Zuweisung explizit aufheben free. Speicher allokiert mit malloc wird am Ende des Blocks nicht automatisch freigegeben.

VLA bietet wie in Ihrem Beispiel diese “Scope-Defeating” -Funktionalität nicht. Ihr Array k befolgt weiterhin reguläre bereichsbasierte Lebensdauerregeln: Seine Lebensdauer endet am Ende des Blocks. Aus diesem Grund kann VLA im Allgemeinen unmöglich ersetzen malloc und andere dynamische Speicherzuordnungsfunktionen.

Aber in bestimmten Fällen, wenn Sie den Bereich nicht “besiegen” und einfach verwenden müssen malloc Um ein Array in Laufzeitgröße zuzuweisen, könnte VLA tatsächlich als Ersatz für angesehen werden malloc. Denken Sie nur noch einmal daran, dass VLAs normalerweise auf dem Stapel zugewiesen werden und die Zuweisung großer Speicherblöcke auf dem Stapel bis heute eine ziemlich fragwürdige Programmierpraxis bleibt.

  • @Rahul: C wird nicht unterstützt static VLAs. Wenn n ist also ein Laufzeitwert static int k[n] ist nicht erlaubt. Aber selbst wenn es erlaubt wäre, würde es nicht jedes Mal einen neuen Speicherblock zuweisen. In der Zwischenzeit. malloc weist bei jedem Aufruf einen neuen Block zu. Also keine Ähnlichkeit mit malloc hier sogar mit static.

    – AnT steht zu Russland

    24. September 2017 um 6:26 Uhr


  • Die Formulierung in “in einer typischen Implementierung wird Ihr k am Ende ein einfacher int * -Zeiger sein” erscheint etwas riskant. Es gibt viel Verwirrung über Zeiger und Arrays, so wie sie sind.

    – Ilja Everilä

    24. September 2017 um 6:30 Uhr

  • @Rahul: VLA wurden im C99-Standard eingeführt. Formal gibt es sie jetzt seit etwa 18 Jahren. Natürlich brauchte die Compiler-Unterstützung einige Zeit.

    – AnT steht zu Russland

    24. September 2017 um 6:31 Uhr


  • Nun, ich auf jeden Fall würde nicht nennen Sie dies ein *int * pointer”, weil es hier kein solches C-Objekt gibt.

    – Antti Haapala – Слава Україні

    24. September 2017 um 7:52 Uhr

  • @rcgldr: VLA ist sehr ähnlich alloca und ist definitiv “inspiriert” von alloca. Jedoch, alloca weist Speicher mit “Funktions”-Lebensdauer zu: Der Speicher bleibt bestehen, bis die Funktion beendet wird. Dh es ignoriert alle Blockgrenzen außer der äußersten – dem Funktionskörper selbst. Inzwischen hat VLA eine normale blockbasierte Lebensdauer. In dieser Hinsicht unterscheidet sich VLA stark von alloca. Es stimmt, dass Visual Studio VLA bis heute nicht unterstützt. Allerdings ist es nichts wert, dass da C11 VLA ist Optional Merkmal der Sprache.

    – AnT steht zu Russland

    25. September 2017 um 3:52 Uhr


In C ist es Sache des Compilers, wie ein Compiler VLAs (Arrays mit variabler Länge) unterstützt – er muss es nicht verwenden malloc()und kann (und tut es oft) verwenden, was manchmal als “Stack” -Speicher bezeichnet wird – zB mit systemspezifischen Funktionen wie alloca() die nicht Teil von Standard-C sind. Wenn Stack verwendet wird, ist die maximale Größe eines Arrays normalerweise viel kleiner als dies mit möglich ist malloc()weil moderne Betriebssysteme Programmen ein viel kleineres Kontingent an Stack-Speicher zugestehen.

  • Moderne Betriebssysteme (und in einigen Fällen sogar alte und eingebettete Betriebssysteme) ermöglichen die Benutzerkonfiguration der Stapelgröße

    – MM

    24. September 2017 um 23:59 Uhr

Benutzeravatar von Plugwash
Plugwash

Speicher für Arrays mit variabler Länge kann eindeutig nicht statisch zugewiesen werden. Es kann jedoch auf dem Stack allokiert werden. Im Allgemeinen beinhaltet dies die Verwendung eines “Rahmenzeigers”, um den Ort des Funktionsstapelrahmens angesichts dynamisch bestimmter Änderungen des Stapelzeigers zu verfolgen.

Wenn ich versuche, Ihr Programm zu kompilieren, scheint es tatsächlich so zu sein, dass das Array mit variabler Länge optimiert wurde. Also habe ich Ihren Code geändert, um den Compiler zu zwingen, das Array tatsächlich zuzuweisen.

#include <stdio.h>
int main(int argc, char const *argv[])
{
    int n;
    scanf("%d",&n);
    int k[n];
    printf("%s %ld",k,sizeof(k));
    return 0;
}

Godbolt-Kompilierung für Arm mit gcc 6.3 (mit Arm, weil ich Arm-ASM lesen kann) kompiliert dies https://godbolt.org/g/5ZnHfa. (Kommentare von mir)

main:
        push    {fp, lr}      ; Save fp and lr on the stack
        add     fp, sp, #4    ; Create a "frame pointer" so we know where
                              ; our stack frame is even after applying a 
                              ; dynamic offset to the stack pointer.
        sub     sp, sp, #8    ; allocate 8 bytes on the stack (8 rather
                              ; than 4 due to ABI alignment
                              ; requirements)
        sub     r1, fp, #8    ; load r1 with a pointer to n
        ldr     r0, .L3       ; load pointer to format string for scanf
                              ; into r0
        bl      scanf         ; call scanf (arguments in r0 and r1)
        ldr     r2, [fp, #-8] ; load r2 with value of n
        ldr     r0, .L3+4     ; load pointer to format string for printf
                              ; into r0
        lsl     r2, r2, #2    ; multiply n by 4
        add     r3, r2, #10   ; add 10 to n*4 (not sure why it used 10,
                              ; 7 would seem sufficient)
        bic     r3, r3, #7    ; and clear the low bits so it is a
                              ; multiple of 8 (stack alignment again) 
        sub     sp, sp, r3    ; actually allocate the dynamic array on
                              ; the stack
        mov     r1, sp        ; store a pointer to the dynamic size array
                              ; in r1
        bl      printf        ; call printf (arguments in r0, r1 and r2)
        mov     r0, #0        ; set r0 to 0
        sub     sp, fp, #4    ; use the frame pointer to restore the
                              ; stack pointer
        pop     {fp, lr}      ; restore fp and lr
        bx      lr            ; return to the caller (return value in r0)
.L3:
        .word   .LC0
        .word   .LC1
.LC0:
        .ascii  "%d\000"
.LC1:
        .ascii  "%s %ld\000"

Der Speicher für dieses Konstrukt, das als “Variable-Length-Array”, VLA, bezeichnet wird, wird ähnlich wie auf dem Stapel zugewiesen alloca. Wie genau dies geschieht, hängt davon ab, welchen Compiler Sie verwenden, aber im Wesentlichen geht es darum, die Größe zu berechnen, wenn sie bekannt ist, und dann zu subtrahieren [1] die Gesamtgröße aus dem Stapelzeiger.

Du brauchst malloc und Freunde, weil diese Zuordnung “stirbt”, wenn Sie die Funktion verlassen. [And it’s not valid in standard C++]

[1] Für typische Prozessoren, die einen Stapel verwenden, der “gegen Null wächst”.

Wenn gesagt wird, dass der Compiler Speicher für Variablen bei zuweist Kompilierzeit, bedeutet dies, dass die Platzierung dieser Variablen festgelegt und in den ausführbaren Code eingebettet wird, den der Compiler generiert, und nicht, dass der Compiler Platz für sie bereitstellt, während er arbeitet. Die eigentliche dynamische Speicherallokation wird vom generierten Programm durchgeführt, wenn es läuft.

1415190cookie-checkWie weist der Compiler Speicher zu, ohne die Größe zur Kompilierzeit zu kennen?

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

Privacy policy