C++ och dess framtid att programmera minnessäkert - Hur går utvecklingen?

Permalänk
Medlem
Skrivet av klk:

Att en del i SAP är skrivet i C/C++ tror jag beror på omfattningen och deras krav på flexibilitet, de måste själva ha ett grundläggande ramverk som gör att de kan anpassa deras system. Deras databaser är gigantiska i förhållande till andra "vanliga" system men de tar också rejält betalt.

Anledningen är snarare att det inte fanns några alternativ alls på den tiden det begav sig.

Den ursprungliga versionen av SAP skrevs garanterat inte i C++, och knappast i C heller. C++ hade inte uppfunnits ännu, och C kom till ungefär samtidigt som första versionen av SAP.

Ett par versioner av SAP senare så är vi på 90-talet. Om man då skulle skriva program som skulle fungera både på Windows och under Unix (och SAP portades till bägge) med grafiskt användargränsnitt så fanns det helt enkelt inte mycket att välja på annat än C/C++.
Idag finns det mängder av mer eller mindre lämpliga alternativ. Det fanns det inte då.

Permalänk
Datavetare
Skrivet av klk:

Och dessa jxxxa NuGet'ar kan driva utvecklare till vansinne. Tyvärr är kvaliteten fantastiskt låg eftersom det är så enkelt att sprida NuGettar och de som aldrig varit med om att nya versioner av .NET kommer och annat inte är anpassat utan måste ligga kvar vet inte vad som väntar. Har deltagit i arbete som varat i månader (närmare halvår) endast för att lyfta C# projekt

Så problemet var inte brist på 3:e-parts bibliotek, utan det är tvärtom så att det finns för många?

Du hävdar att du jobbat med C# längre upp, du är medveten om att assemblies specificerar vilken version de minst kräver och att de normalt fungerar framåt?

Skrivet av klk:

Störst problem i C# enligt mig är att allt är objekt och svårigheterna att jobba direkt med minnet. Jag skulle gärna se någon lite mer avancerad trådad lösning i C# där utvecklare måste ha koll på hur synkronisering av minne minimerats för att få upp hastigheten. Det är en typ av uppgift som enligt mig måste vara mycket svår i språket

C#, till skillnad från t.ex. Java, har både referens types och value types. Hur vet du inte det om du programmerat C#?

C# har inbyggt stöd för atomära operationer + det har tillgång till de normala OS-primitiverna för synkronisering, vad är det du saknar? Så skulle hävda att de atomära operationerna är rätt analoga med de man hittar i C++11, C# har ett bredare stöd för vanliga OS-primitiver jämfört med std c++.

Skrivet av klk:

Största SAP databasen jag arbetat med hade över 3000 tabeller och det fanns tabeller med över 1000 kolumner. Vill se det ramverk som fixar det där. SAP har speciella system för att sköta röran

Beklagar. Jag lär aldrig ta mig nära ett SAP-projekt, fast låter lite som något man knappast behöver något mer än lite Python-script till...

Skrivet av klk:

Förstår du hur duktiga de utvecklarna var/är som klarade av det?

Självklart, vi som knackade spel/demos på 80-talet i assembler har en speciell aura

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 klk:

Beror på vad du menar med "sämre", de hade haft andra kunskaper och inte behövt kämpa lika mycket för att lära sig. Då var det riktigt tufft att lära sig programmera, man behövde anstränga sig ordentligt.
Idag kan nära nog vem som helst lära sig skriva kod och göra något på en vecka om man lägger manken till

Självklart är det personer som blir nördiga och lär sig annorlunda tekniker idag med men de kan inte prata öppet om det

Tröskeln för att börja programmera assembler var(är) hög, men vad är det egentligen som skiljer assemblerkodande från vem som helst-programmering?

Det krävs i stort sett samma skill set. Du behöver kunna överblicka problemet och bryta ner det i mindre delar och när delproblemen är tillräckligt små måste du mentalt kunna omvandla det till programmeringens grundläggande Legobitar: sekvens, selektion och iteration. Mängden tillgängliga variabler (register) är begränsad och de har konstiga förutbestämda namn, och ibland även användningsområden. Har du fler variabler får du spara dem någon annanstans (stack eller minne) och använda registren som "cache". Selektion och iteration är krångligare att göra i assembler än att skriva if-satser eller for-loopar, men när du väl gjort det ett par gånger så vet du hur man gör. Funktionsanrop blir krångligare. Hur skickas parametrarna? Register, minne? Beror på hur många man vill skicka, men å andra sidan har du full frihet att använda de register du råkar ha lediga i caller. Här är det frihet under ansvar

68K- och Z80-assembler i sig är faktiskt inte så långt från C som man skulle kunna tro. 6502-assembler är en annan femma, det är väl närmast att likna vid en stackmaskin. Zero-page och andra skojigheter gjorde att det inte var en lika rättfram mappning mellan assembler och vem som helst-programmering.

Men vad skiljer då?

En avgörande sak är att du inte har något standardbibliotek med bra-att-ha-funktioner. Tänk C, men inget libc. Man fick göra mer själv.

Ofta skulle det interageras med någon form av hårdvara (grafik, ljud) och då krävdes det att man kunde den delen utan och innan. Här finns det normalt ett par abstraktionslager ovanpå för vem som helst-programmeraren.

