Bjarne Stroustrup: Lærebøger om C++ er elendige

Da C++-faderen Bjarne Stroustrup fik læst nogle undervisningsbøger om C++, blev han så gal, at han besluttede at skrive sin egen. Kvaliteten af undervisningen i C++ er faldet drastisk, mener han.

Programmeringssproget C++ er altså ikke så svært at bruge, som det har fået ry for. Problemet er, at folk tror, man skal lære det hele, og det er ikke meningen.

Sådan lød det fra danske Bjarne Stroustrup, manden bag C++, da han var med til at lancere det nyt C++-værktøj Embarcadero C++ Builder XE3. Det skriver The Register.

Omvendt forstår Bjarne Stroustrup godt, hvorfor mange finder C++ svært at komme i lag med. For den undervisning, man kan få i sproget, er helt elendig, sagde han.

»I cirka tre uger sad jeg og læste lærebøger i C++, med røg ud af ørene, mens jeg mumlede ’hvis det er C++, så kan jeg heller ikke lide det’,« sagde han, ifølge The Register.

De seneste 10-15 år er lærebøgerne gået fra at undervise i programmering med C++ til bare at gennemgå funktionerne i sproget, mener han, så derfor valgte han i stedet at skrive sin egen lærebog.

Skal han give gode råd til de mange, som bruger C++ rundt omkring, er det først og fremmest at droppe den massive brug af nedarvning. Uanset om andre sprog bruger nedarvning hele tiden, hører det ikke hjemme i C++, lød det fra sprogets fader.

Også brugen af makroer skal ned, hvis Bjarne Stroustrup kunne bestemme. Jo flere, der er, jo sværere er koden at vedligeholde, og derfor skal man prøve at få dem væk, for i C++ er der bedre alternativer, mener han.

C++ har siden 1998 været en standard, og sproget bliver dermed styret af mange forskellige firmaer og personer på én gang. Den udvikling var Bjarne Stroustrup i første omgang meget i mod, men efterfølgende er han glad for, at C++ gik standard-vejen, fortalte han.

»Hvis du vil have et sprog, som er meget udbredt, har du brug for en eller anden form for standard. Når sprog, som er ejet af en enkelt virksomhed, vokser, vil de før eller senere begynde at gå i mærkelige retninger, som skal understøtte denne virksomheds forretningsplan. Virksomhedens allierede vil blive tilgodeset,« sagde Bjarne Stroustrup.

Kommentarer (52)

Jens Madsen

Jeg er helt enig med Bjarne i, at C++ ikke altid bruges fornuftigt. Pludseligt, går det måske "mode" i nedarvning, og så skal alt nedarves.

Men desværre, har C++ også mange fejl. Og det er et forældet sprog. Måske netop derfor, er der ingen dygtige skribenter, der gidder beskæftige sig med sproget.

Så i stedet for, at skrive en ny bog - så skulle Bjarne måske overveje et nyt sprog? Eller, forbedre C++, så det får et ekstra plus.

Et af de store ulemper er pointere. Det gælder både C, og C++. I Java, har man søgt det løst, med automatiske oprydere - der efter min opfattelse, viser en total mangel på forståelse af problemet. Problemet med pointere, er at de er ustrukturerede. Og oprydning, er kun en symptom løsning, på dette problemet. Den korrekte måde er, at der også er struktur, i dynamiske data, der er i sproget. Så selvom det er data på en heap, skal de også struktureres, og være allokeret i strukturer. Jeg kunne godt ønske, at dem der udvikler programmeringssprog, fik et grundlæggende datalogi kursus, så de var i stand til, at kunne gennemskue analyserbarhed af kode, og i stand til, at f.eks. kunne oversætte deres kode, til en parallel struktur, analysere dataafhængigheder, og få forståelse for, hvordan der er sammenhæng, imellem, at en compiler kan behandle kode, og et sprogs evne til at kunne paralleliseres automatisk. Med andre ord, skal de som minimum, kunne oversætte deres sprog, til en dataflow graf, helt ned på elementært bit niveau, eller assembler niveau, således de får en forståelse for selve flowet, og de skal kunne oversætte en dataflow graf, fra et sekventielt program, til en fuld parallel struktur, og have forståelse for de problemer, det giver, f.eks. med hensyn til den tvungne trådning som opstår, og begrænsningerne i de ombytninger som det fører til. De skal have styr på begreber som determinisme i programmering, og kunne gennemskue, om de strukturer de tillader, er deterministiske, eller om sproget i visse tilfælde, kan tillade kode, der medfører resultater, der ikke er definerbare, og som f.eks. afhænger af kodens måde, eller rækkefølge, at blive oversat. Og de skal overveje, om det er noget, som kan testes for, og hvordan sproget skal designes, så test kan indføres, og er muligt.

Konklussion: Der er ikke brug for en ny C++ bog. Der er brug for et nyt sprog. Og hvis det er godt, skal der nok komme gode bøger.

Måske, kan det være bagud kompatibelt til C++, hvis alle sikkerhedstjek slås fra. Måske skal også overvejes et nyt navn. Selvom "c++" er tilladt i C++, så virker navnet lidt til grin - det er jo en af de konstruktioner fra C, som udfra et datalogisk synspunkt, er meget dårlig, fordi den kan lede til resultater, der giver ikke determinisme, og samtidigt, er det også ofte svært at overskue.

Idag, kan man ikke sige, at funktionen c++, giver mere optimal kode, da compilerne er blevet så gode, at de kan analysere koden, og nemt give ligeså optimal kode, selvom det f.eks. skrives med for/next, eller hvor c++ alene bruges som procedure, og ikke som funktion. Det væsentlige i et programmeringssprog, er overskuelighed, og om det er analyserbart, deterministisk, og sikkert.

Lars Tørnes Hansen

De nye sprog er her allerede: Det er C# og Java.

Det man skulle kigge på er måske at det nogle gange kunne være fordelagtigt at bruge et mix af programmeringssprog til en given opgave for at spare udviklertid (=penge til løn).
F.eks. kan et desktop program der skal lave noget systemnært have en server-klient arkitektur, hvor en serveren fint kan skrives i C++, og klienten enten i C# eller java.
Kommunikation kan ske f.eks. over TCP/IP på localhost (127.0.0.1), og på Linux/BSD/UNIX'er også over UNIX domain sockets1.
Er man lidt smart bruger man værktøjer der generere kode (et API) på begge sider, som kan pakke data ind og ud (marshalling).2

Jens Madsen

Nye sprog er der mange af - og netop det, får naturligvis skribenterne til, at gå bort fra C++. Men jeg er ikke enig i, at hverken Java, eller C# er "løsningen". Så derfor, tror jeg sagtens, at vi kunne bruge en ny c++ fra Bjarne.

Ofte, kunne være praktisk, og "kombinere" flere sprog. Måske kunne et sprog, være mere fleksibelt end C++, således at et sprog, udgør en "type" i sproget. Det vil muliggøre, at forskellige sprog, kan inkludderes som plugins. Som nævnt i en revy - hvorfor ikke, kunne overloade semikolon operatoren? Ja, hvor meget skal der egentligt til, før vi kan definere et helt nyt sprog i sproget? Eller implementere sprog som Pascal, C#, Basic, og Java, som "plugin"?

På et tidspunkt, må vi dog nok antage, at vi derved mister overblikket. Masser af programmører vil vælge, at løse deres opgaver, ved at udvikle et sprog til opgaven. Og programmet, vil blive smækfyldt, af bunker af nye sprog, lavet som plugins - hver programmør, med sit bibliotek, og sit yndlingssprog, som de selv definerer.

Ingen tvivl om, at jeg allerede nu, mister "lysten" til at finde fejl i programmerne... Og, det bliver nok god grund til en bog, af Bjarne, der giver retningslinier for, hvordan det skal (må) bruges.

Jens Madsen

C++ har siden 1998 været en standard, og sproget bliver dermed styret af mange forskellige firmaer og personer på én gang. Den udvikling var Bjarne Stroustrup i første omgang meget i mod, men efterfølgende er han glad for, at C++ gik standard-vejen, fortalte han.

Personligt, syntes jeg, at C++ er "for meget" standardiseret. F.eks. findes i sproget typer som int, der i virkeligheden er en "maskinær" type. Andre sprog, som Pascal, undgår helst typer, som arves fra det maskinære niveau. Datalogi, er en matematisk videnskab, der ikke må "arve" noget fra det maskinære niveau. F.eks. må matematik, ikke indeholde fysiske konstanter, som avogadros konstant. Og tilsvarende, bør programmeringssprog, ikke indeholde typer som byte, og integer. Disse er maskinære typer, der ikke hører hjemme i programmering. Da vi lærte datalogi, måtte vi ikke bruge integer - i stedet, skulle vi definere intervallet, som vi ønskede at bruge. Integer, var en maskinær type, der ikke hørte hjemme i højniveau programmering. Samtidigt, er også et krav, at de typer der kan defineres, f.eks. hvis vi definerer en type, udfra et interval, at dette interval, så må være uendeligt stort - der er, datalogisk set, intet minimum, og intet maksimum.

Peter Jespersen

Det er godt nok efterhånden et stykke tid siden jeg sidst rodede med Java - men så hut jeg hvisker fandt jeg Javas abstration fra pointere yderst glimrende. Det vigtigste er vel at eliminere den ekstreme fejlkilde som pointere reelt er.

Lars Tørnes Hansen

Som en ny C++ kan det måske være at du synes D i version 2 (D2) måske er interessant: http://en.wikipedia.org/wiki/D_%28programming_language%29

Kig evt her:
http://dlang.org/comparison.html
D2 language reference: http://dlang.org/lex.html
D2 runtime bibliotek (Phobos): http://dlang.org/phobos/index.html
Et andet udbredt 3. parts bibliotek hedder "Tango for D2".

Jeg synes D2 ser meget fornuftigt ud som programmeringssprog. Faktisk har jeg også brugt det.

Jens Madsen

Som en ny C++ kan det måske være at du synes D i version 2 (D2) måske er interessant: http://en.wikipedia.org/wiki/D_%28programming_language%29

Kig evt her:
http://dlang.org/comparison.html
D2 language reference: http://dlang.org/lex.html
D2 runtime bibliotek (Phobos): http://dlang.org/phobos/index.html
Et andet udbredt 3. parts bibliotek hedder "Tango for D2".

Jeg synes D2 ser meget fornuftigt ud som programmeringssprog. Faktisk har jeg også brugt det.

Har læst om D på wiki (brugt 3 minutter). Min umiddelbare vurdering, er bedre - men ikke perfekt. Ser vi på "overskrifterne" lever det op til mange af mine krav. Men det som for mig er det største krav, er den datalogiske forståelse, under det hele. Og den kan jeg ikke helt spore.

Det er lidt svært, at formulere sig kort, hvis jeg skal forklare problemet - desværre. Det kræver en grundig gennemgang, af hele problematikken omkring f.eks. pointere, parallelisme osv. At forstå, hvordan at sekventiel kode kan paralleliseres, og hvorfor det er vigtigt at vide, hvilken sammenhæng det har med at have frihed til at ombytte instruktioner, og at optimere kode osv. Som eksempel, er vigtigt, at forstå hvilket problem, at en pointer giver. Og, hvordan vi løser det.

Det medfører blandt andet, at vi ikke mere kan tillade os, at ikke strukturere vores dynamiske data. Vi ved alle, at der er struktur i statiske data. Men de dynamiske er ikke ordentligt strukturerede. De giver anledning, til mangel på analyserbarhed. For at opnå, at vi får struktur på data, er nødvendigt, at dynamiske data, erklæres på samme måde som statiske. Altså, at vi ikke bruger "new", "allocate" og den slags C ordre, til at oprette data. De skal have skuffer, og der skal være steder, de skal placeres.

