Permalänk
Skrivet av Yoshman:

Angående argumenten, inser att det finns ett ännu enklare sätt att separera allokering och ctor som även gör så att argumenten trillar på plats helt automatiskt.

  1. allokera minne endera på stacken, heapen eller custon new (som måste returnera pekare till utrymmet enligt C++ språkstandard), lägg adressen till minnet där första argumentet ska vara (this)

  2. stoppa dit alla argument som ctor tar

  3. anropa ctor, this pekar nu på objektutrymmet och resten av argument är där de förväntas vara

I detta läge kan man ha samma minnesallokeringslogik för alla typer oavsett antal argument till ctor och ctor behöver överhuvudtaget inte bry sig om hur minnet allokeras.

Beskriver du inte bara den vanliga ooptimerade modellen här, eller har jag missat något?

Skrivet av Yoshman:

Beskrivning du gör är typ omvänd Pascal-calling och det blir problem vid automatiska variabler då det är anropad funktion som allokerar utrymmet på stacken -> helt omöjligt att hoppa tillbaka till den som anropar på "normalt" sätt då returadress normalt ligger på stacken och den typen av instruktioner modifierar stackpekare (vars topp nu är utrymmet för this!). Även om man kommer tillbaka har nu den som anropar ett problem, den måste veta hur mycket minne ctor allokerade annars går det inte att fria minnet på korrekt sätt -> extra tillstånd som tar instruktioner och utrymme någonstans.

Jag kan inte alla detaljer. Jag har bara studerat den genererade koden.

Autoobjekten har fått sitt minne reserverat på stacken i caller och den adressen kommer in i this-pekaren. De behöver inte allokera minne i den optimerade konstruktorn. Det minne som reserverades på stacken kommer, precis som för alla andra auto-objekt på stacken, släppas när caller returnerar.

Det är bara för de dynamiskt allokerade objekten som konstruktorn allokerar minne. För dessa objekt kan kompilatorn ersätta anropen till operator new och det efterföljande konstruktoranropet till en kombinerad allokator+konstruktor funktion som både allokerar minne och konstruerar objektet. Signaleringen att minne behöver allokeras kan skötas på olika sätt, det enklaste är nog att this-pekaren är NULL, men man kan även ha en separat flagga. Denna kombinerade funktion kan även användas för konstruktion av auto-objekt. Då skickar man in adressen där objektet skall konstrueras och då anropar den kombinerade funktionen inte operator new utan konstruerar objektet i det minne som this-pekaren pekar ut. En och samma funktion kan anropas för villkorlig allokering och konstruktion objekt, oavsett om det är ett auto-objekt eller dynamiskt

Motsvarande trick går att göra destruktorsidan, men det kan man nog bara göra om destruktorn är virtuell. Annars vet man nog inte om det är en klasspecifik operator delete eller den globala som skall användas för släppa minnet. Återigen är det bara de dynamiskt allokerade objektens destruktorer som skall frigöra minnet och här behövs det nog en extra parameter som talar om om det skall ske.

Dessa optimeringar kan naturligtvis inte göras om man följer ett ABI som specar att det skall gå till på ett annat sätt.

Nu ger jag upp. Om detta inte räcker för att du skall acceptera att det går att göra kan vi avsluta den här diskussionen nu. Vi har kommit ganska långt OT...

Om du fortfarande inte tror att kompilatorerna kan skicka extra argument eller göra saker bakom ryggen på dig kan du fundera på olika sätt att lösa problemet med att konstruktorna för B och C måste veta om de skall anropa sin virtuella bas eller inte. Se exemplet nedan. Vilken av de olika lösningarna du kommer på är mest effektiv ur storlekssynpunkt.