När mina gamla kollegor skrev/gjorde ändringar i ABC80-BASICen hände det att BASIC och OS inte rymdes i 16KB. Ett av sätten de använde var att hoppa in halvvägs in i andra funktioner där svansen gjorde precis det man ville. Det var överkurs att hoppa in mitt i andra instruktioner. Z80 hade en del multibyte-instruktioner och byte-sekvensen efter prefixet kunde vara en eller flera giltiga instruktioner som gjorde något helt annat. Detta är en konstart som vem som helst-programmeraren slipper ägna sig åt

Permalänk
Medlem
Skrivet av Yoshman:

Så problemet var inte brist på 3:e-parts bibliotek, utan det är tvärtom så att det finns för många?

Vet du hur NUGET packages används? Internt i företag vimlar det av dem. När jag skrev C# fanns det över 500 microservices på det företaget, ingen vågade radera något eftersom man inte visste vad som skulle kunna gå sönder. Till slut gjordes det ändå och så fick man invänta problemen

Skrivet av Yoshman:

Du hävdar att du jobbat med C# längre upp, du är medveten om att assemblies specificerar vilken version de minst kräver och att de normalt fungerar framåt?

Spelar det någon roll när kvaliteten på koden är värdelös
Företag som väljer och utveckla så som många gör med C#, där slutar utvecklare bry sig om koden, man går till jobbet för att få ut sin lön, gör det man måste men inte mer än så

Permalänk
Datavetare
Skrivet av klk:

Vet du hur NUGET packages används? Internt i företag vimlar det av dem. När jag skrev C# fanns det över 500 microservices på det företaget, ingen vågade radera något eftersom man inte visste vad som skulle kunna gå sönder. Till slut gjordes det ändå och så fick man invänta problemen

Räcker att veta hur jag själv vill använda dem. Givet att det finns >400k paket får man räkna med att de hel del är junk, men är inte precis svårt att hitta pärlor.

Här är några som har fungerat lysande

https://www.nuget.org/packages/Vortice.DirectX/
https://www.nuget.org/packages/google.protobuf/
https://www.nuget.org/packages/Google.OrTools/
https://www.nuget.org/packages/YamlDotNet/
https://www.nuget.org/packages/commandlineparser/
https://www.nuget.org/packages/serilog/
https://www.nuget.org/packages/mqttnet/

Men du kanske anser att det är en förbättring när det är komplicerat som med C och C++ där man aldrig enats om en standard för byggsystem, där man aldrig fått en package-manager med samma unisona uppslutning som NuGet för C#, Cargo för Rust, pip för Python.

Sen finns det andra som "tänker utanför boxen" som Go, där är normalfallet att moduler inkluderas enbart via deras github länkar!

Skrivet av klk:

Spelar det någon roll när kvaliteten på koden är värdelös
Företag som väljer och utveckla så som många gör med C# där slutar utvecklare bry sig om koden, man går till jobbet för att få ut sin lön, gör det man måste men inte mer än så

Det kan man ju göra oavsett språk. Och det finns också fall där de iblandade bryr sig väldigt mycket och resultatet är lysande, det oavsett 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
Medlem
Skrivet av Yoshman:

Räcker att veta hur jag själv vill använda dem. Givet att det finns >400k paket får man räkna med att de hel del är junk, men är inte precis svårt att hitta pärlor.

Här är några som har fungerat lysande

Tänk på att flera där är C/C++

Permalänk
Datavetare
Skrivet av klk:

Tänk på att flera där är C/C++

Vad spelar det för roll? Har du tänkt på att alla C/C++ projekt egentligen bara körs som maskinkod, blir C och C++ värdelösa då? För är ju bara 80-tals assemblerhjältar som riktigt vet hur man kodar "på riktigt"...

Olika språk har olika optimeringspunkter. En viktig orsak att C# tagit över t.ex. applikationsutveckling på Windows från C++ är att man från start valde att designa FFI-delen i .NET så det är i stort sett noll overhead att anropa Win32 API (och alla andra C-baserade APIer). Också därför spel fungerar väl med C#, det är effektivt att jobba med DirectX, Vulkan eller vad man nu kör.

Tar vi Vortice t.ex. så får man i många fall mer begripliga felmeddelande jämfört med att köra DirectX från C++. Så även i fallen där det rätt mycket "bara" en FFI-wrapper på ett C-bibliotek så får man, om wrappern är bra designad, fördelarna från att jobba med ett minnessäkert språk + prestandafördelarna att kunna köra med C-bibliotek. Win/win och frågan man bör ställa sig: varför skulle jag i det läget välja att harva vidare i C++?

I fallet Rust är det också "gratis" att anropa in i C. Är också trivialt att anropa in i C om man accepterar "unsafe". Även här är de bra wrapperna gjorda så att de utnyttjar existerade vältestade C-bibliotek, men de lägger på "tillräckligt" mycket Rust så man får till livstidsanalys och liknande vilket då ger säkerhetsfördelarna (minus eventuella buggar i C-koden).