Dette gør også, at vi f.eks. ikke får problem med ring-strukturer, hvor pointere peger på sig selv, og vi skal have en opryder, der finder ud af det. For, vi har en peger, der "holder" data. Det, at "holde" data, altså at indeholde data, er en helt anden problemstilling, end at pege på data. En peger, og en dataholder, er to forskellige ting.

For at definere dataholderne, så defineres de, i strukturer. Præcis, som i C++. Men, vi anvender kompakte strukturer. En kompakt struktur, er defineret ved, at den intet fylder, før noget puttes i. I princippet, så er den bare en pointer, indtil vi skriver noget ind. Så peger pegeren på data. Denne peger, er ikke bare en peger - den er en dataholder. Fordi, at det er den som "holder" data. Den er dataansvarlig. Slettes den, så er dine data væk. De dataansvarlige pointere, er altså defineret i strukturer.

Hvis du definerer et træ, f.eks. et rød-sort træ, så gøres det, på omtrent samme måde som normal. Men den virker som et statisk træ, der er uendeligt stor, fordi den er rekursiv. For at den ikke skal fylde uendeligt, anvender vi kompakte strukturer. Så der er pointere, der reelt peger på data. Det betyder, at vi rekursivt kan definere et træ. Vi har også adgang til, at kunne få data til at gå op, eller ned i træet. Altså, at swappe pointere, i vores rekursive struktur. Det viser sig, at være en operation, som ikke giver problemer analyseteknisk. Og den er samtidigt ekstrem praktisk, når du skal håndtere træer. Vi kan forstå det sådan, at vores data kan avancere til højere niveau, eller degradere til et lavere niveau i et træ. Det svarer til, at du ombytter data. Træerne, kan også selv initialisere en peger, til det øvre niveau, såfremt at det typemæssigt kan opnås.

Du får to typer pointere. Der er de ansvarlige, som er defineret i strukturerede træer. Du kan gå ind og aflæse, hvor meget hukommelse, at en sådan træ bruger - altså, du kan se den dynamiske hukommelse. Hvis en programmør, placerer alle sit dynamiske hukommelsesforbrug i samme klasse, eller laver en overordnet klasse/struct, som hæfter det sammen, så har du mulighed for, at se hvor mange elementer der er i strukturen, hvor meget hukommelse den fylder, og hvor "dyb" strukturen er. Hvor dyb den er, kan være væsentligt, for at sikre, at programmøren altid kun anvender balancerede strukturer, og at enhver algoritme, ikke har større kompleksitet end O(log(n)), der er den største kompleksitet, som en algorithme må have, ifølge datalogien, hvis den skal svare, indenfor det der betragtes som endelig tid.

Pegere, er et lidt andet problem. De indeholder ikke data. De peger til data, i vores erklærede strukturer. Vores dynamiske data, er derfor struktureret, og de findes i erklærede strukturer, ligesom statiske. Peger en data, på en struktur, så vil den pågældende struktur, ikke kunne deletes, uden at alle pegere fjernes først (f.eks. nulstilles). Det viser sig, at være den mest sikre metode, så vidt jeg ved. Vi kender det lidt fra operativsystemer. Er noget "åbent", og derfor i brug, så kan det ikke slettes. Først, når alt lukkes, så er muligt at lukke/slette noget. Det er den almindelige måde, og den viser sig, at også give færrest fejl, fordi det sikres, at man har overvejet det først, ved at få alle pegere bort fra det område der skal slettes, før destruktion udføres.

En peger, skal helst ikke kunne pege på hele hukommelsen. Så en peger af typen pointer integer er forbudt. Der findes flere metoder, at løse det på. F.eks. kan pegerne låses fast, til at kun pege på bestemte steder. Hvis det skal minde om C++, så gøres det enklest ved, at man ikke tillader typer som pointer til en integer, eller andre globale typer. Du bliver nød til, at definere en integer type - f.eks. kan vi kalde den jint, og så pege på den. Nu er din peger låst fast, til at kunne pege på de få felter, i vores strukturer, der er erklæret af typen pointer jint. På den måde, afgrænses værdiområdet, for vores pointer. Det er vigtigt, af hensyn til analyse, parallelisme, og af mange andre årsager. Hvis vores peger, peger på en "global" type, og ikke en der er placeret i en struct, eller class, så vil vi som minimum, skulle give en warning. Og helst ikke tillade det, da det i høj grad, ødelægger muligheden for, at compileren kan have overblik over hvordan koden arbejder. Det ses tydeligt, hvis vi forestiller os, at koden arbejder parallelt, og selv paralleliserer enhver sekventiel kode.

Det, som jeg savner, er at programmørerne ikke kun kan kode, og implementere gode fiduser, eller teknologier fra datalogien. Men også kreere datalogi. Actor modellen er fin, og jo, det er helt klart den rette, når der skal laves parallelisme. Men, det er også nødvendigt, at have forståelse for sammenhængen, imellem sekventiel kode, og parallel kode. Og at vide, at forskellen imellem sekventiel kode, og actor modellen, ikke går langt videre, end til at indføre stream of typer. Altså, streams, der tillader, at koden kan "snakke sammen", på kryds af det sekventielle, altså så den er sekventiel erklæret, og at dette måske medfører låsning af rækkefølge, men at vi i princippet, kan tillade fifoer, af en bestemt datastruktur, der går "tilbage" i koden, og ikke kun er fremadrettet. Så er muligt, at indføre parallelisme, på en meget tæt på C++ lignende måde. Det væsentlige er ikke at gøre det sådan. Men at forstå det. At forstå, sammenhæng mellem parallel og sekventiel. Og at forstå, hvordan datagraferne hænger sammen. At kunne gå, fra en beskrivelse til en anden, og at kunne lave dataflow grafer for såvel sekventiel, som parallel kode. Det er væsentligt, også af hensyn til, at kunne lave ombytninger i sin kode.

Jeg er også imod at et programmeringssprog, anvender maskinære typer, som nævnt tidligere. Programmeringssprog skal være datalogi. Og de må intet arve fra det underlæggende lag. Da vi lærte datalogi, måtte vi ikke bruge integer. For det var en maskinær type. Vi kunne skrive xintv=0..91 - da vi så erklærede det, af hensyn til vores behov og opgave. Ikke noget med 0..255 - for hvad pokker er 255? Det er et tal, der er stjålden, fra den maskinære verden, og ingen betydning har for opgaven. I stedet, er den rette metode, at erklære et interval, og der må ikke være nogen maksimale grænser for dette interval. Matematik, og datalogi, er uden grænser. Også float typer, giver ikke mening. I stedet, går vi den modsatte vej om. Vi angiver, hvor stor præcision vi behøver. Og computeren tilbyder os dette. Vi kan bede om, at få angivet hvor stor præcision vi får. Og den beregnes. Altså, igen - den fysiske verden, co-processorens nøjagtighed osv. må ikke afspejle sig, i et højniveau sprog.

Det er også vigtigt, at have styr på, om sine strukturer er deterministiske, eller om de kan give noget forskelligt. F.eks. funktioner, der ændrer værdi (c++), uinitialiserede variable, eventuelle problematikker ved parallelismen osv. Et sprog, skal helst være deterministisk, med mindre, det tydeligt fremgår andet af instruktionen (f.eks. random).

Jeg kunne fortsætte - men det som jeg foreslår er, at dem der laver højniveau sprog, lærer lidt datalogi. Så de - i det mindste - ved lidt om, hvad de udvikler.

Lars Tørnes Hansen

Dem der laver D2 compileren er alle ret gode ud i programmeringssprog.

Ham der laver D2 compileren hedder Walter Bright: http://www.walterbright.com/ , http://en.wikipedia.org/wiki/Walter_Bright
Walter Bright har lavet indtil flere compilere og jeg ret sikker han er blandt de få der kender hvert eneste hjørne af C++, incl de ulemper der er i C++.

En 2. person i D2 projektet hedder Andrei Alexandrescu : http://en.wikipedia.org/wiki/Andrei_Alexandrescu , http://erdani.com/ - han har lavet runtime biblioteket Phobos.

Du skulle kigge lidt på det der hedder SafeD:
Der skriver Bartosz Milewski nogle vise ord om programmeringssprog.
Bartosz Milewski, http://bartoszmilewski.com/ har i øvrigt lavet en type-sikker Erlang lignende actor baseret concurreny implementering i D2.

Jeg kan derudover nævne at i D kan man sætte en type til at være immutable, en anden mulighed er const, der er mindre striks. Noget der er immutable kan selvfølgelig deles imellem flere tråde uden problemer.

Det sidste jeg lige vil nævne er at D2 har 3 niveauer af hvor usikkert/sikkert det er at programmere, safe har jeg nævnt, der er så en i midten som jeg ikke på stående fod ikke lige kan huske navnet på.
Den laveste er så system, hvor man har de usikre pointere du snakker om - f.eks. interfacing med C kode.

Leonard Kramer

Jeg programmere selv i C++, og er basalt set glad for sproget, selv om der da er plads til forbedringer.

Problemet omkring pointers er at folk bruger dem uden at tænke sig om. Pointers a fremragende som referencer, saa længe man skriver kode hvor man holder ordentlig styr paa ejerskab af den oprindlige hukommelses blok, og sørger for at rydde op efter sig selv.

Java (og andre garbage collected sprog) har egentlig det præcis samme problem; hvis du ikke har styr på din hukommelse ender dit program med at blive sløvt og resurse krævende (I og med at din garbage collector er på over arbejde hele tiden).

Peter Jensen

Verden set i min optik (starter på dette område ca 1980)
Først havde vi assembler - det var os der var gode, men selv de gode kan blive trætte af bits, bytes, words, registre og indirekte adressering, så kom c, der jo blot er en mere struktureret assembler, man skal stadig gøre alt arbejdet selv (hvis man ikke kan finde et godt bibliotek), og da vi så skulle til at lave noget mindre maskinnært så kom c++ og hjalp lidt til, og det er c++ sådan set udmærket til hvis man skal have en færdig compileret binær file.

Idag bruger jeg helst python hvis jeg kan, men 9/10 dele af de biblioteker jeg bruger er enten skrevet i c eller c++, sådan er verden så finurlig

Thomas Bianchini Daugaard

Jeg er forholdsvis ny i C++ verdenen, men har læst en del artikler omkring dét med pointers og jeg gik fra at bruge dem meget til enten at bruge references (som de fleste anbefaler) eller smart pointers (boost::(scoped|shared)_ptr, std::auto_ptr og std::unique_ptr) som afhjælper en del af problematikken omkring pointers, netop ejerskab og 'dangling pointers' som vist kan være enhver programmørs værste mareridt.

Henrik Birkholm

Jeg kan huske, at vi havde Bjarne Stroustrups egen bog på DIKU link til bogen på amazon. Jeg husker ikke bogen, som nogen fornøjelse. Det siger sikkert mere om mig, end om bogen, men jeg husker den som temmelig ringe. Den er sikkert god, som reference manual, men den var ikke nogen god pædagogisk indføring i sproget. Så hvis at Bjarne Stroustrup mener, at det er måden at skrive en god bog på, så er der nok flere meninger omkring. Jeg har i hvert fald fundet andre bøger, som jeg mener er skrevet mere pædagogisk.

Jens Madsen

Jeg programmere selv i C++,
Jeg programmere selv i C++, og er basalt set glad for sproget, selv om der da er plads til forbedringer.

Problemet omkring pointers er at folk bruger dem uden at tænke sig om. Pointers a fremragende som referencer, saa længe man skriver kode hvor man holder ordentlig styr paa ejerskab af den oprindlige hukommelses blok, og sørger for at rydde op efter sig selv.