class V { V(); } ; class B : virtual public V { B(); }; class C : virtual public V { C(); }; class D : public B, public C { D() : V(), B(), C() {} } void fun() { new C; // Här skall C::C() anropa V::V() new D; // Här skall B::b::() och C::C() inte anropa V::V() }

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Beskriver du inte bara den vanliga ooptimerade modellen här, eller har jag missat något?

Ser inte hur den skulle vara optimerad ur vare sig storlek eller hastighetssynpunkt. Total separation mellan processen att allokera minne från att initiera det genom att köra ctor, ingen specialhantering på något ställe gör det enklare att få koden väldigt liten då det är enklast möjliga logik.

Skrivet av Ingetledigtnamn:

Jag kan inte alla detaljer. Jag har bara studerat den genererade koden.

Autoobjekten har fått sitt minne reserverat på stacken i caller och den adressen kommer in i this-pekaren. De behöver inte allokera minne i den optimerade konstruktorn. Det minne som reserverades på stacken kommer, precis som för alla andra auto-objekt på stacken, släppas när caller returnerar.

Det är bara för de dynamiskt allokerade objekten som konstruktorn allokerar minne. För dessa objekt kan kompilatorn ersätta anropen till operator new och det efterföljande konstruktoranropet till en kombinerad allokator+konstruktor funktion som både allokerar minne och konstruerar objektet. Signaleringen att minne behöver allokeras kan skötas på olika sätt, det enklaste är nog att this-pekaren är NULL, men man kan även ha en separat flagga. Denna kombinerade funktion kan även användas för konstruktion av auto-objekt. Då skickar man in adressen där objektet skall konstrueras och då anropar den kombinerade funktionen inte operator new utan konstruerar objektet i det minne som this-pekaren pekar ut. En och samma funktion kan anropas för villkorlig allokering och konstruktion objekt, oavsett om det är ett auto-objekt eller dynamiskt

Tycker det låter komplicerat, en klass är en struktur med till typen associerade funktioner. Är därför trivialt att veta hur mycket minne som måste allokeras innan man anropar ctor. Varför då inte allokera minne först och sedan skicka this som första argument oavsett hur det minnet var allokerat? Finns gått om dåliga kompilatorer, så bara för att en viss kompilator genererar skum kod ska man inte se det som något facit. g++ och MSVC++ verkar göra något betydligt närmare det jag beskrivit när jag tittar på genererad kod och de producerar bra kod.

Skrivet av Ingetledigtnamn:

Motsvarande trick går att göra destruktorsidan, men det kan man nog bara göra om destruktorn är virtuell. Annars vet man nog inte om det är en klasspecifik operator delete eller den globala som skall användas för släppa minnet. Återigen är det bara de dynamiskt allokerade objektens destruktorer som skall frigöra minnet och här behövs det nog en extra parameter som talar om om det skall ske.

Dessa optimeringar kan naturligtvis inte göras om man följer ett ABI som specar att det skall gå till på ett annat sätt.

Svårt att hantera virtuella dtor på något annat sätt än vad ABI dikterar, icke virtuella anrop går däremot att helt optimera bort i lägen där man inte tar adressen på funktionen, funktionen är i samma "translation unit" (det som anropas och det som anropar hamnar i samma objektfil). I det läget behövs ingen ABI för det finns rent tekniskt inget anrop till en funktion -> hur man skickar argument är helt upp till kompilatorn. Noter att det inte påverkar det jag skrev i första posten då dessa optimeringar inte kan göras om man anropar funktionen indirekt (d.v.s via en pekare, alla virtuella anrop är indirekta likaså alla anrop i C som görs via funktionspekare).

Skrivet av Ingetledigtnamn:

Nu ger jag upp. Om detta inte räcker för att du skall acceptera att det går att göra kan vi avsluta den här diskussionen nu. Vi har kommit ganska långt OT...

Det vi diskuterar är fortfarande en konsekvens av det tråden handlar om. Anledningen att jag fortsätter är att det är fullt möjligt att det finns aspekter jag missat (har jobbat en lite granna med kompilatorer men det betyder knappast att man vet allt), än så länge tycker jag inte någon sådan framkommit.

Skrivet av Ingetledigtnamn:

Om du fortfarande inte tror att kompilatorerna kan skicka extra argument eller göra saker bakom ryggen på dig kan du fundera på olika sätt att lösa problemet med att konstruktorna för B och C måste veta om de skall anropa sin virtuella bas eller inte. Se exemplet nedan. Vilken av de olika lösningarna du kommer på är mest effektiv ur storlekssynpunkt.

class V { V(); } ; class B : virtual public V { B(); }; class C : virtual public V { C(); }; class D : public B, public C { D() : V(), B(), C() {} } void fun() { new C; // Här skall C::C() anropa V::V() new D; // Här skall B::b::() och C::C() inte anropa V::V() }

Så här skulle jag lösa detta problem:
Alla klasser som använder sig av virtuellt arv får i praktiken två ctors i assembler. Den ena är grundläggande och kör bara kroppen till sin typ, den anropar inte basklassens ctor. Den andra anropar först basklassens ctor och sedan den grundläggande ctor (så den består av väldigt få instruktioner).

new C anropas den andra av de två ctors (så ctor för V körs), enda argumentet som behövs är this
new D anropar sin ctor med this enda argument, D ctor anropar ctor för V följt av den grundläggande ctor för B och C (så ctor för V inte körs igen), this är det enda argumentet i alla anrop.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Ser inte hur den skulle vara optimerad ur vare sig storlek eller hastighetssynpunkt. Total separation mellan processen att allokera minne från att initiera det genom att köra ctor, ingen specialhantering på något ställe gör det enklare att få koden väldigt liten då det är enklast möjliga logik.

Jag skrev "ooptimerade", men så här i efterhand skulle jag naturligtvis valt formuleringen "icke-optimerade".

Skrivet av Yoshman:

Tycker det låter komplicerat, en klass är en struktur med till typen associerade funktioner. Är därför trivialt att veta hur mycket minne som måste allokeras innan man anropar ctor. Varför då inte allokera minne först och sedan skicka this som första argument oavsett hur det minnet var allokerat? Finns gått om dåliga kompilatorer, så bara för att en viss kompilator genererar skum kod ska man inte se det som något facit. g++ och MSVC++ verkar göra något betydligt närmare det jag beskrivit när jag tittar på genererad kod och de producerar bra kod.

Det är precis lika trivialt att veta hur mycket minne som skall allokeras om operator new-anropet sker i konstruktorn som om det sker utanför. Objektet är precis lika stort när man befinner sig i konstruktorn som när man är utanför och man vet ju typen på objektet man konstruerar. Varför envisas du med att det skulle vara svårt att göra allokeringen i konstruktorn?

Jag säger det igen. Anledningen till att man skulle vilja göra en sådan optimering är att minska kodstorleken. Färre (statiskt) antal funktionsanrop ger mindre kod -> koden ryms in mindre minne -> man kan få in mer funktionalitet i begränsat minne eller koden kan rymmas i en mindre MCU som är billigare. Men man får betala ett pris i form av komplexare kod och extra tester så det är bara värt att göra när man optimerar för minimal kodstorlek. Du får se det om en invers funktionsinlining. Vid inlining offrar man kodstorlek för att få snabbare kod. Här offrar man snabbhet för att få mindre kod. En annan kodstorleksoptimering är procedural abstraction, där kompilatorn letar efter gemensamma kodsekvenser och om de är tillräckligt långa bryts de ut till separata funktioner. Vinsten är att man bara behöver en kopia av den gemensamma koden, kostnade är ett extra funktionsanrop varje gång den skall exekveras. Du kanske tycker det låter helt befängt, men i embedded-industrin har kodstorlek varit mycket viktigt. Nu när det finns billiga ARM-MCUer är det inte riktigt lika viktigt längre, men mängden av on-chip-memory spelar fortfarande roll. Några cent hit eller dit gör skillnad när du tillverkar 100000 enheter.

Varken g++ eller MSVC har fokuserat på att generera liten kod. I desktop-världen är minne gratis. G++ följer väl dessutom IA64-ABIt på alla plattformar och där är det reglerat hur konstruktion skall skötas.

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Jag skrev "ooptimerade", men så här i efterhand skulle jag naturligtvis valt formuleringen "icke-optimerade".

Haha, skrev fel. Menade på vilket sätt det inte skulle vara optimalt att ha dessa två saker helt separerade. D.v.s. skulle hävda att det är optimerat.

Skrivet av Ingetledigtnamn:

Det är precis lika trivialt att veta hur mycket minne som skall allokeras om operator new-anropet sker i konstruktorn som om det sker utanför. Objektet är precis lika stort när man befinner sig i konstruktorn som när man är utanför och man vet ju typen på objektet man konstruerar. Varför envisas du med att det skulle vara svårt att göra allokeringen i konstruktorn?

Min poäng är att om man separerar de två processerna "allokera minne" och "kör ctor" så kan den senare vara exakt samma oavsett om det är en automatisk variabel, default "new" eller custom "new". Du kan inte lösa fallet där minnet ligger på stacken när anropet redan är gjort, det måste göras av den som anropar.

Skrivet av Ingetledigtnamn:

Jag säger det igen. Anledningen till att man skulle vilja göra en sådan optimering är att minska kodstorleken. Färre (statiskt) antal funktionsanrop ger mindre kod -> koden ryms in mindre minne -> man kan få in mer funktionalitet i begränsat minne eller koden kan rymmas i en mindre MCU som är billigare. Men man får betala ett pris i form av komplexare kod och extra tester så det är bara värt att göra när man optimerar för minimal kodstorlek. Du får se det om en invers funktionsinlining. Vid inlining offrar man kodstorlek för att få snabbare kod. Här offrar man snabbhet för att få mindre kod. En annan kodstorleksoptimering är procedural abstraction, där kompilatorn letar efter gemensamma kodsekvenser och om de är tillräckligt långa bryts de ut till separata funktioner. Vinsten är att man bara behöver en kopia av den gemensamma koden, kostnade är ett extra funktionsanrop varje gång den skall exekveras. Du kanske tycker det låter helt befängt, men i embedded-industrin har kodstorlek varit mycket viktigt. Nu när det finns billiga ARM-MCUer är det inte riktigt lika viktigt längre, men mängden av on-chip-memory spelar fortfarande roll. Några cent hit eller dit gör skillnad när du tillverkar 100000 enheter.

Och jag håller inte med om att det blir mindre kod om man stoppar in heap-allokatorn i koden för ctor, då måste man separera fallet att det är en automatisk variabel kontra heap/custom "new". Delen som allokerar minne kan ju vara "malloc", den funktionen kommer du ändå ha. Anropa "malloc" är 2 assembler instruktioner, stoppa in storleken där första argumentet ska vara enligt ABI, gör hopp. Eventuellt får man en till instruktion om returvärdet inte kommer där första argumentet ska vara (är t.ex. fallet på 32-bitars x86 och många enklare CPUer där argument går på stacken och returvärde kommer i register).

Det kostar minst två instruktioner att bara särskilja på fallet om det är en automatiskt variabel eller heap allokering, ovanpå det ska man ha kod för att göra heap-allokering, något som måste göras i varje ctor i det du föreslår. Ser inte hur det kan bli mindre kod än att hårt separera processen att allokera minne (och göra det först) från att köra ctor med this som argument.

Skrivet av Ingetledigtnamn:

Varken g++ eller MSVC har fokuserat på att generera liten kod. I desktop-världen är minne gratis. G++ följer väl dessutom IA64-ABIt på alla plattformar och där är det reglerat hur konstruktion skall skötas.

g++ med "-Os" genererar väldigt lite kod både på x86 och ARM. I ARM fallet kan du kombinera det med att generera Thumb/Thumb2 för att få riktigt litet kodsegment. ARM Cortex M3 och mindre är CPUer som kostar <$1 (M0 kostar run 10 cent om jag inte missminner mig), de har väldigt lite rå kraft och typiskt minimalt med flash/RAM. Finns ingen anledning att köra något annat än gcc/g++ på dessa ändå.

Folk använder ju en delar av gcc ända ner till Zilog 80 CPUer, (finns inget g++, men det beror mest på att C++ är helt enkelt inte lämpligt för så små enheter).

g++ kör med ARM ABI (kallas EABI på Linux för Embedded Application Binary Interface) på 32-bitars ARM, en snarlik variant på 64-bitars ARM, det är en ABI för 32-bitars x86 (alla argument på stacken där t.ex.) medan det är den ABI jag postat länkar på för 64-bitars x86.

Linux har stöd för ännu en ABI på 64-bitars x86 som kallas "x32 ABI". Där är pekare 32-bitars men man har access till alla AMD64/Intel64 specifika register och funktioner, dock finns det rätt få färdiga bibliotek för den ABIn men libc finns i alla fall (som både innehåller stdc och en rad POSIX specifika saker som trådar, sockets etc).

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Du kan inte lösa fallet där minnet ligger på stacken när anropet redan är gjort, det måste göras av den som anropar.

Detta är tredje gången jag skriver att konstruktorn INTE allokerar stack-minne. Eftersom du uppenbarligen inte läser vad jag skriver lägger jag av nu.

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Detta är tredje gången jag skriver att konstruktorn INTE allokerar stack-minne. Eftersom du uppenbarligen inte läser vad jag skriver lägger jag av nu.

Det jag menar är att man ändå måste ta hänsyn (då du varje gång sagt att det fallet hanteras genom att den som anropar allokerar i detta fall) till det fallet och det fallet blir då identiskt med det enda fall som behövs om minne alltid allokeras innan man kör ctor. Är därför jag överhuvudtaget inte kan se hur det skulle kunna bli mindre kod om man specialbehandlar fallet med heap-allokering inuti ctor, framförallt inte när det är logik som då måste replikeras till varje ctor.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Det jag menar är att man ändå måste ta hänsyn...

Det är inte riktigt den betydelsen jag lägger i "du kan inte lösa fallet".

Skrivet av Yoshman:

Är därför jag överhuvudtaget inte kan se hur det skulle kunna bli mindre kod om man specialbehandlar fallet med heap-allokering inuti ctor, framförallt inte när det är logik som då måste replikeras till varje ctor.

Allt beor på hur många new du gör till varje konstruktor (statiskt). Du byter ut N anrop till operator new mot att i konstruktorn göra ett test, ett villkorligt hopp, 1 anrop till operator new och en tilldelning. Om N blir tillräckligt stort är det väl klart att man går med vinst? Fler konstruktorer, samma ekvation per konstruktor. Om N == 1 är det torsk, men tror man att N oftast är större tjänar man på detta.

En annan variant är att skapa en konstruktor som ALLTID allokerar (ingen extra logik!) som bara anropas från de ställen där man gör new. Då kan man råka ut för att man får båda smakerna av konstruktor, men om N är tillräckligt stort är det vinst då också.

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Det är inte riktigt den betydelsen jag lägger i "du kan inte lösa fallet".

Det jag hela tiden skrivit är: man inte kan lösa fallet allokering på stacken inuti ctor, det måste ske av den som anropar (det är en konsekvens hur en stack fungerar och vad den används till av en CPU). Så det blir ett fall där man först allokerar minne på stacken och skickar utrymmet som this, ett annat fall där man allokerar på heap. Vart om minnet på heapen allokeras innan anropet eller inuti spelar inte så stor roll i isolation, men gör man det innan klarar man sig med en enda implementation av ctor som alltid får allokerat minnet som första argument (this).

Skrivet av Ingetledigtnamn:

Allt beor på hur många new du gör till varje konstruktor (statiskt). Du byter ut N anrop till operator new mot att i konstruktorn göra ett test, ett villkorligt hopp, 1 anrop till operator new och en tilldelning. Om N blir tillräckligt stort är det väl klart att man går med vinst? Fler konstruktorer, samma ekvation per konstruktor. Om N == 1 är det torsk, men tror man att N oftast är större tjänar man på detta.

En annan variant är att skapa en konstruktor som ALLTID allokerar (ingen extra logik!) som bara anropas från de ställen där man gör new. Då kan man råka ut för att man får båda smakerna av konstruktor, men om N är tillräckligt stort är det vinst då också.

Det blir alltid bara ett anrop för minnesallokering per typ, man allokerar sizeof(min_typ) som är den totala storleken man får av alla datamedlemmar i alla klasser som är med i arvshierarkin. Vissa typer har sina egna ctors så för dem gör man bara om processen rekursivt till det bara är primitiva typer.

Det blir då lika många anrop till minnesallokering vare sig man allokerar minne up-front eller inuti ctor, fast fallet där man gör det up-front hanterar allokering på stacken, från heap och från custom "new" med exakt samma kod för själva ctor.

Anta att fallet med automatiska variabler eller custom "new" inte existerar, i det läget blir fallen identiska sett till antal instruktioner så i det läget kvittar det om minnet allokeras up-front eller inuti ctor.

Så det är min invändning, varianten där man inte allokerar up-front är som bäst lika bra eller så är den både krångligare och kräver fler instruktioner. Processen att allokera minne från en heap är tillräckligt komplicerad att två/tre instruktioner för att göra det via ett funktionsanrop tjänar man igen mångdubbelt bara det är mer än ett anrop till funktionen.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Det jag hela tiden skrivit är: man inte kan lösa fallet allokering på stacken inuti ctor, det måste ske av den som anropar (det är en konsekvens hur en stack fungerar och vad den används till av en CPU). Så det blir ett fall där man först allokerar minne på stacken och skickar utrymmet som this, ett annat fall där man allokerar på heap. Vart om minnet på heapen allokeras innan anropet eller inuti spelar inte så stor roll i isolation, men gör man det innan klarar man sig med en enda implementation av ctor som alltid får allokerat minnet som första argument (this).

Det blir alltid bara ett anrop för minnesallokering per typ, man allokerar sizeof(min_typ) som är den totala storleken man får av alla datamedlemmar i alla klasser som är med i arvshierarkin. Vissa typer har sina egna ctors så för dem gör man bara om processen rekursivt till det bara är primitiva typer.

Det blir då lika många anrop till minnesallokering vare sig man allokerar minne up-front eller inuti ctor, fast fallet där man gör det up-front hanterar allokering på stacken, från heap och från custom "new" med exakt samma kod för själva ctor.

Anta att fallet med automatiska variabler eller custom "new" inte existerar, i det läget blir fallen identiska sett till antal instruktioner så i det läget kvittar det om minnet allokeras up-front eller inuti ctor.

Så det är min invändning, varianten där man inte allokerar up-front är som bäst lika bra eller så är den både krångligare och kräver fler instruktioner. Processen att allokera minne från en heap är tillräckligt komplicerad att två/tre instruktioner för att göra det via ett funktionsanrop tjänar man igen mångdubbelt bara det är mer än ett anrop till funktionen.

Nu skall vi ha räkneövning. Om jag kompilerar följande kodsnutt med GCC 4.8.2 (en kompilator som jag hoppas ingår i kategorin "vettiga" kompilatorer)

class A { public: A(); }; int test() { new A; new A; new A; new A; new A; }

Då får jag följande assemblerkod:

.text .globl _Z4testv .type _Z4testv, @function _Z4testv: .LFB0: .cfi_startproc .cfi_personality 0,__gxx_personality_v0 .cfi_lsda 0,.LLSDA0 pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 pushl %esi pushl %ebx subl $16, %esp .cfi_offset 6, -12 .cfi_offset 3, -16 movl $1, (%esp) .LEHB0: call _Znwj .LEHE0: movl %eax, %ebx movl %ebx, (%esp) .LEHB1: call _ZN1AC1Ev .LEHE1: movl $1, (%esp) .LEHB2: call _Znwj .LEHE2: movl %eax, %ebx movl %ebx, (%esp) .LEHB3: call _ZN1AC1Ev .LEHE3: movl $1, (%esp) .LEHB4: call _Znwj .LEHE4: movl %eax, %ebx movl %ebx, (%esp) .LEHB5: call _ZN1AC1Ev .LEHE5: movl $1, (%esp) .LEHB6: call _Znwj .LEHE6: movl %eax, %ebx movl %ebx, (%esp) .LEHB7: call _ZN1AC1Ev .LEHE7: movl $1, (%esp) .LEHB8: call _Znwj .LEHE8: movl %eax, %ebx movl %ebx, (%esp) .LEHB9: call _ZN1AC1Ev .LEHE9: jmp .L12 // Massa exception-crap bortklippt

Här hittar jag fem anrop till operator new (_Znwj) och fem anrop till konstruktorn (_ZN1AC1Ev). Hur rimmar det med "Det blir alltid bara ett anrop för minnesallokering per typ"? Jag förstår inte hur du har tänkt dig att det skall funka? Man måste allokera minne för alla objekt man skall konstruera!

Kan vi komma överens om att det krävs fem operator new-anrop för att dynamiskt konstruera fem objekt? Kan vi också enas om att koden krymper om vi kan ta bort dessa fem operator new-anropen i löpande kod och ersätta dem med ett i konstruktorn? Även om man utöver operator new-anropet måste lägga till ett test och ett villkorligt hopp.

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Nu skall vi ha räkneövning. Om jag kompilerar följande kodsnutt med GCC 4.8.2 (en kompilator som jag hoppas ingår i kategorin "vettiga" kompilatorer)

class A { public: A(); }; int test() { new A; new A; new A; new A; new A; }

Då får jag följande assemblerkod:

.text .globl _Z4testv .type _Z4testv, @function _Z4testv: .LFB0: .cfi_startproc .cfi_personality 0,__gxx_personality_v0 .cfi_lsda 0,.LLSDA0 pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 pushl %esi pushl %ebx subl $16, %esp .cfi_offset 6, -12 .cfi_offset 3, -16 movl $1, (%esp) .LEHB0: call _Znwj .LEHE0: movl %eax, %ebx movl %ebx, (%esp) .LEHB1: call _ZN1AC1Ev .LEHE1: movl $1, (%esp) .LEHB2: call _Znwj .LEHE2: movl %eax, %ebx movl %ebx, (%esp) .LEHB3: call _ZN1AC1Ev .LEHE3: movl $1, (%esp) .LEHB4: call _Znwj .LEHE4: movl %eax, %ebx movl %ebx, (%esp) .LEHB5: call _ZN1AC1Ev .LEHE5: movl $1, (%esp) .LEHB6: call _Znwj .LEHE6: movl %eax, %ebx movl %ebx, (%esp) .LEHB7: call _ZN1AC1Ev .LEHE7: movl $1, (%esp) .LEHB8: call _Znwj .LEHE8: movl %eax, %ebx movl %ebx, (%esp) .LEHB9: call _ZN1AC1Ev .LEHE9: jmp .L12 // Massa exception-crap bortklippt

Här hittar jag fem anrop till operator new (_Znwj) och fem anrop till konstruktorn (_ZN1AC1Ev). Hur rimmar det med "Det blir alltid bara ett anrop för minnesallokering per typ"? Jag förstår inte hur du har tänkt dig att det skall funka? Man måste allokera minne för alla objekt man skall konstruera!

Kan vi komma överens om att det krävs fem operator new-anrop för att dynamiskt konstruera fem objekt? Kan vi också enas om att koden krymper om vi kan ta bort dessa fem operator new-anropen i löpande kod och ersätta dem med ett i konstruktorn? Även om man utöver operator new-anropet måste lägga till ett test och ett villkorligt hopp.

Det jag menar är att det är ett anrop per typ per instans (det sista trodde jag var självklart, framförallt då jag skrev att man rekursivt måste anropa ctor för för sammansatta typer).

Visst, i detta fall sparar du två assembler instruktioner per skapad klass mot att ha två extra instruktioner i ctor + eventuell logik att hantera fallet att det är en automatisk variabel.

Vad du har förlorat är möjligheten att styra om minnet ska allokeras via heap eller via custom-new på alla klasser som använder denna teknik om du inte samtidigt bygger om allt. Men det kanske inte var så viktigt med ANSI/ISO C++ i detta läge eller?

Du har rätt i att man spar några anrop och vet från att programmerat 8-bitars mikrokontrollers att det ganska snabbt blir ont om minne. Har däremot aldrig varit med om att man kör med C++ på den typen av enheter, det har varit C och/eller assembler så hela diskussionen är akademiskt för när man kliver upp på ARM-baserade 32-bitars mikrokontrollers så lär ingen spara in två assemblerinstruktioner på något som frångår förväntat beteende (stöd automatiska variabler och custom new) i ett språk.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Det jag menar är att det är ett anrop per typ per instans (det sista trodde jag var självklart, framförallt då jag skrev att man rekursivt måste anropa ctor för för sammansatta typer).

Eftersom du upprepade gånger ifrågasatt att koden krymper om man gör så här (det är i mina ögon är en uppenbar storleksminskning och något som jag ser som självklart) vet jag inte längre vad som är självklart eller inte. Den enda förklaringen som jag kunde komma på till att du inte kom till samma slutsats, var att du möjligen kunde ha missat att det sker ett anrop per instans.

Skrivet av Yoshman:

Vad du har förlorat är möjligheten att styra om minnet ska allokeras via heap eller via custom-new på alla klasser som använder denna teknik om du inte samtidigt bygger om allt. Men det kanske inte var så viktigt med ANSI/ISO C++ i detta läge eller?

... frångår förväntat beteende (stöd automatiska variabler och custom new) i ett språk.

Nu förstår jag inte riktigt dina invändningar. Ursäkta om jag är övertydligt, men jag vet inte vad du har missat?

Om konstruktorn anpassas för att hantera normalfallet för dynamisk allokering (dvs konstruktorn innehåller ett villkorat anrop till klasspecifik operator new om den finns, annars den globala ::operator new, operator new anropas om this == NULL) kan då den optimerade konstruktorn hantera att det redan finns minne allokerat (variabler med static storage duration, vector new, subkonstruktorer, osv) om det skickas in i this-pekaren?

Kan den hantera fallet då minne finns reserverat på stacken (auto-variabler) om det skickas in i this-pekaren?

Kan den allokera och konstruera dynamiska objekt om man skickar NULL som this?

Om koden innehåller ett explicit anrop till ::operator new trots att det finns en operator new i klassen, kan då kompilatorn hantera detta genom att anropa ::operator new och skicka det minnet till konstruktorn i this?

På vilket sätt frångår detta ditt "förväntade beteende"? Jag trodde vi var överens om man kunde hantera auto utan problem (det var ett tag sedan den invändningen kom upp) och jag hoppas att jag här övertygat dig om att kompilatorn kan hantera båda operator new-fallen.

Låt oss ta ett steg till. Vad händer om man i koden redan har anropat operator new (eller Klass::operator new om det finns en sådan) och skickar in det minnet till konstruktorn? Den kommer inte att allokera nytt minne utan bete sig som den icke-optimerade konstruktorn. Se där. Den optimerade konstruktorn är faktiskt länkkompatibel med icke-optimerad kod.

För att sammanfatta: Jo, kompilatorn kan välja vilken operator new som används (är det det du menar när du skriver "möjligheten att styra om minnet ska allokeras via heap eller via custom-new"?) och, nej, man behöver inte kompilera allt. Även om man nu skulle behöva kompilera om allt (för det är väl det du menar när du skriver "bygger"?), vad har det med ANSI/ISO C++ att göra?