Go har valt en annan modell. Där är det fortfarande väldigt lätt att använda C-bibliotek via FFI, men p.g.a. hur man valt att designa Go:s runtime (som är optimerad för CSP-style tänk) så är det inte speciellt effekt. En effekt av det ser man i att långt fler populära Go-bibliotek är inte wrappers på C-bibliotek, de är i långt större utsträckning rewrites i Go. Det har också för och nackdelar, den uppenbara fördelen är att Go-kod är typiskt trivial att köra på alla OS/CPU-arch som tool-chain stödjer, vilket kan vara värdefullt inte minst i vissa embedded projekt (och det är av flera orsaker till varför Go blivit så populärt i "molnet").

Visa signatur

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

Permalänk
Medlem

Börjar påminna lite om diskussioner med exfrun här, finns ingen logik i världen som hjälper. Slösa inte mer energi @Yoshman, det går inte ha rätt. Pratar av erfarenhet

Visa signatur

Citera mig för svar.
Arch Linux

Permalänk
Medlem
Skrivet av Yoshman:

Vad spelar det för roll? Har du tänkt på att alla C/C++ projekt egentligen bara körs som maskinkod, blir C och C++ värdelösa då? För är ju bara 80-tals assemblerhjältar som riktigt vet hur man kodar "på riktigt"...

Ja och en stor fördel med att knacka lite assembler är att man lär sig hur processorn fungerar.

Angående dina andra kommenterar kring språk så absolut, de är enklare så för programmerare som inte vill ödsla tid på att nörda ner sig eller som vi så många gånger sagt, inte vill skapa egen funktionalitet mer än det nödvändiga så finns dessa. Men fortfarande, det som är lätt kan plötsligt bli svårt för det finns saker som arkitektur och smarta lösningar för att lösa arkitekturen bättre. Då är det bra att ha tränat sig i att jobba direkt mot minnet för de som gjort det förstår så mycket bättre vad de kan göra.

Och enligt mig så dessa förenklingar de enklare språken har har i princip försvunnit i och med AI. C++ är exempelvis inte svårt längre för alla dessa löjligt svåra felmeddelande som framförallt var svåra för det sprutat ut sådana enorma textmassor. Det där är hemmaplan för AI. C++ har plötsligt blivit enkelt.

Språkval med AI spelar mycket mindre roll, nu handlar det mycket mer om programmerare som kan använda språket för att lösa problem.

Permalänk
Medlem
Skrivet av klk:

Ja och en stor fördel med att knacka lite assembler är att man lär sig hur processorn fungerar.

Nja, njae, nej inte nödvändigtvis.

När jag knackade 6502-maskinkod (Sån lyx som en assembler hade vi inte på den tiden!) på min gamla C64 på 80-talet så lärde jag mig en hel del om hur den processorn och datorn fungerade.

Hur mycket av det hjälper mig att förstå hur en modern processor fungerar, med cache-minne i många nivåer, pre-fetch av både instruktioner och data, pipe-linad instruktionsavkodning, multipla kärnor, spekulativ exekvering, m.m?
Inget av det där fanns på C64:an, eller på andra klassiska 8-bitars datorer. så sånt lärde jag mig ingenting om genom att knacka 6502-kod!

Jag har kodat assembler på lite modernare processorer också, men det mesta jag vet om hur de fungerar har jag inte lärt mig genom att knacka assembler utan genom att läsa böcker och artiklar som beskriver moderna processorer och hur de fungerar.

Permalänk
Medlem
Skrivet av Erik_T:

Jag har kodat assembler på lite modernare processorer också, men det mesta jag vet om hur de fungerar har jag inte lärt mig genom att knacka assembler utan genom att läsa böcker och artiklar som beskriver moderna processorer och hur de fungerar.

Vad jag menar med att förstå hur en processor fungerar handlar om hur du skriver kod för att dra nytta av hastigheten. En fördel är att ha knackat assembler och därmed lärt sig debugga assemblerkod, man kan självklart lära sig det på andra sätt med men inte lika bra. Det är effektivt att praktisera något för då lär man sig ordentligt.

Pröva och se om du kan lära ut programmering till någon utan att vederbörande skriver en enda kodrad. Flummig teori är inte samma sak som att man vet

Permalänk
Medlem
Skrivet av klk:

Vad jag menar med att förstå hur en processor fungerar handlar om hur du skriver kod för att dra nytta av hastigheten.

Hur jag optimerar kod på en 6502 är väldigt annorlunda mot hur man optimerar kod på en modern x86-64 eller arm64 processor.
På en gammal 6502 (eller z80, eller andra processorer från den tiden) så handlar det mycket om att minimera mängden instruktioner, och att minimera mängden data som används så det får plats i minnet.

För en modern CPU är det helt andra kriterier. Till en första approximation så är instruktionsexekvering gratis på en modern CPU. Det är minnesaccesser som tar tid. En modern CPU har oftast multipla kärnor, så att parallellisera algoritmer (i den mån det är möjligt) är också en viktig del i att optimera kod för en modern CPU.

Väldigt många av de trick man lärde sig på 80- och 90-talet för hur man skulle dra nytta av hastigheten i en processor är sorgligt föråldrade idag, och kan rent av försämra prestandan.

Permalänk
Medlem
Skrivet av Erik_T:

Väldigt många av de trick man lärde sig på 80- och 90-talet för hur man skulle dra nytta av hastigheten i en processor är sorgligt föråldrade idag, och kan rent av försämra prestandan.