Java (og andre garbage collected sprog) har egentlig det præcis samme problem; hvis du ikke har styr på din hukommelse ender dit program med at blive sløvt og resurse krævende (I og med at din garbage collector er på over arbejde hele tiden

Jeg giver dig stort set ret - men alligevel ikke. Det er korrekt, at du kan komme et stykke vej, ved at tænke dig om, når du bruger pointere. Men, det er ikke nok. For problemet ligger "under" sproget, altså på compiler niveauet. Du kan ikke kode dig ud af analyserbarhed. Enten, så er programmeringssproget lavet til, at det er muligt, at analysere. Eller også, er det ikke. Enten, er det datalogi, der ligger under. Eller også er det ikke datalogi. I forbindelse med pointere, er det ikke datalogi. Hellerikke, når der bruges garbage kollektion. Det er decideret fusk, som jeg ser det. Det er nødvendigt, at du får struktur, på dine dynamiske data. Det kan ikke gøres ved, at du gør det pænt, når du koder. Selvom, at der stort set ikke, er forskel til det at jeg skriver. Problemet er, at det SKAL være en del af sproget. Ellers, så opnås ikke analyserbarhed. Det ER forkert, at gøre som både C++, java, og de andre sprog gør. Det rigtige er, at gøre som en bogholder gør: Tingene placeres på reoler, og det at holde bøger, er ikke samme opgave, som at pege på dem. I sprog som C++, og også java, er det at pege, det samme som at holde data. Altså, data hviler på en pointer. Et programmeringssprog, hvor dynamiske datastrukturer, defineres ved, at data hviler på en rød pil - det er ganske enkelt ikke brugbart.

Det som du skriver omkring garbage kollektors er også korrekt - igen, er dette problem løst, hvis data gemmes i strukturer, også de dynamiske. Du får ikke behov, for en garbage kollektor, der pludseligt optager tid. Du styrer fuldstændigt selv, hvornår data skal lægges ind i strukturerne, og hvornår de skal slettes. Og der er ikke ring-strukturer, der skal "opdages". Metoden er også meget logisk, og meget simpel. Du definerer bare dine strukturer, på samme måde, som normale structs, og klasser - men laver dem rekursive, så de fylder uendeligt. Så har du defineret en struktur, du kan gemme data i. Sættes data ind, så fylder det. "kataloger" og "underkataloger" kreeres automatisk. Og du kan slette dele, eller grene af strukturen. Det er muligt, at få data til at advancere op, eller ned i strukturen. Og muligt, at programmere f.eks. heaps, rød-sort træer, kædede lister, og alt andet, på en overskuelig måde. Du har altid styr på, hvor meget din dynamiske struktur fylder, og kan få oplyst såvel dybde, som hukommelsesforbrug (antal elementer), osv. Men, det vigtigste er, at når du analyserer din kode, så får du et helt andet resultat, end hvis du bare bruger en "pointer" der kan pege på hvadsomhelst. En pointer, er ekstrem destruktiv kodeanalytisk, fordi den medfører trådning. Ved du ikke, præcist, hvilket område i lageret, at du har mulighed for at skrive til, så kan du f.eks. ikke afgøre, om to pointere, kan udføres parallelt, eller i omvendt rækkefølge. Det har du, når du erklæer det i strukturer. Du kan analysere koden, og bytte om, også selvom du ikke kender pointerens værdi. For du ved, at pointeren peger på en bestemt struktur. Og at den kun kan pege, på bestemte værdier. Det gør koden analyserbar, og at sekventiel kode, kan paralleliseres, eller at indstruktionerne, kan ombyttes frit. Som jeg siger - dem der laver programmeringssprog, kan godt brygge kode sammen. Og det er også meget brugbart, fra en brugers synspunkt. Men datalogi - det forstår de sig altså desværre ikke på. Du kan også se det på mange andre detaljer i programmeringssprogene, at datalogien halter - f.eks. at det er muligt med kode, der giver "tilfældig" resultat. Programmeringssprogene, er tættere på maskinkode, end højniveau sprog, når at co-processorens datarepræsentation indgår i sprogets defination. Osv. Altså, hvor er datalogien blevet af? Selv i indledende datalogi, på DTU, måtte vi ikke bruge integer typer, fordi de var maskinære.

Netop tidsproblemet er også væsentligt, og et programmeringssprog, skal også helst laves, så det er muligt at analysere svartid. Anvendelsen af nul-terminerede strenge, er dårligt, eller helt forbudt, udfra et datalogisk synspunkt, fordi det ikke lever op til O(log(n)) reglen, og klasificeres ofte, som at der ikke er svar.

Martin M. S. Pedersen

Jeg har læst to bøger af Bjarne og de er ganske elendige.
Jeg kan ikke forestille mig at Bjarne Stroustrup er blevet bedre til at t
skrive lærebøger.
Selvom Bjarne er en god sprogudvikler, er det ikke givet at han er den rette til at skrive lærebøger.
Lidt ligesom forfattere. De kan være ganske elendige oplæsere at deres egne bøger.

Lars Tørnes Hansen

Smartpointers, og referencer er en god ide, men brug RAII, hvor du kan slippe af sted med det :

C++ eksempel på wikipedia siden om RAII:
http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization#C.2B...

"RAII in C++" - artikel:
http://jrdodds.blogs.com/blog/2004/08/raii_in_c.html

B. Stoustrup om RAII
http://www.stroustrup.com/bs_faq2.html#finally

Wikibooks om RAII i C++
http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Resource_Acquisition_Is...

EDit:
Link tilføjet
http://www.hackcraft.net/raii/

Nicolai Møller-Andersen

Det forekommer mig, at debatten hurtigt kommer til at handle om alt det farlige i C++. Alle dem med pointer-angst råber op om andre sprog, hvis største dyd er, at de forhindrer programmøren i at bruge hardwaren bare nogenlunde fornuftigt. Man forbyder da ikke køkkenkniven og stegepanden, fordi man kan slå ihjel med dem?
Det interessante i artiklen er vel, at Bjarne fortæller os, at rigtigt mange har misforstået, hvordan man bruger C++...

Lars Tørnes Hansen

Jeg ejer den i artiklen omtalte bog - og jeg synes at han er sluppet godt fra det - han netop fået hjælp af en underviser, så han har måske lært noget nyt siden sidst (Det er min første Stoustrup-bog).

På bogens webside kan du gratis downloade en pdf af kapitel 12.

Jens Madsen

Det forekommer mig, at debatten hurtigt kommer til at handle om alt det farlige i C++. Alle dem med pointer-angst råber op om andre sprog, hvis største dyd er, at de forhindrer programmøren i at bruge hardwaren bare nogenlunde fornuftigt. Man forbyder da ikke køkkenkniven og stegepanden, fordi man kan slå ihjel med dem?
Det interessante i artiklen er vel, at Bjarne fortæller os, at rigtigt mange har misforstået, hvordan man bruger C++...

Nej - det jeg skriver om, handler ikke om, hvad det er farligt. At det så måske også er farligt, er en anden sag. Det drejer sig om datalogi - og forståelsen for datalogi. Ikke mindst forståelsen for, hvordan et sprog fungerer, de underlæggende flow grafer, oversættelse og analyse, samt at definere sprog, der er analyserbare - herunder at koden kan oversættes til parallelisme, selvom den er sekventielt beskrevet, at compileren har lov at ombytte indstruktioner, og kan frigøre sig, fra selve sprogets beskrivelse af, hvordan det skal gøres, at compileren kan oversætte til hardwaren, og at hardwaren ikke er del i sproget, osv. Når vi analyserer kode, så har vi komplekse algorithmer til det - og hvordan skal de fungere på kode, som er ikke analyserbar? Hvordan, vil du lave en computer, der er i stand til, at kunne "analysere" sig til resultatet af en loop, og analytisk kan finde ud af, hvor mange gange løkken gennemgås? Manglende forståelse for datalogi, kan ikke kurreres, ved at klistre "how to do" og "gode metoder" på programmeringssproget. Og, hvis dem der udvikler sprogene, også mangler den datalogiske indsigt, så kan jo diskuteres, om disse how to do guides, og "how to behave in programming", overhovedet er fornuft. Jeg vil ihvertfald ryste lidt på hovedet, hvis de er skrevet af en, der har formuleret et programmeringssprog.

At det farlige så samtidigt forbydes, er jo en anden sag. Og det er ikke så dårligt. Men, det er et resultat af datalogien. Sjovt nok, viser det sig, at der ofte er en sammenhæng, imellem manglende analyserbarhed og "fare". Så når noget er farligt, er der måske noget, der er galt "inde i". Derudover, er også vigtigt, at tage hensyn til, hvad det er farligt - for hvorfor lægge fælder ind i programmeringssproget for sjov? Hvis vi ser på determinisme, så er det også væsentligt for compileren. For er det ikke deterministisk, så bliver det nemt en gang endnu større rod, når compileren går i sving, med at lave om. Tingene, skal naturligvis være ordentligt defineres, og ellers skal tydeligt fremgå af ordren, at den medfører et tilfældigt resultat - f.eks. hedde random.

Men, det er korrekt, at datalogi også handler om, hvad det er farligt. Og igen - så viser netop det, at noget er farligt, i programmeringssproget, at dem der har udviklet det, ikke har forstået, eller bestået, datalogi.

Kenni Lund

Jeg kan huske, at vi havde Bjarne Stroustrups egen bog på DIKU link til bogen på amazon. Jeg husker ikke bogen, som nogen fornøjelse. Det siger sikkert mere om mig, end om bogen, men jeg husker den som temmelig ringe. Den er sikkert god, som reference manual, men den var ikke nogen god pædagogisk indføring i sproget.


Ok, men det er jo også præcis som forventet, eftersom bogen du nævner ER en referencebog for sproget, som man benytter til opslag, mens bogen "Programming - Principles and Practice Using C++" er en lærebog med en pædagogisk tilgang til at lære læseren at programmere :) Sidstnævnte bog udkom i 2008-2009.

Jesper Louis Andersen

Med andre ord, skal de som minimum, kunne oversætte deres sprog, til en dataflow graf, helt ned på elementært bit niveau, eller assembler niveau, således de får en forståelse for selve flowet, og de skal kunne oversætte en dataflow graf, fra et sekventielt program, til en fuld parallel struktur, og have forståelse for de problemer, det giver, f.eks. med hensyn til den tvungne trådning som opstår, og begrænsningerne i de ombytninger som det fører til.

Det der sker allerede i de fleste C++ compilere. LLVM bygger f.eks. en dataflow graf over programmet. Desuden gør en moderne CPU det at den autoparalleliserer der hvor den komme afsted med det. Hvis jeg skal pege på noget som er galt i mange sprog, så er det rækkefølge af instruktioner. Jo mere du har specificeret en rækkefølge programmet skal udføres i, jo flere begrænsninger sætter du til at kunne køre dele af programmet i parallel. De sprog du skal have fat i, er temmeligt Prolog-agtige i deres udformning.

Eske Christiansen

Datalogi, er en matematisk videnskab, der ikke må "arve" noget fra det maskinære niveau. F.eks. må matematik, ikke indeholde fysiske konstanter, som avogadros konstant. Og tilsvarende, bør programmeringssprog, ikke indeholde typer som byte, og integer. Disse er maskinære typer, der ikke hører hjemme i programmering.

Det svare til at sige at man i fysik ikke må anvende lysets hastighed da det jo er en konstant som kommer fra det "maskinære"...
Du koder på en maskine. Denne maskine har nogle fysiske begrænsninger, disse ville altid være rare at vide.

Lasse Dessau

Stroustrup har fuldstændigt ret. Mit indtryk er, at mange der i dag programmerer i C++ enten er gamle C-programmører eller har læst en gammel C++ bog. Pointen er at C++ ikke bare er C med Classes. Det er sit eget sprog der, hvis det bruges som tiltænkt, er læseligt og pænt.