Ja, det tjänar ju inte mycket till att handoptimera vanlig assemblerkod när out-of-order-logiken stuvar om det efter eget tycke vid exekveringen (infogandet av SSE/AVX-kod är väl undantaget).

Zen 5 har en reorder-buffer på 448 mikro-op-koder, så det är inga små kodsegment som hanteras internt i varje ögonblick. Fördelen är naturligtvis ett effektivt stuvande av pipelinen för att nyttja beräkningsenheterna maximalt, men även för att ge tid till förladdningen (prefetch) av cachen i de fall vi riskerar att få en cache-miss.

Nutidens processorer är verkligen små mångsidiga data-fabriker, väsenskilda från 80-talets by-smedjor.

Permalänk
Medlem
Skrivet av mc68000:

Nutidens processorer är verkligen små mångsidiga data-fabriker, väsenskilda från 80-talets by-smedjor.

Enligt mig är det framförallt kompilatorerna som gått framåt och kan göra så mycket mer med koden för att optimera maskinkoden för olika processorer och det är bra om vet vilken typ av kod som kompilatorn har lättare att optimera kontra vad som är svårare.

Permalänk
Skrivet av klk:

Enligt mig är det framförallt kompilatorerna som gått framåt och kan göra så mycket mer med koden för att optimera maskinkoden för olika processorer och det är bra om vet vilken typ av kod som kompilatorn har lättare att optimera kontra vad som är svårare.

Hmm, jag skulle nog snarare säga att det har blivit mindre viktigt för kompilatorn att optimera koden för olika processorer. En del saker kompilatorerna gjorde tidigare, som software pipelining och instruction scheduling i allmänhet (där man var tvungen att se till att det fanns oberoende instruktioner mellan exempelvis en load och när resultatet skulle användas för att undvika bubblor i pipelinen), inte längre är lika relevanta. Detta gör en modern CPU bättre på egen hand och kompilatormakaren behöver inte hålla koll på om det var fyra eller fem cyklers latency innan multiplikationsresultatet fanns tillgängligt. Finns det oberoende instruktioner kommer de köras medan man väntar på resultatet.

Fokus har skiftat lite till att ge CPUn bättre förutsättningar till att maximera nyttan av exempelvis djupa reorder-buffrar. Mer aggressiv unrolling och inlining där det ger större sektioner med rak kod där CPUn kan göra loads tidigt så resultaten redan finns i cachen när värdena skall användas.

Jag blir lite nyfiken på vad du menar med "det är bra om vet vilken typ av kod som kompilatorn har lättare att optimera kontra vad som är svårare". Kan du ge exempel på kod som skulle vara lätt/svår att optimera och hur du skulle skriva om den svåra koden så den blev lättare för kompilatorn att optimera?

Permalänk
Medlem
Skrivet av Ingetledigtnamn:

Hmm, jag skulle nog snarare säga att det har blivit mindre viktigt för kompilatorn att optimera koden för olika processorer.

Det tror inte jag

Minns när Intel åkte på en hel massa kritik för att deras kompilator favoriserade deras processorer. Kommer inte ihåg exakt vilken metod de hade optimerat nu men det var något i stil med memcpy eller liknande. Kompilatorn hade en inbyggd kontroll som kollade om det var en intelprocessor och körde isåfall andra instruktioner (avx). Om det inte var Intel (läs: AMD) kördes långsammare kod.

Idag behöver de inte optimera metoder som memset, strcpy och liknande för det klarar kompilatorn av att göra lika bra (eller bättre).

Ta den här metoden

void strcpy(char* piTo, const char* piFrom) { while (*piTo++ = *piFrom++); }

Om maskinkoden blir en exakt kopia av koden ovan blir det inte alls så snabbt som det skulle kunna bli. Men med lite flaggor och annat kommer den genererade maskinkoden att se mycket annorlunda ut.

Att se vilken kod som genereras baserat på vad man skriver är intressant för då får man bättre förståelse vilket jobb de lagt på att skapa bästa möjliga kod. Bolag har troligen varit beredda på att plöja ner en hel del pengar i sådant här eftersom det sparar ström och annat om kompilatorn klarar skapa bättre kod. Också att processortillverkare är intresserade av att kod till deras processorer blir så bra som möjligt.

Kompilatorn är ett viktigt verktyg och också viktigt att utvecklare förstår hur verktyget används på bästa möjliga sätt

Permalänk
Skrivet av klk:

Det tror inte jag

Minns när Intel åkte på en hel massa kritik för att deras kompilator favoriserade deras processorer. Kommer inte ihåg exakt vilken metod de hade optimerat nu men det var något i stil med memcpy eller liknande. Kompilatorn hade en inbyggd kontroll som kollade om det var en intelprocessor och körde isåfall andra instruktioner (avx). Om det inte var Intel (läs: AMD) kördes långsammare kod.

OK, så när du skriver olika processorer så innebär "olika" X86, Arm och Risc-V? Som jag uppfattat det består din värld av Windows på X86 och tolkade "olika" processorer som Intel/AMD och olika generationer av dessa. Men om vi pratar olika ISA så har ju tillverkarna självklart incitament att producera så bra kod som möjligt för just sina processorer.

Skrivet av klk:

Idag behöver de inte optimera metoder som memset, strcpy och liknande för det klarar kompilatorn av att göra lika bra (eller bättre).

Ta den här metoden

void strcpy(char* piTo, const char* piFrom) { while (*piTo++ = *piFrom++); }

Om maskinkoden blir en exakt kopia av koden ovan blir det inte alls så snabbt som det skulle kunna bli. Men med lite flaggor och annat kommer den genererade maskinkoden att se mycket annorlunda ut.

Klassisk strcpy är knepig att optimera då du riskerar aliasing mellan piTo och piFrom. Om piTo råkar vara piFrom + 1 blir det fel om du gör bredare läsningar än 1 char i taget. Här skulle kompilatorn kunna testa för överlapp och generera två olika versioner av loopen, en snabbare och en långsam som man kör om det är överlapp. Men sedan får man ju inte läsa via piFrom bortanför den första \0 man hittar. Det gör det svårt att vektorisera. Risken är nog överhängande att det blir just en kopia av koden ovan. Vilka flaggor hjälper här? Vad skulle du ändra för att få det lätt att optimera?

Permalänk
Medlem
Skrivet av Ingetledigtnamn:

Klassisk strcpy är knepig att optimera då du riskerar aliasing mellan piTo och piFrom. Om piTo råkar vara piFrom + 1 blir det fel om du gör bredare läsningar än 1 char i taget. Här skulle kompilatorn kunna testa för överlapp och generera två olika versioner av loopen, en snabbare och en långsam som man kör om det är överlapp. Men sedan får man ju inte läsa via piFrom bortanför den första \0 man hittar. Det gör det svårt att vektorisera. Risken är nog överhängande att det blir just en kopia av koden ovan. Vilka flaggor hjälper här? Vad skulle du ändra för att få det lätt att optimera?

Är det överlapp så är det undefined behavior, så det fallet kan kompilator och bibliotek glatt strunta i.
Det gäller för de flesta funktioner i standard biblioteket för C som kopierar minnesblock - överlapp mellan källa och mål ger undefined behavior.
Ett viktigt undantag är memmove() som är definierad även för fallet att det är överlapp.

Så länge det anropande programmet inte kan märka det, så kan standardfunktioner som strcpy() i princip läsa bortom slutet på strängar utan att göra fel. Implementationen av standardfunktioner behöver inte vara gjord i C, eller följa reglerna för C.

Det här är ett område där åtminstone tidvis (jag är inte säker på hur det ser ut just nu), kompilatortillverkarna utnyttjar vad standarden lovar för att ge bättre optimeringar men samtidigt försämra användbarheten av C för många klassiska användningsområden.

Eftersom undefined behavior (UB) innebär att kompilatorn/koden får göra precis vad som helst (så vitt det angår C standarden åtminstone), så antar en del kompilatorer att UB inte kan inträffa och kan därmed göra en del extra optimeringar baserade på det antagandet.
Problemet är att en hel del som är UB i C standarden kan ha användbart beteende i maskinspecifik kod. T.ex. så kanske det ligger något intressant på address 0 i ett visst system, men eftersom det är UB i C att de-referera en null-pekare så kan kompilatorn optimera bort alla försök att läsa från address 0.

Så, försök att göra C snabbare för generell kod kan ställa till det rejält när man försöker använda C som "portabel assembler" som ju åtminstone förr var ett viktigt användningsområde för C. Problemet ligger inte i C standarden som sådan, utan i hur kompilatorförfattare väljer att tolka och använda den.

Permalänk
Datavetare
Skrivet av Erik_T:

Är det överlapp så är det undefined behavior, så det fallet kan kompilator och bibliotek glatt strunta i.
Det gäller för de flesta funktioner i standard biblioteket för C som kopierar minnesblock - överlapp mellan källa och mål ger undefined behavior.
Ett viktigt undantag är memmove() som är definierad även för fallet att det är överlapp.

Så länge det anropande programmet inte kan märka det, så kan standardfunktioner som strcpy() i princip läsa bortom slutet på strängar utan att göra fel. Implementationen av standardfunktioner behöver inte vara gjord i C, eller följa reglerna för C.

Det här är ett område där åtminstone tidvis (jag är inte säker på hur det ser ut just nu), kompilatortillverkarna utnyttjar vad standarden lovar för att ge bättre optimeringar men samtidigt försämra användbarheten av C för många klassiska användningsområden.

Eftersom undefined behavior (UB) innebär att kompilatorn/koden får göra precis vad som helst (så vitt det angår C standarden åtminstone), så antar en del kompilatorer att UB inte kan inträffa och kan därmed göra en del extra optimeringar baserade på det antagandet.
Problemet är att en hel del som är UB i C standarden kan ha användbart beteende i maskinspecifik kod. T.ex. så kanske det ligger något intressant på address 0 i ett visst system, men eftersom det är UB i C att de-referera en null-pekare så kan kompilatorn optimera bort alla försök att läsa från address 0.

Så, försök att göra C snabbare för generell kod kan ställa till det rejält när man försöker använda C som "portabel assembler" som ju åtminstone förr var ett viktigt användningsområde för C. Problemet ligger inte i C standarden som sådan, utan i hur kompilatorförfattare väljer att tolka och använda den.

Det är mer komplicerat...