Med C++11 er der opdateret funktionsbibliotek, smart pointers, for-range loops, auto variabler, lamda funktioner osv. Det giver adgang til et sprog, der kan skrives ligeså hurtigt og pænt som Java, men med fantastisk fleksibilitet og en ydeevne der ellers næsten kun kan findes i C.

Jens Madsen

Det der sker allerede i de fleste C++ compilere. LLVM bygger f.eks. en dataflow graf over programmet. Desuden gør en moderne CPU det at den autoparalleliserer der hvor den komme afsted med det. Hvis jeg skal pege på noget som er galt i mange sprog, så er det rækkefølge af instruktioner. Jo mere du har specificeret en rækkefølge programmet skal udføres i, jo flere begrænsninger sætter du til at kunne køre dele af programmet i parallel. De sprog du skal have fat i, er temmeligt Prolog-agtige i deres udformning.

Jeg kan kun sige, at du ikke har lært det grundigt nok. Der er ingen forskel, på sekventiel kode, og parallel kode.

I nogle tilfælde, f.eks. ved pointere, risikerer du, at tvinge en rækkefølge. Men hvis du gør, som jeg skriver, er et sekventielt program, fuldt ligeså parallelt, som et parallelt program.

En dataflow graf, er parallel af natur. Men, i nogen tilfælde, kan dataflow grafen, medføre at dit program "trådes".

For at forklare lidt om, hvordan en pointer virker, så skal du forstå begrebet datastrømme. En datastrøm, kan godt være hele ram lageret i en PC. Den kan også være en del af ramlageret. En datastrøm, kan også være en enkelt bit. Når du ændrer noget i lageret, ved at tildele det en værdi, så har du en komponent, der har nogle inputs og outputs. Som input, har du hele lageret som datastrøm - det er nødvendigt, hvis du har en pointer, der kan modificere en del af lageret. Du har også selve addressen, altså pointerens værdi, som input. Og du har den værdi, som du ønsker addressen erstattet med, som input. Og outputtet, er så en datastrøm, med det nye lager. Har du flere modifikationer efter hinanden, er det flere bokse. Der gælder regler for, hvordan de kan ombyttes. F.eks. så kan du tage en samling af aflæserkasser til din ram-data-strøm, og ombytte. Mens, der er problemer med, at ombytte tilskrivninger. Tilskrivningerne medfører, at du påtvinger en trådning. Og ikke nok med det - hvis du har hele ramlageret, som tråd, så medfører det stort set, at du får trådet alt. Hvis du gør, som jeg skriver, så har du 100% kontrol med dine pointere. Du kan angive værdimængden, som din pointer kan pege på. Og nu kan du pludseligt ombytte dine skrivekasser! Programmet kan håndtere det sådan, at det simpelthen arbejder med forskellige datastrømme, hvor hver datastrøm, nu ikke mere er hele computerens arbejdslager - men kun den "lille" del, af lageret, som er relevant, for din indstruktion. Det betyder, at ting, der ikke vedfører den, vil kunne køre parallelt. Og din compiler, kan også ombytte mv.

Det er korrekt, at der findes compilere, der arbejder dataflow orienteret. Jeg arbejder meget med VHDL. Det er 100% dataflow orienteret. Når du f.eks. laver en hukommelse her, så bruges multipleksere, til at modificere dine data med. Og, hvis data ikke ændres, er der en sløjfe tilbage, så dine multiplexere udgør en hukommelsescelle. Måden, er dog en lidt andet, end i sprog som C++.

I sprog som C++. er de grundlæggende blokke du bruger samme som dem, der bruges i asynkron logik. (Du kan evt. søge på dette). Disse blokke, svarer fulstændigt, til parallel programmering, efter actor modellen.

Hvis dem, der laver programmeringssprog, forstod en lille smule, så vil de ikke bruge pointere, på samme måder som C++ og Java. For det er noget rod. Og at ansætte en "opryder", er den totale fiasko. En misforståelse, af dimmensioner. De skulle i stedet ansætte en bogholder, eller dataholder, til at holde data, og at holde dem i system og strukturer. Det vil medføre at compileren har mulighed for at kende værdimængder for pointere, og kan analysere koden bedst muligt.

Med andre ord. Alle sprog, der anvender garbage kollektion, er noget rod, og de er ikke lavet af dataloger, der har forstået pensum. På DTU, ved jeg ikke, om man har undervist i datalogi i de sidste mange år. Dengang jeg gik der, havde vi datalogisk institut. Men, lærerne nægtede, at undervise i C og C++. Blandt andet, var historien med, at c++ funktionen, ikke var deterministisk, en af deres i indledende datalogi. Og man valgte på grund af press fra erhvervslivet, at flytte undervisningen til maskin retningen. Til sidst, blev datalogisk institut nedlagt, og de rester som var tilbage, lagt ind under institut for matematisk statistik og operationsanalyse. Hvilken katastrofe. Jeg ved ikke, om der stadigt undervises i datalogi.

Frej Soya

Det er godt nok efterhånden et stykke tid siden jeg sidst rodede med Java - men så hut jeg hvisker fandt jeg Javas abstration fra pointere yderst glimrende. Det vigtigste er vel at eliminere den ekstreme fejlkilde som pointere reelt er.


De gjorde bare pegere implicit og ja fjernede fornuftigt nok aritmetik med pegere. Men det var allerede en mulighed i c++ (referencer). Begge sprog har stadig et kæmpe problem med stadig null-pegere som ikke bliver opdaget ved oversættelsen.

Jens Madsen

Det svare til at sige at man i fysik ikke må anvende lysets hastighed da det jo er en konstant som kommer fra det "maskinære"...
Du koder på en maskine. Denne maskine har nogle fysiske begrænsninger, disse ville altid være rare at vide.

Selvfølgeligt, må du gerne bruge lysets hastighed i fysik - for det er jo en fysisk konstant. Men du må ikke bruge den, i matematik. Du må gerne bruge en matematisk konstant i fysik.

Du må ikke bruge en maskinær konstant i datalogi. Datalogi, er abstrakt - og det bruger ikke maskinære konstante. Datalogi, er en del af matematikken. Maskinens opbbygning, er "fysik", og det undervises i det, i elektronik (digitalelektronik). Med mindre, at det er et maskinsprogsprogram, må det ikke anvende maskinære konstante. I maskinsprog, må du naturligvis godt.

Compilerens opgave er, at oversætte fra et abstrakt sprog, der ikke er maskinnær, til computerens sprog. I princippet, kan du også oversætte dit program til en VLSI chip, eller gener. Hvis du oversætter den, til en VLSI chip, vil man analysere dit program, og undersøge, om det skal implementeres fysisk, eller om det skal sekventialiseres, så komponenterne kan genbruges. Det sidste, er det samme som, at der regnes ud, om der skal laves en CPU til at udføre indstruktioner, der laver funktionen. Det betaler sig, hvis at det som skal udføres er relativt langsomt, mens det betaler sig, at omsætte til fysiske transistorer, for det som er hurtigt. Compileren, kan regne dette ud. Compileren, er også i stand til, at f.eks. folde en løkke ud, så den f.eks. står to gange efter hinanden. Hvis du har en løkke, f.eks. en fortolker løkke, der fortolker CPU indstruktioner, så medfører en udfoldning, f.eks. at der udføres to indstruktioner per klokcyklus. I gennemnsnit, så vil indstruktioner der udføres udfoldet, kun tage log2(n) tid. Du kan også lave sproget, så det oversætter til gener. Men, jeg har kun hørt om en der har arbejdet med dette i kantinen, så det kan siges, at være en "upålidelig kilde". Teoretisk, er det dog intet i vejen for det.

Selvom du i et abstrakt programmeringssprog, ikke må bruge maskinære typer, så er det dog kun, for at beskrive din funktion. Du må godt have interfacetyper i forbindelse med interfaces, og disse kan også inkludere maskinkære typer, ved et maskinært interface. Som eksempel, så kan man godt acceptere en slags bit-union type, der inkluderer typer som byte, shortint osv. og hvor der er defineret hvordan disse forstås bitvis. Denne type, må dog ikke anvendes i algorithmer, for hvis den bruges, så presser man compileren, og processoren, til at lægge dataene i den pågældende type. Ofte, kan det tage meget lang tid, hvis CPU'en ikke har den pågældende type i dens hardware. Arbejder du med gener - hvem ved, hvad du så har? Eller, du anvender måske en CPU, der anvender residueregning. Derfor, så er det kun når du ønsker decideret at kommunikere med et interface, hvor at din grænseflade er opgivet, ved hjælp af bestemte typer, at man kan forestille sig maskinære typer kan forekomme. I en algorithme, vil den aldrig forekomme, da der ikke sker kommunikation med noget udenfor sproget. En af diskussionerne er så, hvordan man skal kommunikere med software skrevet i andre sprog - her kan det være muligt, at bruge typer, som er specificeret for det pågældende sprog. I nogle tilfælde, kan det også være typer som byte, der er spejlvendt, eller hvor hver anden bit er negativ, eller sprunget over, og højeste byte, flettet ind for hver anden bit. En sædvanlig integer type, eksisterer derfor ikke, når man beskriver noget i programmeringssproget. Hvis der er en integer, så er den fra minus uendelig, til plus uendelig. Ellers, så erklæres den som et interval. Dette interval, har ingen grænser, så man kan ikke sige, at der er en maksimum integer, eller minimum integer i sproget. Også ved float, er det det samme. Du lader dig ikke styre af hardwaren. Du styrer hardwaren. Du "arver" ikke en type fra din co-processor. Du beskriver funktionen, og din compiler sørger for, at oversætte det optimalt, parallelt eller sekventielt, til din hardware. Eller, lægge noget ind, direkte som transistorer, i din FPGA. Typisk vil en byte type, defineres i sproget, ved hjælp af mod og div indstruktioner.

Esben Nielsen

Det er ikke sproget, den er gal med, det er brugernes vaner. C++ programmøre er for ofte inginøre frem for dataloger. Dvs. de mangler ofte erfaring med andre sprog, især funktionelle sprog som ML eller Haskell. De er rigtigt godt at få inspiration herfra, selvom man koder i et ikke-funktionelt sprog - og særligt i C++.

Og der er stadig alt for stor fokus på C++ som objektorienteret C. Og især fokus på nedarvning. I Java har man interfaces. Det har man også i C++ - de er blot klasser med kun rent virtuelle metode. Brug dem. Drop eller i det mindste minimér nedarvning af egentligt klasser.

Hold op med at deklarer klasser i .h og implementationen af metode .cpp. Små klasser ligges direkte .h, hvor metoder bliver inlinet og for store klasser lig kun en interface klasse med rene virtuelle klasser i .h filen og selve implementationen i en .cpp fil i et anonymt namespace. Så kan man editere, man vil i .cpp filen - lave ny hjælpe metoder osv. - men klienterne skal ikke genoversættes. Ej heller skal linkeren belemres med alle disse metoder.

Brug std::function eller boost::function. De giver enorm afkobling mellem klasser og komponenter, at man kan angive vilkårlige signature for callbacks rundt omkring.

Brug templates! Det er svært og kringlet, men det gør, at man kan lave en masse abstrakt udvidelsesbart kode uden det koster runtime i modsætning til OO teknikker, som koster runtime.

Og så lad være med at lave multitrådet programmering. Det er meget, meget ofte ikke nødvendigt. Riv de sider ud af lærebøgerne. Brug select eller lignende, så én tråd kan klare alt. Det er meget sjældent en process har behov for at sprede sig over flere CPU'er. Kun i få tilfælde kan man ikke klare sig med single tråede processer - medmindre ens API'er ikke findes i en non-bloking version, som desværre ofte er tilfældet, hvis man bruger 3. parts biblioteker.

Jens Madsen