C11 standard säger att en implementation av strcpy får förutsätta att det inte finns något överlapp och göra optimeringar därefter. Det @klk visar ovan får inte gör det antagandet då inget i den definition han ger säger att överlapp är otillåtet.

void strpy_no_overlap(char * restrict dst, const char * restrict src) { while (*dst++ = *src++) {} }

Ovan hjälper marginellt (nu har man berättat för C-kompilatorn att den kan förutsätta att det inte finns överlapp) då man forfarande har kvar restriktionen att vilken byte som helst kan vara '\0', så går fortfarande inte att läsa mer än en byte i taget

Visa signatur

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

Permalänk
Datavetare
Skrivet av klk:

Ja och en stor fördel med att knacka lite assembler är att man lär sig hur processorn fungerar.

Du är verkligen kvar i en svunnen tid.

Ja, på en 68k kunde man helt förlita sig på att xxx.l dX,dY gav 3 cykler per byte, xxx.w dX,dY gav 4 cykler per byte samt xxx.b dX,dY gav 8 cykler per byte.

I en modern CPU är cykelräkning i praktiken helt omöjligt då load/store latens typiskt dominerar vilken perf/Hz man kan få ut, det kombinerat med hur långa sekvenserna med beroende operationer är.

Läs på om erfarenheterna från "C++26: Standard library hardening". Trots att det resulterar i rätt mycket overhead i grundläggande operationer så blir påverkan i moderna CPUer <1 % kostnad. Du verkar överhuvudtaget inte greppa hur moderna CPUer hanterar "minne", erfarenhet och trix från 80 och tidigt 90-tal (in-order designs) är nära nog helt irrelevanta idag.

Just då latens och möjlighet att ha endera ha mer frihet att göra saker "senare" eller ännu bättre "senare potentiellt på en annan CPU-kärna" är långt viktigare idag än att försöka få bort en instruktion eller två. Det är ännu en i rader av orsaker varför saker som GC, som historiskt var relativt dyrt, idag kan vara snabbare än manuell minneshantering. Med compacting kan allokering vara så enkelt som en pointer-add och de-allokering sker överhuvudtaget inte på den kritiska app-tråden, långt snabbare än en typisk C/C++ heap-allokator.

Det finns ändå optimeringar som kan göras, men vad som är "rätt" optimering beror idag långt mer på dynamiskt accessmönster mot RAM än hur många instruktioner det råkar bli.

State-of-the-art här måste ju därför bli JIT då man där kan ta input från faktiskt dynamiskt beteenden och sedan optimera assembler för det. DET är orsaken till varför JVM-språk idag kan göra det vi lite drog på smilbanden åt på 90-talet: vara snabbare än optimerad C/C++ kod!

Visa signatur

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

Permalänk
Medlem

@klk Rekommenderar dessa analyser av några olika processorer på en nivå som lär uppskattas.

Senast ut är AMD’s Zen 5 (9900X)
https://chipsandcheese.com/p/running-gaming-workloads-through

Motsvarande diskussion för Skymont (E-kärnor)
https://chipsandcheese.com/p/skymont-in-gaming-workloads
https://chipsandcheese.com/p/skymont-intels-e-cores-reach-for...

Samt för Lion Cove (285K)
https://chipsandcheese.com/p/intels-lion-cove-p-core-and-gami...

Permalänk
Medlem
Skrivet av Yoshman:

State-of-the-art här måste ju därför bli JIT då man där kan ta input från faktiskt dynamiskt beteenden och sedan optimera assembler för det. DET är orsaken till varför JVM-språk idag kan göra det vi lite drog på smilbanden åt på 90-talet: vara snabbare än optimerad C/C++ kod!

Tror jag hört den typ av argumentation sedan java. Vet att den första typ av argumentation handlade om att nya allokerade block i java med GC inte letade efter lediga utrymmen utan bara tog nästan fria. Ett av argumenten som användes för att beskriva varför java var så snabbt.
Problemet var bara att java inte var snabbt När de började ploppa ut program var de fasligt långsamma. Det har bättrat sig med det är fortfarande segt.

Två största problemen med språk som använder GC är att de förbrukar normalt mycket mer minne vilket försämrar lokalitet och att de mer eller mindre struntar i hur minnet placeras, allt lämnas över till kompilatorn.
Det blir inte snabbt. Att vissa saker ändå har ok hastighet beror på att runtimebiblitek som används ofta är optimerade.

En genomtänkt minnesarkitektur kommer vara mycket snabbare än kod med GC där de struntat i samma sak.

Eftersom det troligen kommer bli vanligare att sprida arbeten till flera kärnor antar jag att minne och hur minnet hanteras blir viktigare men att det då handlar om synkronisering. Minimera den typen av kod

Permalänk
Datavetare
Skrivet av klk:

Tror jag hört den typ av argumentation sedan java. Vet att den första typ av argumentation handlade om att nya allokerade block i java med GC inte letade efter lediga utrymmen utan bara tog nästan fria. Ett av argumenten som användes för att beskriva varför java var så snabbt.
Problemet var bara att java inte var snabbt När de började ploppa ut program var de fasligt långsamma. Det har bättrat sig med det är fortfarande segt.

Två största problemen med språk som använder GC är att de förbrukar normalt mycket mer minne vilket försämrar lokalitet och att de mer eller mindre struntar i hur minnet placeras, allt lämnas över till kompilatorn.
Det blir inte snabbt. Att vissa saker ändå har ok hastighet beror på att runtimebiblitek som används ofta är optimerade.

En genomtänkt minnesarkitektur kommer vara mycket snabbare än kod med GC där de struntat i samma sak.

Läs vad @mc68000 säger om storleken på ROB – den har ökat drastiskt de senaste åren. Faktum är att de 448 instruktioner som Zen 5 stoltserar med må vara väldigt mycket jämfört med CPU:er bara ett par generationer tillbaka, men Lion Cove har 576 instruktioner och Apple M4 samt Arm Cortex X925 ligger runt 800 instruktioner.

Detta påverkar återigen flera gamla sanningar. Framför allt ger det större möjligheter att "gömma latens mot RAM". Om spekulation om framtiden är lätt att göra, så borde det rimligen vara än enklare för något så enkelt som en bump allocator.

Det är alltså fullt möjligt att påståendet "JVM snabbare än C/C++" faktiskt inte var praktiskt möjligt när Java var nytt, men att det är det idag på grund av hur hårdvaran har förändrats.

Samtidigt är ett problem med GC-språk att deras prestanda delvis är en funktion av hur väl man kan approximera "det finns oändligt med RAM". Men även här har det hänt mycket, och det finns långt ifrån en sanning längre.

Server-JVM:er verkar vilja ha ganska stort working set. De är rätt minneshungriga. .NET-klienten och även Go:s GC prioriterar däremot latens framför rå throughput och har inte alls särskilt stor overhead. Python (och Swift) tar det ytterligare ett steg genom att använda automatisk referensräkning vilket har samma RAM-overhead som manuell minneshantering (Python hanterar cykler med en mer traditionell GC).

Vidare, om man gör något som är soft real-time, t.ex. spel, så kommer man av de skälen hålla nere mängden kortlivad data, vilket ytterligare minskar gapet mellan manuell minneshantering och GC när det gäller working set. Och den skillnad som finns kvar kan man ofta kompensera för om man har multitrådade applikationer, eftersom det är lättare att skriva lock-/wait-free-algoritmer i en GC-miljö jämfört med när man hanterar minne med malloc/free eller new/delete.

Visa signatur

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

Permalänk
Medlem
Skrivet av Yoshman:

Läs vad @mc68000 säger om storlek på ROB, dess storlek har ökat drastiskt på senare år. Faktum är att de 448 instruktioner Zen 5 stoltserar med må vara väldigt stort jämfört med CPUer ett par generationer innan, men Lion Cove har 576 instruktioner och Apple M4 samt Arm Cortex X925 har båda passerat 800 instruktioner.

Detta åter igen påverkan hos "gamla sanningar". Framförallt ger det större möjligheter att "gömma latens mot RAM". Så om bara spekulation om framtiden är lätt att göra, vilket rimligen borde vara fallet för en enkelt "bump allocator".

Vad spelar det för roll när det dräller av klasser och dess medlemsvariabler, menar du att det nya moderna "skriver om koden"?

Permalänk
Medlem
Skrivet av klk:

Vad spelar det för roll när det dräller av klasser och dess medlemsvariabler, menar du att det nya moderna "skriver om koden"?

Vad har antalet klasser med det hela att göra?

Ja, en modern processor "skriver om" koden rätt rejält. Den delar upp instruktioner i mikroinstruktioner som sen kan köras i olika ordning eller köras spekulativt, och delvis kan köras parallellt, där exakt hur de körs kan variera från CPU modell till modell. Naturligtvis med begränsningar så att semantiken inte förändras, men exakt hur koden kommer att exekveras är nästan omöjligt att avgöra bara genom att läsa maskinkoden, och ännu svårare att gissa baserat på ursprungliga källkoden.

Det är egentligen inget nytt att göra sånt i processorn. Stordatorer har gjort delar av ovanstående sedan innan många av oss ens var födda (IBM System/360 Model 91, från 1960-talet var antagligen först med Out-of-Order Execution), och i PC-världen har det gjorts åtminstone sedan gamla Pentium Pro (släppt 1995), även om det har blivit mycket mer avancerat och effektivt sedan dess.

Permalänk
Medlem
Skrivet av klk:

Vad spelar det för roll när det dräller av klasser och dess medlemsvariabler, menar du att det nya moderna "skriver om koden"?

Nu får Yoshman svara för sig, men enligt min mening, om du syftar på processorn: Ja!

Det du kodar, kanske lägger ner tid på att finurligt placera kod och data på ett bra sätt, lär först manglas sönder av kompilatorns optimeringsalgoritmer. Den koden, som om man tittar på den redan nu är ganska oigenkännlig (i alla fall så ger den mycket huvudbry om "vaffö gör di på detta viset?"). Redan små förändringar i koden kan få "-O3" att fullständigt kasta om saker likt en glad tombola på ett nöjesfält. (Vilket gör det hopplöst att prestandatesta utan verktyg på den här nivån.)