Jeg er meget enig, i de ting du skriver. I nogle tilfælde, kan multitrådet programmering, være uoverskueligt, og medføre ikke determinisme, hvis den måde som multitrådningen gøres, ikke er udviklet til at udvise determinisme.

Et ordentligt programmeringssprog (her har c++ et problem), er i øvrigt selv i stand til, at oversætte sekventiel kode til parallel kode. Så formålet, med at beskrive tingene parallelt, ligger alene i det overskuelige. Med andre ord - bliver det ikke overskueligt, af at blive beskrevet parallelt, så er det ikke måden at gøre det.

Selvom du kun har en processor, og alt dermed udføres i en tråd, så vil et sprog, der tillader parallelisering, have fordele som at koden kan optimeres bedre, og at det er muligt, at splitte koden op i tråde, og automatisk muligt, at prioritere de forskellige dele af koden. Det betyder, at kritiske dele, automatisk udregnes først. Det er derfor ikke bare en mini-optimering, som sker, i moderne data-flow compilere, og CPU'er. Men en optimering, der baseret på f.eks. prioriteter, kan tage beslutninger, og optimere koden langt mere optimalt. Det er muligt, at bruge det sammen med operativsystemet, så optimeringen og paralleliseringen sker, under hensyntagen til andet software, der udføres på samme computer.

Endeligt, så viser det sig også, at det er meget mere overskueligt, og forståeligt, f.eks. at lave træer, og kædede lister, når der bruges de metoder som jeg nævner. Ikke kun er muligt at optimere dem, at analysere dem, og at parallelisere. Men, det giver også en meget mere overskuelig kode. Du laver ikke fejl, med lister der "hæftes" forkert. Eller glemmer at tråde noget. Stort set, sker det automatisk, uanset om du har kædede lister, træstrukturer, eller andet. Og fjerner du data, så får du det også gjort korrekt. Defineringen af data i strukturer gør, at det er strukturerne der kæder listerne. De initialiserer også automatisk en "up" pointer, så du kan gå op i strukturen, og der sikres, at der er typemæssigt kompatibilitet, hvis der er angivet sådan en pointer. Disse ting, kan du dog kode dig ud af - det væsentlige, er analyserbarheden, og at computeren kan håndtere programmet. Skal vi sige det på datalogisk: C++ kan ikke kompileres. Den manglende forståelse, medfører sproget ikke er maskinært læsbart og oversættelig, på et datalogisk niveau.

Peter Jespersen
Jesper Louis Andersen

Mens, der er problemer med, at ombytte tilskrivninger. Tilskrivningerne medfører, at du påtvinger en trådning. Og ikke nok med det - hvis du har hele ramlageret, som tråd, så medfører det stort set, at du får trådet alt. Hvis du gør, som jeg skriver, så har du 100% kontrol med dine pointere.

Lad mig gå i undervisermode en smule: Hvad nu hvis jeg har 2 skrivninger til hukommelsen. Og jeg ved, grundet en semantisk analyse, at de 2 skrivninger er til forskellige dele af hukommelsen? Kan jeg så ikke bare skrive, thi der kan ikke på nogen måde (i den nærliggende fremtid) være en konflikt mellem skrivningerne.

Og hvis jeg har følgende sekvens af instruktioner (bemærk at der ikke er nogen pointere her. Vi har bare et par registre),

  1. A = 2 * X
  2. B = A / 3
  3. A = 9 * Y

Kan jeg så omordne instruktion 1 og 3 i sekvensen?

Hints:
http://en.wikipedia.org/wiki/Alias_analysis
http://en.wikipedia.org/wiki/Data_dependency
http://en.wikipedia.org/wiki/Hazard_(computer_architecture)

Jens Madsen

Nej, du kan ikke ombytte dem. Men du kan flytte 3 op, så den bliver udført før 1, hvis du har lyst. Der er forskel, på de to A'er. A har intet med A at gøre.

Problemet, er ikke i det tilfælde, som du angiver - men hvis du bruger pointer tilskrivninger. En pointer tilskrivning, ændrer en addresse i ram lageret. Hvis du ikke kender addressen, har du ikke mulighed for at flytte indstruktionerne. Du skal vide, hvilken addresse du modificerer, så du kan afgøre, om de addresser, du ønsker at skrive til er disjunkte.

Det betyder, at det er vigtigt, at have overblik over pointer addressernes addresserum, altså på forhånd ved analysen, at kunne vide hvilke muligheder at pointerens addresse kan antage.

Det sker ved, at du angiver brugen af dynamisk hukommelse i strukturer. Og, at du - som jeg tidligere skrev - ikke bruger pointer integer typer, men laver en særlig type, som du kun angiver i din struktur, der eventuelt kan være integer. Herved, så viser du compileren, at to strukturer, der måske begge har pointer integer typer, ikke bruger den samme pointer integer. De har forskellig navne, er af forskellig type, og derfor disjunkte. Ellers, så dumper koden, når du typetjekker. Det kan gøres på mange andre måder, men den måde, er mest lig c++ og pascal.

Nu bliver vores compiler, også i stand til at håndtere pointere.

I visse tilfælde, kan vi håndtere pointere, også uden at gøre det sådan - hvis vi kan spå om, hvilken værdi de har. Vi kan f.eks. søge, at "simulere" programmet igennem, og se om vi kan regne ud, hvilket udfaldsrum som er muligt. I mange tilfælde, fås dog svaret, at vi ikke kender pointerens værdi, eller ikke kan udregne et interval, som pointeren er indenfor. Det skyldes, at det ikke er gjort, som jeg skriver.

Endeligt, så er også meget nemmere, at f.eks. håndtere træstrukturer, og kædede lister, hvis brugen af dynamisk hukommelse erklæres. De tråder selv listen, når der sættes elementer ind. Du får automatisk angivet pointer, til niveauet over, hvis en up-pointer angives (Det sætter krav, til ens type), og det viser sig også, at være langt nemmere, at sikre korrekt brug, f.eks. kan du bare tælle antallet af pegere, som peger på en struktur, når du skal kunne afgøre om den er fri, og kan slettes. Ved sprog som Java, kan der være pegere, der holder sig selv oppe cyklisk, og det skal også tjekkes. Det er ikke muligt her, da de ansvarlige dataholdere, er defineret i strukturer, der angiver hvordan hukommelsen anvendes. Disse er, når det er dynamisk hukommelse, at betragte som "uendelige", men opfylder kun begrænset, da der bruges kompakte typer, som reelt er en pointer (eller rettere dataholder, da det ikke er en peger, der ikke holder data), og de initialiseres automatisk, når du indsætter data i en struktur, således der altid sikres, at træet du har defineret er korrekt. Det er væsentligt, at der er garanti for tingene, for at det er analyserbart, og at compileren ved, hvad der foregår, så den kan behandle koden. I sprog, hvor du ikke har adskildt det, at holde data, og at pege på data, er ganske enkelt umuligt, at analysere sig til statisk, om en rutine, der anvender pointere, kan gøres parallel. Det kan være muligt dynamisk, men det er tungere, og ofte umuligt. Mange platforme, f.eks. hvis du skal lave kode til en ROM kreds, eller til et fysisk transistor niveau (som VHDL), lader sig kun analysere, med en statisk analyse. Når samtidigt, at koden bliver mere overskueligt, og at et træ, er defineret som et træ, så kan det undre, at nogle begynder at finde på automatiske oprydere, og andre symptomløsninger. Eller, at tænke rodet, og smidde alle data på gulvet, med gule sløjfer om, så de ikke bliver væk. Det virker hverken overskueligt, eller ordentligt.

Data skal sættes på hylder og reoler. Og de skal opdeles i rum, og underrum. Dette gælder også dynamiske data. Og de skal derfor defineres. Det er ikke nok, at definere nogle typer, for vores pegere. Det vigtige er, at vi også ved, om en peger holder data, ikke mindst, at vi har styr på dens definationsmængde.

Sprog som C++, er ikke lavet til, at kunne analyseres. Det er smækfyldt af pointere, og hvis du lavede dit program, med pointere, så kunne der være problemer. Ikke altid, fordi vi måske netop i det pågældende tilfælde, vil kunne analysere pointerens værdi på forhånd. I nogle tilfælde, kan programmet analyseres, så vi direkte tildeler den en værdi på forhånd, og så kan problemet løses, men ikke hvis pointerne bruges andet end statisk.

Tager vi dit program, så vil den, når vi ser på den analyse teknisk, indeholde nogle dataflows. Der er X, og Y som input. Og A og B som output.
Første linie, definerer, at udgangen af en multiplier blok, kaldes A. Den er ikke A, men kaldes kun A. Det er et lokalt A, som kun angives på datastrømmen imellem to blokke. Og at indgangen er X, samt konstanten 2. Ofte har vi en blok, vi kan indkode en fast konstant i, så implementeres som en 2X blok. Indgangen er X, og udgangen kaldes A. Den er ikke A. Udgangen går videre, til blok 2, der dividerer med en konstant. Og her, deles den med 3, og udgangen kaldes B. Nu definerer du så, at udgangen A, er 9*Y, altså du bruger en blok, med Y som indgang, og A som output. Som du kan se af flow grafen, så har denne intet med de andre at gøre, og hvornår du udfører den, er ligegyldigt. Den kan udføres helt parallelt med de andre, og på et vilkårligt tidspunkt. En dataflow graf, er parallel i natur.

En datastrøm, f.eks. X, skal forstås som en datalinie, hvorpå der kan være værdier. Det kan også være mange data, f.eks. hele hukommelsen. Datastrømmen er faktisk en FIFO, og betragtes udfra et parallelt synspunkt, som en beskedskø. Enhver operation - eller rettere næsten enhver - virker sådan, at samtlige indgange læses, for at give et resultat på udgangen, eller udgangene, af den pågældende komponent. Altså, operationen kan først udføres, når der ligger noget, i fifoen på samtlige indgange, f.eks. af vores plus, eller multiply operation. Og så sættes dataene på udgangen, eller rettere - de gemmes i dens fifo.

Der er kun meget få operationer, eller komponenter, der ikke opfylder disse regler. Men, når vi laver løkker, og kontrol strukturer, så bruges komponenter, der ikke nødvendigvis læser alle indgangene samtidigt, og skriver til alle udgangene. En multiplexer, vil kun læse den indgang, som den får besked på, af styresignalet. Og en demultiplexer, vil kun skrive til den udgang, som den får besked på af styresignalet. Vi bygger ofte multiplexere, og demultiplexere sammen i en komponent, da de ofte bruges sammen - en sådan komponent, virker som "løkke" komponent, eller kan bruges til, at overspringe kode.

Endeligt, så sker ofte, at vi har brug for at præ-initialisere vores datastrømme. Du vil ofte se, at der på en datastrøm står et "1" tal, som eksempel, i repræsentationen. Det betyder, at den er præ-initialiseret med et "1" tal. Og, hvis den f.eks. går ind i en multiplexer, så vil den vælge "1" retningen første gang. Når først at "1" tallet er læst, så er den ude af verden, og nye værdier, skal tilføjes fifoen.

Vi kan godt have flere "1" taller på vores datastrøm, så vi indikerer, at fifoen behøver at være flere niveauer dyb, og at der så er en sekvens af præ-initialiserede data. Vi kan også have en "loop" tilbage, som angiver, at sekvensen repeteres. F.eks. vil en generator, der giver "1"'er ud, ofte angives som et 1 tal på datastrømmen, kombineret med en loop retur.

Det er fuldt asynkront. Og det er 100% parallelt. Du kan ikke ud af dataflow grafen se, hvor mange processer, at programmet oprindeligt er skrevet med. Måske er det en enkelt tråd - who knows?