När programmet sedan exekveras så rullar instruktionerna in till dekodern som översätter assemblerinstruktionerna till mikro-operationer som slussas vidare till ROB (reorder buffer) Dessa mikro-operationer analyseras hela tiden och om någon operation saknar data i cachen så måste det först läsas in från minnet innan just de operationerna kan släppas fram. Under tiden så har processorn andra mikro-op att välja på som kan få gå före (Out-of-order). Om det finns ett beroende så hanteras det i efterhand läste jag precis. Magi ja, men någonting skall väl antalet miljarder transistorer lyckas uträtta om de får tugga i sig 90W.

Ett sätt att designa en snabbare processor är att lägga till flera beräkningsenheter, man brukar prata om att man gör den bredare och utvecklingen just nu är att man breddar genom att lägga till flera parallella enheter. Nackdelen med bredden, som jag förstår det, är att pipelinen blir djupare. Här har det nog skett en hel del utveckling de senaste 5-10 åren då en djup pipeline förr var känslig för avbrott då prestandan fullständigt kastades bort när det hakade upp sig (Pentium 4 med 31 steg). Nu antar jag att man täcker upp för betydligt fler sådana händelser, t.ex. genom att se till att behövligt data hela tiden finns i cachen samt att efter-synkningen blivit smartare.

Kan dela med mig av min första optimering som jag kom på när jag började lära mig datorer på gymnasiet, någon som vill gissa hur man kan förbättra denna lilla BASIC-kod?

10 PRINT "Vad heter din datafil: " 20 INPUT F$ 30 REM Initiera buffrar och variabler. .... 40 REM Gör vad du är skapt för. ....

Permalänk
Medlem
Skrivet av mc68000:

Nu får Yoshman svara för sig, men enligt min mening, om du syftar på processorn: Ja!

Det du kodar, kanske lägger ner tid på att finurligt placera kod och data på ett bra sätt, lär först manglas sönder av kompilatorns optimeringsalgoritmer. Den koden, som om man tittar på den redan nu är ganska oigenkännlig (i alla fall så ger den mycket huvudbry om "vaffö gör di på detta viset?"). Redan små förändringar i koden kan få "-O3" att fullständigt kasta om saker likt en glad tombola på ett nöjesfält. (Vilket gör det hopplöst att prestandatesta utan verktyg på den här nivån.)

Ta följande exempel, skulle du kunna berätta hur kompilatorn/processorn gör om följande kod?
Vad tror du ligger bakom respektive design eller spelar det någon roll

#include <string> struct StructWithString { int int_val; double double_val; std::string string_val; }; struct StructWithBuffer { int int_val; double double_val; char buffer[32]; };

Permalänk
Medlem
Skrivet av Erik_T:

Vad har antalet klasser med det hela att göra?

Massor.

Om du lägger ner tid och optimerar en klass som klarar av samma jobb som kanske annars hade behövt 20 olika, då vinner du dels på att klassen är bättre anpassad för återanvändning, mindre buggar och investerad tid är inte bortkastad om det behövs ändringar och klassen klarar det.

Optimera kod tar tid och koden kan se lite lustig ut om man inte är van. Skrivs bara nya klasser med hårdkodad logik för just den uppgiften finns inte tid för optimeringar, även om den tiden fanns blir koden så komplex att det är omöjligt att hantera den.

1 optimerad klass är enklare än att optimera 20

Permalänk
Medlem
Skrivet av klk:

Massor.

Om du lägger ner tid och optimerar en klass som klarar av samma jobb som kanske annars hade behövt 20 olika, då vinner du dels på att klassen är bättre anpassad för återanvändning, mindre buggar och investerad tid är inte bortkastad om det behövs ändringar och klassen klarar det.

Optimera kod tar tid och koden kan se lite lustig ut om man inte är van. Skrivs bara nya klasser med hårdkodad logik för just den uppgiften finns inte tid för optimeringar, även om den tiden fanns blir koden så komplex att det är omöjligt att hantera den.

1 optimerad klass är enklare än att optimera 20

Finns det realistiska scenarion där du kan ersätta 20 klasser med 1 klass? Du kommer behöva bygga in den klasspecifika logiken ändå för det var väl det som var poängen från början, att en ny klass skapades för att det inte passar in i mallen? Sen uppfattade jag det som att huvudpoängen var att kompilatorn och processorn tar hand om merparten av optimeringen ändå, så att optimera för hand kan göra det svårare för dem att göra sitt jobb.

Det känns i alla fall som ett scenario som går att verifiera vem som har rätt.

Permalänk
Medlem
Skrivet av pine-orange:

Finns det realistiska scenarion där du kan ersätta 20 klasser med 1 klass? Du kommer behöva bygga in den klasspecifika logiken ändå för det var väl det som var poängen från början, att en ny klass skapades för att det inte passar in i mallen? Sen uppfattade jag det som att huvudpoängen var att kompilatorn och processorn tar hand om merparten av optimeringen ändå, så att optimera för hand kan göra det svårare för dem att göra sitt jobb.

Det känns i alla fall som ett scenario som går att verifiera vem som har rätt.

Här är ett exempel: https://pastebin.com/RY7vmLt7
header fil för klass som hanterar tabell data, ungefär som en databastabell men internt i minnet

Vanligt och som används av många är klasser som hanterar format, json, xml mm. Knappt någon utvecklare skulle komma på tanken idag att själv skriva logiken för det. Bara för att beskriva poängen med att återanvända kod.