Hvis du modificerer ram kredsen, f.eks. med komponenter, så vil det desværre ofte være nødvendigt, at have hele ram lageret, som en stor tråd, der går igennem alle modifikationsboksene. Det er stort set umuligt i praksis, og derfor smider vi fifoerne bort. Og vi har brug for trådning.
Hvis du undgår pointere som i C++, der har hele ram'en som værdimængde, så kan du opdele i flere ram-tråde, og derved opnås parallelisme, da du får flere uafhængige tråde. I nogle tilfælde, kan du også øge parallelismen, når du analyserer. Det sværre er faktisk, at mappe det fornuftigt ind, på et begrænset antal CPU'er, og at kunne opnå at gøre det tidsmæssigt korrekt, og så det virker effektivt, overfor brugeren.

I VHDL, hvor vi oversætter til hardware, er det omtrent samme principper, men vi arbejder ikke med fifoer imellem komponenterne. Selve diagrammet, er faktisk det samme, men udsættes måske for lidt analyse og kompression. Derfor virker VHDL på en lidt anden måde, og er ikke parallelt efter actor modellen. Hvis derimod, at det er asynkron logik, så anvender vi samme model, hvor hver enkelt operation, betragtes som en komponent, med indgange og udgange, og fifoer imellem.

Vi kan også oversætte programmeringssprog, der er defineret ordentligt, direkte til FPGA'er, ligesom VHDL. Og det er muligt, at analysere, om det betaler sig, at implementere en "CPU" til at udføre de langsomme dele. Eventuelt flere CPU'er. Automatiske sprog, kan tage en højniveau specifikation, f.eks. skrevet i et sprog, som det jeg angiver, eller i visse former for begrænsede udgaver af C, så det direkte implementeres effektivt i hardware. Nogle mener også, at teknikkerne kan bruges, ved helt andre opgaver, f.eks. kemi, gener osv.

Meget af det vi ser i naturen er parallelt. I sin tid, forsøgte "reklamemagerne" at overbevise os om, at det var objekte - ligesom i C++. Men det har aldrig været korrekt. Det korrekte er, at naturen er parallel. Og at den består af parallele blokke. Det kan diskuteres lidt, om de kommunikerer efter VHDL metoden, eller den asynkrone model.

Det er meget morsomt, men meget få ved åbenbart meget om programmering, og datalogi. Derimod, er de god til at "sælge" programmeringssprog. Det virker som om, at nutidens nørder, er udprægede sælgere, der har et sprog, de skal have solgt.

Jesper Louis Andersen

Det er meget morsomt, men meget få ved åbenbart meget om programmering, og datalogi. Derimod, er de god til at "sælge" programmeringssprog. Det virker som om, at nutidens nørder, er udprægede sælgere, der har et sprog, de skal have solgt.

Kære Jens,

Din opgave er desværre til genaflevering. Du svarer ganske nok korrekt på den anden del af opgaven, men du har helt undladt at svare på den første del. Desuden er dit svar af 2. del alt for omfattende. Det ville være rart om du havde gjort et forsøg med den første.

Alt det du beskriver omkring pointere bliver allerede gjort i moderne oversættere. Det er korrekt at du har nemmere ved at udføre analysen på sprog hvor der er færre direkte manipulerbare pointere for programmøren og jeg er normalt også stor fortaler for at man anvender sprog fra disse klasser: Standard ML, Prolog, Mercury og andre varianter heraf. Men i praksis er aliasingproblemet ikke specielt forekommende i C++ programmer. Det er derfor helt forkert at gå efter dem. Til gengæld er pointen i 2. del af opgaven at du rigtigt nok ikke er i stand til at ombytte de to instruktioner. Men dermed har du også erkendt at der er en dataafhængighed mellem de to og dermed har jeg ret i min pointe: Det er sekvens der er problemet. Du beskriver endda fint hvorledes det kan paralleliseres på maskinniveau og nævner også i flæng, uden at vide det, SSA-form. Men her er killeren: Det bliver allerede gjort i en moderne CPU. Du vil derfor vinde præcist 0 nanosekunder.

Grunden til at vi ikke ser mere VHDL og FPGA-anvendelse er primært tre ting: Prisen på "store" FPGAer er for høj. De udvikler for meget varme og fylder for meget. Og så er de generelle, hvilket betyder at timing ikke er optimeret i dem som i en normal CPU. Alternativet er at lave en ASIC, men så er prisen også en helt anden. I stedet bruger vi CPU'er der er billigere i anvendelse. Og et VHDL-agtigt sprog er ikke så nemt at overføre til en CPU hvis hastigheden også skal være fornuftig.

Det er muligt at en ændring i CPU-arkitektur hen imod FPGA'er kræver et andet sprog. Det gør det formentlig. Men det er ikke det vi har i dag og derfor er det ikke specielt interessant i forbindelse med den her diskussion.

Jens Madsen

Jeg vil ikke benægte, at det du skriver er korrekt. Men jeg mener ikke, at det er almindeligt, at oversætte sekventiel kode, til fuld parallelisme. Hvis du gør det, kan du angive tingene parallelt, bare ved at skrive to programmer efter hinanden, såfremt der ikke er dataafhængigt.
Ved mange analyser, går man ud fra type. Det betyder, at du som programmør, bliver nød til, at anvende forskellige typer, hvis der ikke forekommer dataafhængighed. Som eksempel, låser en pointer, til en integer meget - fordi der kan være mange pointere i programmet, til integer typer. Og du ved ikke hvilken, som der peges på. Det er muligt, at kompileren opdeler nummererer alle disse integers, så de internt er forskellige, og forsøger at analyserer sig til, udfra koden, hvor at pointerne kan pege på. Og derved lægger under-typer ind, så compileren selv indfører nye typer, der er datauafhængige, og muliggør parallelisme.

Det er korrekt, at sekvensen er problemet. Men ikke nødvendigvis større, end der kan gøres noget ved den. Der findes mange "operationer" du kan udføre på dine dataflow grafer, der gør dem mere paralelle. Hvis du f.eks. har en løkke, kan den udfoldes, og så opstår måske parallelisme.

Nogle af disse ting, foregår bedst run-time. Du kan seperere løkker, således at det der styrer antallet af gennemløb, er i en kode, mens den så styrer mindst antal gennemløb, i slave-løkker. I nogle tilfælde, kan løkkerne pakkes ud, analyseres, og derved reduceres til o(log2) kompleksitet. Ved at oversætte det til flere løkker, der styres med et minimum antal gennemløb af kontrol-løkken, så opnås bedre elastik i koden, og bedre mulighed for parallel udførsel.

Laver du analysen run-time, så er ofte muligt, at gå længere, end ved en statisk analyse. Du kan også optimere din kode. Og i nogle tilfælde, er du i stand til, at sætte en vis del af resourcerne af på, at optimere din kode run-time. F.eks. kan den optimere fortolkerkode bort, så et fortolker program, kan blive ligeså hurtigt, som en oversætter. Du kan have flere fortolkere, i fortolkere, der i nogle tilfælde reduceres bort. Og du kan f.eks. skrive en pentium fortolker, i basic, og opnå at den går ligeså hurtigt - eller hurtigere - end hvis du kører koden på CPU'en direkte. Da at den kode den kører, jo også optimerer fotolkere bort, og laver andre optimeringer, så vil den i nogle tilfælde, gå hurtigere. Plus, at maskinkoden, automatisk kan deles ud på flere CPU'er.

Det er rigtigt, at FPGA'er kan have nogle ulemper, til at udføre kode. Men de kan godt indeholde CPU'er, således at kun tidskritiske dele, lægges i selve FPGA'en. I så fald, dannes ikke unødig varme. Ofte bruges FPGA'er, med henblik på, at senere omdanne koden til et transistor layout, og her kan også optimeres på koden, så den giver et mere optimalt resultat - og med lavere strømforbrug - end hvis du bruger en CPU.

Når du omsætter sekventiel kode, til fuld parallelisme, så er det desuden et problem, at få den mappet ned i "begrænsede resourcer" igen. For at gøre dette, så bruges prioritering, der bruges timing specifikationer mv. Compileren vil så søge, at både overholde timing specifikationerne ved mapningen, samt søge at prioritere tingene ordentligt. Operativsystemer, der håndterer det, kan foretage prioritering mens programmerne kører, således at det f.eks. opprioriteres, at få noget ud på brugerens skærm, mens at andet, såsom initialisering, kan skubbes i baggrunden. Startes computeren op, så går det hurtigt, fordi at alle initialiseringer mv. af hardwaren, automatisk "skubbes" af operativsystemet, til at være senere end brugergrænseflade, og de ting brugeren har brug for. Det skyldes den automatiske analyse, og parallelisering af den kode som udføres.

Så vidt jeg ved, er det endnu ikke almindeligt, at fuldt parallelisere maskinkoden, og at lave analyser på den, f.eks. i operativsystemerne, når koden kører run-time.

Jens Madsen

Lad mig gå i undervisermode en smule: Hvad nu hvis jeg har 2 skrivninger til hukommelsen. Og jeg ved, grundet en semantisk analyse, at de 2 skrivninger er til forskellige dele af hukommelsen? Kan jeg så ikke bare skrive, thi der kan ikke på nogen måde (i den nærliggende fremtid) være en konflikt mellem skrivningerne.

Jo, som nævnt, er det nemt, hvis du har forskellig type. Med mindre compileren søger, at selv indføre typer, hvor der ikke er konfligt, så sætter det særlige krav til programmøren, f.eks. at ikke bruge pointere til globale typer.

Som regel, er vi ikke "tilfreds" med at det kun gælder den "nærmeste" fremtid. Vi ønsker, at koden fuldt paralleliseres, således der ikke er forskel, mellem en sekventiel og parallel beskrevet algorithme, hvis algorithmen udfører det samme.

I nogle tilfælde, f.eks. hvis opgaven er at mappe løsningen ned i en VLIW processor, så kan det være nok, at se på den nærmeste fremtid. Men, det interessante er jo, at kunne beskrive parallele ting, i et sædvanligt sekventielt programmeringssprog, og derved opnå både det overskuelige, og den determinisme, som det medfører, hvis sproget laves til at udvise determinisme.

Det, som bør afgøre, om vi vælger en sekventiel, eller parallel beskrivelse, er alene hvad vi syntes, er mest overskueligt.

Hvis compileren selv analyserer koden, kan du ofte skrive parallel kode, bare ved at angive to programmer under hinanden, uden der er dataafhængighed imelllem disse. Hvis vi skal kommunikere på sædvanlig vis, imellem de pågældende sekventielle programmer, så kan det være relevant, at indføre mulighed for, at bruge køer imellem programmerne. Fra den der står først, til den der står bagefter, er det forholdsvis nemt at forstå, da det bare virker som en normal kø. Men, fra den næste, til den første, kræver det måske lidt hjernevridning, da koden på sin vis, jo her arbejder baglands.

Jens Madsen

Fra den der står først, til den der står bagefter, er det forholdsvis nemt at forstå, da det bare virker som en normal kø. Men, fra den næste, til den første, kræver det måske lidt hjernevridning, da koden på sin vis, jo her arbejder baglands.

Hvis du har to parallele processer, der håndterer nogle inputs, fra nogle datastrømme (beskedkøer), så vil de typisk omsluttes af en uendelig løkke, således at de kan behandle et uendeligt antal data, der kommer ind via beskedkøerne.

Du kan således altid angive parallelisme, ved at have to dele af koden, der er datauafhængige, anvende fifoer/beskedkøer imellem disse, og anbringe processerne, i uendelige løkker.

Nogle syntes også, at det er nemmest, at bare sætte en stor uendelig løkke, uden om alt det hele.

Esben Nielsen

Jeg ville da gerne kode i sådant et sprog. Men så vidt jeg ved eksisterer det vist bare ikke som noget produktionsklart, selvom datalogerne har forsket i det i årevis. Derfor koder jeg (stadig) i C++. Og deler systemmet op i én-trådede processer, da parellisering må mindre niveau simpelthen er for svært og ofte slet giver nogen performance, da atomare operationer er meget dyre.

Jeg har kodet en del multitrådet og har beskæftiget med mig priority inheritance algoritmerne i Linux; men det har mest været med fokus på realtime regulerings systemer, hvor respondstiden skal holdes nede for dele af systemet. Derfor behøver systemmet flere tråde. Men de kan jo egentligt ligge i hver sin process, da man jo alligevel ofte blot bruger message-parsing imellem dem...

Jens Madsen

Jeg ville da gerne kode i sådant et sprog. Men så vidt jeg ved eksisterer det vist bare ikke som noget produktionsklart, selvom datalogerne har forsket i det i årevis. Derfor koder jeg (stadig) i C++. Og deler systemmet op i én-trådede processer, da parellisering må mindre niveau simpelthen er for svært og ofte slet giver nogen performance, da atomare operationer er meget dyre.

Ja, det er netop en af problemerne.

For en del år siden, blev lavet computere, der fungerede sådan i hardware. På nogle måder, var de hurtige, fordi at alt var parallelt og eventdrevet. Men, til almindelige opgaver, f.eks. en sekventiel algorithme, var de faktisk sløve.

Derfor er nødvendigt, at omsætte den parallele kode, som er parallel ned til atom-basis, med noget der er mere sekventielt - om muligt. Her analyseres det så, med henblik på, om man kan få brug for at ændre prioriteterne senere, brug for at ændre graden af parallelisme, kendskab til timing imellem inputs og outputs, og den slags. Og så går man baglands, fra super parallel kode, til nogle sekventielle koder (eller VLIW, der jo også kan betragtes som delvis parallel), der kommunikerer sammen. Disse er nu større blokke, og sekventiel kode, kører ordentligt. Fordelen ved, at det automatisk deles op i blokke, er dels at du kan køre det parallelt, og tage hensyn til antallet af processer, som din computer kan køre samtidigt. Og, at du kan lave timing analyse, og prioritere din kode, således at svar tider kan opfyldes. Du kan også optimere prioriteterne, således at brugeren får en bedre oplevelse.

Jeg ved ikke, om det er lykkedes særligt godt, med f.eks. pentium maskinkode. Men du kan sagtens definere maskinkode, som det kører på.

Det er ikke så svært igen, at analysere traditionelle højniveausprog, som f.eks. C++ og Pascal, så det i de fleste tilfælde, er i stand til at selv gennemskue om kode er uafhængigt. Det betyder, at du bare angiver paralellisme, ved at sætte flere uafhængige koder efter hinanden, og lader det tale sammen, igennem køer. Normalt, sættes en løkke om hver process, så den fortsætter i det uendelige. Du kan også, sætte flere processer i samme løkke. Stort set, er ingen forskel, bortset fra exit-loop, der i det ene tilfælde, afbryder den ene process, og fortsætter hvor den uendelige løkke slutter. Mens, at i det andet tilfælde, hvor begge processer, bruger samme løkke, så afsluttes begge processer med exit-loop. Og der forsættes "sekventielt", hvor løkken afslutter - hvis man kan kalde det det.

Men det er dog mere sjovt, hvis hele maskinkoden virker på den måde, og hele operativsystemet. Eller, måske rettere operativsystemkoden, da det ikke er maskinkoden.

Jens Madsen

Når programmer udføres, baseret på dataflow grafer, sker det ofte baglands, således et output genereres, når vi spørger om det. Som beskrevet før, så er det ofte ineffektivt, at ikke udføre indstruktionerne i rækkefølge, og programmet analyseres til passende sammenhængende kode "stumper", der udføres i rækkefølge. Disse stumper, hæftes så sammen i en struktur, og kontroleres. Udførslen baglands, sker derfor reelt ved, at de dele af softwaren, som er analyseret til, at være nødvendigt for det pågældende output, kun udføres. Ved analysen, gås direkte "baglands" for at undersøge sådanne dependencies, og kunne opdele i passende blokke.

Hvis vi f.eks. udskriver noget på en skærm, og venter på input fra mus eller tastatur, så vil computeren først finde ud af hvornår vi venter på mus eller tastatur. Koden til, at undersøge dette, har større prioritet, end den, der udskriver på skærmen. Herefter, gås så baglands, for at aktivere de koder, som medfører noget på skærm.

Antages, at vi f.eks. laver en masse beregninger som udskrives, skriver en masse sludder, og herefter scroller det hele op, og sletter skærmen. Så vil det, vi har udregnet og skrevet, aldrig blive udført.
Kun det, som vi beregner, og udskriver på skærmen, og hvor vi venter på et "input", således teksten står stille, udføres.

Pauser i programmet, udføres hellerikke. I nogle tilfælde, flyttes pauser, og andet der ikke gøres, til en særlig "tråd" eller CPU, som intet udfører.

Pauser, der er nødvendige, udføres af selve I/O enheden. Ikke af pauser i programmet.

Derved undgås også, at pauser forsinker alle tråde, når det skrives sekventielt.

Lasse Reinholt

Lad mig gå i undervisermode en smule: Hvad nu hvis jeg har 2 skrivninger til hukommelsen. Og jeg ved, grundet en semantisk analyse, at de 2 skrivninger er til forskellige dele af hukommelsen? Kan jeg så ikke bare skrive, thi der kan ikke på nogen måde (i den nærliggende fremtid) være en konflikt mellem skrivningerne.

Og hvis jeg har følgende sekvens af instruktioner (bemærk at der ikke er nogen pointere her. Vi har bare et par registre),

A = 2 * X
B = A / 3
A = 9 * Y
Kan jeg så omordne instruktion 1 og 3 i sekvensen?


Problemet findes på to niveauer. C++ compilere må gerne ændre rækkefølgen af læse- og skriveinstruktioner til memory (*a = 123;) i forhold til rækkefølgen i C++ kildekoden hvis de ikke overlapper (hvornår compileren gerne må gætte på at noget ikke overlapper er en anden diskussion).

Men mange processorer (ARM, POWER, UltraSPARC) må også gerne ændre rækkefølgen af læsninger og skrivninger i forhold til instruktionerne. x86/x64 må dog ikke.

Så alt i alt kan trådning være pænt diffust med mindre man bruger bruger locks/barrierer/fences, som både findes på compiler og arkitekturniveau.

Jens Madsen

Så alt i alt kan trådning være pænt diffust med mindre man bruger bruger locks/barrierer/fences, som både findes på compiler og arkitekturniveau.

Hvis du oversætter din C++ til en "maskinkode", eller "operativsystemkode", der fuldt tillader ombytninger og flytninger, og kan parallelisere koden, således at sekventiel og parallel programmering, er det samme, så tror jeg ikke at "locks" findes direkte, i indstruktionssættet, som compileren oversætter til. Hvis der ønskes en "lock" så er nødvendigt, at specifikt angive de tråde, der skal udføres samlet i rækkefølge. Ellers låses hele koden - også den, der ikke er grund til låses. Samme problem, findes ved delays. Derfor assoiceres delays til henholdsvis indgang, og udgang i stedet. Jeg mener at "locks" gør det samme - som regel, vil det være i forbindelse med en indgang, eller en udgang (eller flere indgange/udgange), at man har behov for, at angive, at de bruges sekventielt. Har du således flere indgange/udgange, til en bestemt enhed, som skal bruges i rækkefølge, så specificeres dette, og den pågældende enhed virker samlet, og de skrivninger, og læsninger, som laves til den samme enhed, vil foregå i rækkefølge. Hvis der ikke sættes krav om, at skrivninger/læsninger, til en enheds porte, foregår i rækkefølge, men tillades at de kan foregå uafhængigt, så defineres de ikke, til at være trådet sammen. Visse egenskaber, såsom trådning, og timing, specificeres normalt i definationen af f.eks. I/O porte. Computerens operativsystem, analyserer sig så til, hvordan at data og timingkrav opfyldes. På den måde "staller" computeren ikke, på grund af I/O, men kan fortsætte med paralle tråde, der fortsætter, og arbejder på andre uafhængige inputs og outputs. Locks, vil derfor - så vidt jeg ved - normalt erklæares ved, at outputs og inputs, erklæres i blokke, der specificeres til, at dataene skal sendes i rækkefølge til enheden.

Lasse Reinholt

så tror jeg ikke at "locks" findes direkte, i indstruktionssættet, som compileren oversætter til.


C++ compilere indskyder aldrig bus lock-instruktioner automatisk... Det kræver et intrinsics statement i din kildekode (findes tit i trådbiblioteker).

Men ellers er det, du skriver om, jo bare lock free programmering :)

Altså at det er unødvendigt at tage en mutex som låser bussen hvis det, man vil have udelt tilgang til kun er en enkelt variabel som man vil udføre en simpel operation på. I så fald er det tilstrækkeligt med div. reordering fences fremfor bus lock.

Nåja, og ang. det du skrev om compileres analyse af C++ pointere - har du set på unique_ptr?

Jens Madsen

Hvis selve programmeringssproget fungerer, som det er sekventielt, og at parallel programmering kun forekommer, fordi du har køer, der kommunikerer imellem datauafhængige dele af koden, så er normalt ikke nødvendigt, at indføre locks. Det som er problemet, er faktisk modsat - at du ikke umiddelbart, kan opnå tilgang, fra to processer, til samme ram lager, uden det foregår i rækkefølge. Dette vil nemligt føre til in-determinisme. I visse tilfælde, kan det være nødvendigt, og så kan tilgangen styres, med en multiplexer, der får en tilfældig værdi, der vælges afhængigt af, hvad det er mest hensigtsmæssigt for udførsel af softwaren. Der findes en random funktion, der giver tilfældig 1 eller 0, som vælges afhængigt af, hvad det er mest optimalt på det pågældende tidspunkt, og som f.eks. bruges ved tilfældig tilgang til lager.

Jens Madsen

Det lyder løjerligt. Har du en reference til den funktion?

Jeg tror, at den kun eksisterer, når selve maskinkode/operativsystem kode, er lavet til, at forstås såvel sekventielt og parallelt samtidigt. Formentligt, er det en "maskinkode" indstruktion, eller operativsystems indstruktion, hvor værdien run-time sættes til den tilfældige værdi, der er mest hensigtsmæssig.

Hvis du ser på "indmaden" af det som udføres, så vil det i nogle tilfælde, behøve at vente, på grund af semaforer, eller trådning. Du kan, med den pågældende funktion, vælge den mulighed, der først giver adgang.

Normalt, vil der opstå en trådning, hvis du f.eks. har to processer, der søger adgang til samme hukommelsescelle eller ram lager. Ved at anvende "tilfældighedsfunktionen", kan du opnå, at du beskriver, at rækkefølgen som du ønsker adgang er uden betydning, og at du altså vælger en tilfældig funktion, afhængigt af din værdi - der så optimeres, til at være bedste valg, så der undgås låsning. I praksis, medfører det, at du kan skrive parallelt til en hukommelsescelle, eller ram, og at funktionen er tilfældig, afhængigt af rækkefølgen - altså, det sker parallelt, og uden trådning. Du går på kompromiss med determinismen, men opnår parallel adgang, da du tillader tilfældig adgang.

Esben Nielsen

Jens,

du snakker om parellisering som en grundlæggende nødvendighed. Jeg ved godt at CPU'er ikke bliver hurtigere, men vi får flere af dem. Men CPU'er er grundlæggende kun egnet til at køre svagt koblede processer, da de grundlæggende gennemtvinger en sekventiel opfattelse af verden. Det er i det paradigme C++ lever. Hvis man vil undgå det, bør man bruge en FPGA og kode i VHDL. Så undgår man helt, at tvinge elektronik til gøre tingene unaturligt sekventielt.

Se lidt på Linux kernen: Den kan skalere op til rigtigt mange CPU'er (512 eller er det 2048?). Men den gør det ved effiktivt at dele tingene op per CPU, v.hj.a. per-CPU variable og strukture - og fælles read-only data.

Jeg er "ked af", at man i C++11 har indført låse og multitrådning som en del af sproget. Det er imod "ånden" i sproget, som jo netop ikke kræver et runtime system. Hvis det var, kunne jeg altid bruge POSIX låse.

Og derfor er det også lidt malplaceret at slå ned på C++, fordi det ikke kan automatisk parelliseres. Det er der bare ikke noget produktionssprog, som kan! Hvis man absolut vil, kan man gøre det med noget kno-fedt. Men vi oprindelige anbefaling, var blot at lade helt være - i alle sprog. Men jo flere libraries/API kald, som er blokerende, man skal bruge, jo sværere bliver det.

Der er i virkeligheden meget, meget få problemer i den virkelige verden, som kræver parellisering. Man bør istedet koncentrere sig om, at optimere sin algoritme til én tråd. Hvis man ikke umiddelbart kan dele den op på højt niveau, så man kan få flere næsten uafhængige tråde, kan man nok kun spilde en masse CPU cycles i synkronisering ved at forsøge at parellisere på lavt niveau. Og intet automatisk system kan gøre det for én.

Lasse Reinholt

Normalt, vil der opstå en trådning, hvis du f.eks. har to processer, der søger adgang til samme hukommelsescelle eller ram lager. Ved at anvende "tilfældighedsfunktionen", kan du opnå, at du beskriver, at rækkefølgen som du ønsker adgang er uden betydning, og at du altså vælger en tilfældig funktion, afhængigt af din værdi - der så optimeres, til at være bedste valg, så der undgås låsning. I praksis, medfører det, at du kan skrive parallelt til en hukommelsescelle, eller ram, og at funktionen er tilfældig, afhængigt af rækkefølgen - altså, det sker parallelt, og uden trådning. Du går på kompromiss med determinismen, men opnår parallel adgang, da du tillader tilfældig adgang.


Hvis to kerner skriver i samme cpu clock, så foregår de altså parallelt på alle arkitekturer da det sker til write buffers som flushes til L1 cachen og videre. En tilfældig af dem ender i main memory. Så hvad er det egentlig for en funktionalitet, du prøver at beskrive? :)

Jeg er "ked af", at man i C++11 har indført låse og multitrådning som en del af sproget. Det er imod "ånden" i sproget, som jo netop ikke kræver et runtime system. Hvis det var, kunne jeg altid bruge POSIX låse.

C++11 concurrency compileres til få simple interlock og fence instruktioner. De kalder ingen eksterne funktioner, hverken operativsystemets tråd-API eller noget køretidssystem eller service. Så jeg forstår ikke lige den kritik.

Jeg synes, det er lækkert, da mange systemer ikke understøtter POSIX og kræver særbehandling.

Esben Nielsen

C++11 concurrency compileres til få simple interlock og fence instruktioner. De kalder ingen eksterne funktioner, hverken operativsystemets tråd-API eller noget køretidssystem eller service. Så jeg forstår ikke lige den kritik.

std::thread, std::mutex skal jo kalde OS'et. Ok, det skal new, malloc, std::cout etc. også.

Men lige præcis med threading skal man ofte alligevel have fat i den underliggende OS på en ikke standeard måde, da scheduler modellen og prioriteter varierer meget fra OS til OS. Også låse kan varierer. Skal der f.eks bruges priority inheritance, priority ceiling eller andre mechanismer, som ikke er standeard på alle OS'er. Skal man kunne bruge en lås i shared memory eller ej?

Det er fint nok, at man pakker ting ind i std::, men når man ikke kan komme i mål med det fordi OS'er er så forskellige, bør man nok lade være.

Jens Madsen

Desværre skrev jeg forkert i går, omkring hvordan "tilfældig" tilgang laves. Der bruges en "tilfældighedsfunktion", som vi ikke kan spå værdien af på forhånd, men det virker lidt anderledes. Det, som den gør, er at teste på, om der er data i datastrømmen. Hvis vi ønsker, at lave tilfældig skriveadgang, til en hukommelse, bruges denne funktion, sammen med if,then,elsif konstruktion. Derved får vi det, som hedder en arbiter. En arbiter, tager et tilfældigt valg (arbitrært valgt), og kaldes derfor en arbiter. Indenfor parallelisme, er det kun, når der bruges en arbiter struktur, at vi risikerer, at der er ting, som sker tilfældigt. Dvs. hvis vi aflæser, om der er data i en datastrøm. Dette er af tilfældig natur.

Esben skriver, at hvis vi ønsker parallel udførsel, så skal vi vælge VHDL. Jeg er helt enig i, at en FPGA, og VHDL, er det mest perfekte valg. Her sker parallelsismen, helt ned til atom-niveau. Og sproget oversætter netop, til en dataflow repræsentation, der lægges ned i FPGA'en.

Der er imidlertid intet om, at du behøver et parallelt sprog, for at få optimal udnyttelse af flere CPU'er. Der er ingen forskel, på et serielt, og et parallelt sprog, med mindre du bruger arbiters, som giver mulighed, for en tilfældig beslutning, ved parallelisme.

Hvis din compiler, paralleliserer dit sekventielle program, så giver det dig friere mulighed når du skriver kode, for at beskrive tingene som du ønsker. Du kan beskrive det parallelt, ved at skrive to koder, der er uafhængige, og kommunikerer imellem disse, ved hjælp af køer, eller variable.

Den primære forskel, på et sekventielt, og et parallet sprog, begynder dermed at være noget i retning af, at det parallele sprog, er det samme som det sekventielle, bortset fra, at indstruktionerne byttes om i rækkefølge, så det er svært at gennemskue.

Du behøver ikke, at have mange CPU'er, før en oversættelse til parallel struktur giver fordele. Det er en stor fordel, selvom der kun er éen CPU. Processorens tid, kan anvendes meget mere fornuftigt. Og hvis du bruger gode algorithmer, til at prioritere hvad din CPU laver, så bruges den langt mere effektivt, og giver i gennemsnit, en hurtigere svartid.

Hvis du f.eks. har en løkke, så kan der i modellen lægges ind, at den bliver mere "træt", når den kører mange gange. Derved, så undgås, at noget kode, er i stand til, at få computeren til at hænge. En løkke der har kørt mange gange, er trættere end en frisk, der kun har kørt få gange. Og derfor, vil computeren stort set ikke kunne "hænge".

Hvis du laver dit operativsystem sprog, således det fra f.eks. C++ compilere, modtager en repræsentation, hvor dataflow kan bestemmes, så er dit program at opfatte som 100% parallelt. Og operativsystemet, sørger så får, at med run-time dynamisk compilering, at omsætte koden, til parallel kode, på enten flere CPU'er, eller at mappe det ned, med korrekte prioriteter, på en CPU, således at den ikke kun bruges effektivt, men at der også tages højde for realtids problemer, såsom svartid, og prioritet. Normalt, prioriteres f.eks. kommunikation til brugeren højt, fordi det øger brugerens oplevelse. Der kan også være interne prioriteter, der sikrer optimal parallelisering. Ofte, kan være "problemer" i koden, som kræver et svar, før parallelisering er muligt. Og disse problemer, ønskes måske løst hurtigt, for at kunne øge parallelismen.

Det, at mappe de i princippet uendeligt mange tråde, effektivt ned i en CPU, hvor du kun har éen tråd, eller et antal CPU'er, hvor du kun har éen tråd, er en mindst ligså stor videnskab, som at gøre et single-trådet program parallel. Det, at analysere for dataafhængigheder, og gøre f.eks. et C++ program parallelt, er det nemmeste. Det er langt sværre, at herefter mappe den parallele beskrivelse, ind i hardwaren. Operativsystemet gør dette, når den omsætter operativsystemets "binære" kode, til hardwaren, i dens in-time compiler.

Normalt skrives såvel programmer, som operativsystemet, i et program, der kan overætte til operativsystemets kode. Det betyder, at alt, i operativsystemet, bliver parallelt, og foregår optimalt. Det gælder også hardware kommunikation, hvor timing specs automatisk opfyldes.

Som et eksempel, kan vi se på, hvad der sker ved opstart i et normalt Linux system, og et operativsystem, hvor C++ programmerne er analyseret, og udføres af operativsystemet parallelt. Det sidste kører langt bedre. F.eks. ved startup, der i nogle tilfælde, tager flere minutter, der indsér et operativsystem, der opfatter det som flere tråde, automatisk at brugergrænsefladen har størst prioritet. De dele, f.eks. håndtering af printer, og andet hardware, hvor tråden ikke direkte involverer noget i brugeradgangen, skubbes disse i baggrunden. Derfor starter brugergrænsefladen op øjeblikkeligt. I nogle tilfælde, kan man også optimere koden, således at f.eks. hvis der udskrives en masse ligegyldigt, og det herefter slettes, så vil computeren helt springe over at såvel udregne dette, som udskrive det. Den går direkte til, at udskrive skærmbilledet for brugeren, som brugeren venter på. Og de ting, som brugeren skal bruge, f.eks. mus og tastatur aktiveres. Mens at det der styrer floppydrev, USB, printer, osv. ikke nødvendigvis kører med det samme. Det har en lavere prioritet, fordi det ikke "bruges". Først, når brugeren ønsker en printerudskrift, så opprioriteres denne process, således den udføres hurtigt. Normalt, vil computeren dog have "initialiseret" den, inden brugern laver en udskrift, da alle processerne, kører med lav prioritet, så CPU'en hele tiden bruges til noget (med mindre, det er low-power).

Det jeg først skrev med, at det bedste var at bruge dataholdere, og pointere adskildt, betyder ikke så meget. Normalt, kan computeren åbentbart finde ud af det ganske effektivt, så du kan bruge sprog som pascal, delphi, og C++. Men det du skriver, deles automatisk ud på mange CPU'er, og hvis der kun er en CPU, så kører det også langt mere effektivt. Brugeren oplever altid en perfekt oplevelse, fordi at kun real-tids processer, hvor der er hardwarekrav til bestemt svartid, kan være prioriteret højere end brugergrænsefladen. Alt andet, er under brugergrænsefladen, da det væsentlige altid er, at give brugeren hurtigst mulig svartid.

De fleste parallel operativsystemer (windows, linux osv). er så vidt jeg ved, lavet forkert, og de håndterer hverken parallelisme, eller prioriteter korrekt. Hensigten virker, at være mere, at give brugeren den værst tænkelige oplevelse, og altid værst tænkelige svartid. Processoren bruges optimalt, men tingene udføres forkert. De er vel nok lavet som practical jokes.

Nikolaj Brinch Jørgensen
Log ind eller opret en konto for at skrive kommentarer

JobfinderJob i it-branchen

TDC skifter koncernchef efter faldende mobilomsætning

Jesper Stein Sandal Mobil og tele 14. aug 2015

Nyeste job

KurserStyrk dine evner med et kursus

Business Intelligence med SharePoint 2013

Hvornår: Hvor: Østjylland Pris: kr. Efter aftale

Excel 2007 Videregående

Hvornår: 2015-09-16 Hvor: Østjylland Pris: kr. 4950.00

Auditorkursus i kvalitetsledelse

Hvornår: 2015-11-05 Hvor: Fyn Pris: kr. 10400.00

DB2 Programming Workshop for Linux, UNIX and Windows [CL100G]

Hvornår: 2015-09-07 Hvor: Storkøbenhavn Pris: kr. 18000.00

Power View kursus grundlæggende

Hvornår: 2015-12-21 Hvor: Storkøbenhavn Pris: kr. 3500.00