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

Permalänk
Datavetare
Skrivet av klk:

Svarar inte här utan varit upptagen med att fixa till koden för att hantera ett repository.
Inte så märkvärdigt som det låter, det är en fil som kan lagra flera andra filer i sig. Precis så "klart" att det fungerat när jag testar men behöver slipa till den hårdtesta innan det är produktionsfärdigt.

Här är en länk till koden (både header fil och cpp fil i samma)
https://hastebin.com/share/orefaquyis.cpp

Gjorde den här bilden också men det verkar som den förminskas... Lägger in och får göra något annat
Det är i alla fall den röda mitten rutan som är den viktiga för att använda objektet "repository". Resten är inte intressant utåt
<Uppladdad bildlänk>

Det finns en del lösningar i den här koden som vad jag vet är mycket ovanligt att man ser idag. Bl.a. används tekniken "hungarian notation". På windows från början på 1990 till strax innan 2010 var den stilen extremt vanlig för C/C++ utvecklare.
Nästan alla jag känner från tiden som skrev C++ för windows använde det vid utveckling. De flesta där har "tvingats" bort då det sällan accepteras idag vilket är synd. Stilen är fantastiskt effektiv.

Tittar man på koden så är den skriven för att slippa "hoppa runt". En utvecklare måste alltid förstå koden utan att behöva hoppa runt i filer. Det är därför det finns prefix och postfix. Exempelvis har alla statiska metoder *_s, Globala har *_g och så vidare.

Tänkte få ihop någon liten exe som kan användas för att göra backuper men det får bli ett projekt till nästa helg.
Hittade inget för det här så det är orsaken och eftersom vi diskuterar kod blir det här lite mer realistisk kod istället för småsnuttar även om det är lite detta också.

När jag skriver kod försöker jag hålla koden så ren som möjligt från kommenterar och felhanteringskod. Kommentarer är det gott om men de är "runt" koden. Felhaneringskod kan bli långa rader. "assert" flyttas ut till höger i marginalen.

Vad är det i den kod du presenterar här som skulle vara unikt för C++?

Den ser mestadels korrekt ut, bortsett från att den inte hanterar big/little-endian.

Men varför hålla den in-memory-representationen så nära filstrukturen? Vore det inte betydligt enklare att använda en annan intern representation och sedan serialisera till ett mer kompakt format? Det skulle samtidigt eliminera den rätt godtyckliga begränsningen att filnamn inte får vara längre än 260 tecken.

Du verkar ha en stark förkärlek för att lägga data back-to-back. Är det verkligen optimalt år 2025, när vanliga CPU:er sedan länge har flera kärnor, och problem som false sharing i många fall helt dödar eventuella prestandavinster man möjligen uppnår med ett något mer kompakt format?

Och varför använda const std::string_view& som argument? Det spelar kanske mindre roll på Windows, på grund av ABI:n där, men på Linux och macOS är det faktiskt mer effektivt att skicka std::string_view by value, eftersom det då placeras direkt i två register, istället för att skicka en pekare till ett std::string_view, vilket kräver en extra avreferens. Om man ändå bryr sig om “minneshantering, varför annars använda C++?

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:

Att Visual Studio hade stöd var sanning med en hel del modifikation. Det finns möjlighet att spela in en trace och sedan, i read-only läge, steppa framåt/bakåt. Ett stort problem är att det verkar kräva Enterprise edition, vilket jag personligen inte ser värdet att betala för givet att MSVC++ idag är en sämre kompilator än både GCC (som i.o.f.s. har begränsat Window-stöd) och Clang (som har lysande Windows, MacOS och Linux stöd).

Steppa bakåt har jag inte varit med om, men det går att "köra om" kod och det behöver man inte enterprise för att göra.

Skrivet av Yoshman:

Då du verkar gilla C++ lite granna borde du väl ändå se en del värde i det Apple gett oss genom åren? Det är väldigt mycket Apples förtjänst att LLVM gick från ett coolt forskningsprojekt till på rätt kort tid på flera sätt bli världens viktigaste kompilatorinfrastruktur, med bland annat en av de absolut bästa C++-kompilatorerna som finns.

Menar inte att dessa IT jättar är kassa rätt igenom, de gör självklart bra saker om de tjänar på det.

Men det är aktiebolag och ägare kräver avkastning samt att ägarstrukturen till de största IT bolagen är samma, samma ägare hos alla. Hela systemet är kontrollerat. Det här blockerar massor av möjligheter.

Om Apple velat hade de kunnat haft mycket mer funktionalitet i vad de säljer men de hade inte tjänat på det. Vad Apple (Microsoft, Google mm) gör är saker för att låsa kunden.
Tror också att denna kritik som plötsligt dök upp om minnesproblem i C++ lobbats för eftersom det kommer från amerikanska underrättelsetjänster. De agerar proaktivt för de kommer få jobba om sina modeller med AI, en teknik de troligen inte kan kontrollera.

Att exempelvis Google haft över 90% av sökmarknaden är rubbat.

Skrivet av Yoshman:

Till skillnad från Torvalds som verkar genuint avsky C++ så sticker också XNU (kernel som används i MacOS och iOS) i att vissa delsystem tillåter en begränsad form av C++, väldigt mycket hur Linux tillåter Rust i vissa delsystem.

Håller med och Linux är byggt mycket efter hur Linus "tänker". Men jag tror det varit nyttigt för linux för hans kritik mot C++ behöver inte vara fel. Bra kod i C++ ställer mycket höga på utvecklare och C++, alldeles för lätt att skriva C++ kod som även om den skulle vara bra är helt omöjlig att förstå för andra.
Exempel på sådan kod är sol2. Fantastisk kod men räkna inte med att någon annan utvecklare kommer ta över koden.

Skrivet av Yoshman:

Och vill man stödja något under väldigt lång tid har nog historien nu allt oftare visat oss: se då för f_n till att all källkod är under din kontroll. D.v.s. använd vare sig MacOS eller Windows, använd Linux!

Håller med. Mycket kod kommer dö med utvecklaren som ligger bakom. Att få stöd hos andra är så mycket svårare än vad många förstår. Enda realistiska möjligheten som jag ser det är att företag som tjänar pengar på produkten också får del av koden och kan börja underhålla den, då gärna flera.

Skrivet av Yoshman:

Majoriteten av programvaran vi kör på våra desktop OS har knappast något problem med livslängden. En viktig orsak är det tråden handlar om: säkerhetsproblem är så vanligt förkommande att man varken vill eller bör använda programvara på sitt desktop OS som inte längre utvecklas aktivt, då specifikt hanterar CVE:er.

Och den typen av kod som kan leva så länge är i princip 100% utvecklad i C eller möjligen C++, bra om det dyker upp alternativ men det kommer fortsätta domineras av C eller C++

Permalänk
Medlem
Skrivet av Yoshman:

Vad är det i den kod du presenterar här som skulle vara unikt för C++?

Jag försökte precis förklara det, det är inget i den koden som är unikt för C++ men utvecklare i andra språk skriver inte sådant. Det är mycket ovanligt och jag förklarade varför

Permalänk
Medlem
Skrivet av Yoshman:

Men varför hålla den in-memory-representationen så nära filstrukturen? Vore det inte betydligt enklare att använda en annan intern representation och sedan serialisera till ett mer kompakt format?

Det går säkert att göra detta på en hel hög med olika sätt. Problemet jag nu hade var att jag inte hittade en enda lösning på det här. INGEN.
Du får nog förklara mer vad som skulle vara "enklare"? Inte säker på att jag är hundra på vad du menar även om jag anar

Jag ville i alla fall ha något enkelt för att lätt kunna radera eller lägga till om filen behöver expanderas eller rensas.

Och om det skulle behövas komprimering är det ett senare projekt. Har inte tid att sitta med denna lösning allt för länge.

Skrivet av Yoshman:

Och varför använda const std::string_view& som argument? Det spelar kanske mindre roll på Windows, på grund av ABI:n där, men på Linux och macOS är det faktiskt mer effektivt att skicka std::string_view by value, eftersom det då placeras direkt i två register, istället för att skicka en pekare till ett std::string_view, vilket kräver en extra avreferens. Om man ändå bryr sig om “minneshantering, varför annars använda C++?

För att följa fasta mönster. Skickar jag något med referens så blir det alltid const. string_view som är speciell får samma behandling.

Och jag tvivlar på att koden i slutändan kommer skilja sig, kompilatorer kommer generera det som är mest optimalt.

Kod som behöver prestanda och strängar, där brukar jag ha andra lösningar för att få upp hastigheten

Permalänk
Datavetare
Skrivet av klk:

Jag försökte precis förklara det, det är inget i den koden som är unikt för C++ men utvecklare i andra språk skriver inte sådant. Det är mycket ovanligt och jag förklarade varför

Ignorerat att det inte är det mest optimala sättet att representera det hela så skulle ju en logiskt ekvivalent implementation i C, Go, Rust och antagligen även Zig (har har läst om Zig, har aldrig programmerat det) var väldigt likvärdiga.

Ser inte heller att en C# eller Java variant skulle skilja sig radikalt.

Sen är det lite märkligt att nästan allt är publikt, det begränsar definitivt framtida optimeringsmöjligheter!!!! Likt hur man ska göra saker så enkla som möjligt, men inte enklare så ska APIer vara så små som möjligt men inte mindre.

Skrivet av klk:

För att följa fasta mönster. Skickar jag något med referens så blir det alltid const. string_view som är speciell får samma behandling.

Och jag tvivlar på att koden i slutändan kommer skilja sig, kompilatorer kommer generera det som är mest optimalt.

Fast här kan faktiskt inte kompilatorn göra det som är "mest optimalt" på Linux/MacOS, finns gränser för vilka transformer som är tillåtna.

Samma gäller när man anpassar program för multi-core, mest kompakta format behöver inte alls vara det mest effektiva!

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:

Ignorerat att det inte är det mest optimala sättet att representera det hela så skulle ju en logiskt ekvivalent implementation i C, Go, Rust och antagligen även Zig (har har läst om Zig, har aldrig programmerat det) var väldigt likvärdiga.

Ser inte heller att en C# eller Java variant skulle skilja sig radikalt.

Tror inte jag heller men kan du hitta en enda java eller C# utvecklare som ens skulle fundera på att skriva en sådan här lösning?
Det finns inte, att ens diskutera lösningar likt denna är lönlöst

Det råd jag själv fått när jag arbetat i team med C# är att det inte är någon ide. Självklart finns det undantag, alltså utvecklare som vet om annat.

Skrivet av Yoshman:

Sen är det lite märkligt att nästan allt är publikt, det begränsar definitivt framtida optimeringsmöjligheter!!!! Likt hur man ska göra saker så enkla som möjligt, men inte enklare så ska APIer vara så små som möjligt men inte mindre.

Och det är inte du den första att säga till mig

Tog upp det tidigare i denna tråden, att kod under utveckling är smidigare att jobba med om allt är publikt. Lättare att testa saker.
Och tittar man på objektet så är de metoder som "borde" vara protected eller private statiska med slutklämmen *.s.

Jag menar inte att detta är rätt utan förklarar bara ett annorlunda tänk. Tycker det är viktigt att utvecklare tänker själva och hela tiden funderar över på om det finns bättre sätt att lösa något på

Permalänk
Datavetare
Skrivet av klk:

Tror inte jag heller men kan du hitta en enda java eller C# utvecklare som ens skulle fundera på att skriva en sådan här lösning?
Det finns inte, att ens diskutera lösningar likt denna är lönlöst

Det råd jag själv fått när jag arbetat i team med C# är att det inte är någon ide. Självklart finns det undantag, alltså utvecklare som vet om annat.

Och det är inte du den första att säga till mig

Tog upp det tidigare i denna tråden, att kod under utveckling är smidigare att jobba med om allt är publikt. Lättare att testa saker.
Och tittar man på objektet så är de metoder som "borde" vara protected eller private statiska med slutklämmen *.s.

Jag menar inte att detta är rätt utan förklarar bara ett annorlunda tänk. Tycker det är viktigt att utvecklare tänker själva och hela tiden funderar över på om det finns bättre sätt att lösa något på

OK, spenderade _väldigt_ lite tid på detta. Men har ungefär samma API som din C++ kod, är betydligt färre rader kod och kompaktare diskformat

https://pastebin.com/F8qkBHfc

package main import ( myarchive "example/archive" "fmt" "log" ) func main() { a := myarchive.New() path := "test_repo.arc" for i := range 20 { name := fmt.Sprintf("file%d.txt", i) content := fmt.Sprintf("Hello, World! %d", i) if err := a.AddFile(name, []byte(content)); err != nil { log.Printf("Failed to add file %s: %v\n", name, err) } _ = a.Save(path) } loaded, err := myarchive.Load(path) if err != nil { log.Fatalf("Failed to load archive: %v\n", err) } fmt.Println("Archive content:") for name := range loaded.Names() { fmt.Println(name) } if err := loaded.RemoveFile("file2.txt"); err != nil { log.Printf("Remove failed: %v\n", err) } _ = loaded.Save(path) fmt.Println("After removing file2.txt:") for name := range loaded.Names() { fmt.Println(name) } // Reload to confirm changes persisted reloaded, err := myarchive.Load(path) if err != nil { log.Fatalf("Failed to reload archive: %v\n", err) } fmt.Println("After reload:") for name := range reloaded.Names() { fmt.Println(name) } if entry, err := reloaded.GetEntryByName("file3.txt"); err == nil { fmt.Printf("Content of file3.txt: %s\n", string(entry.Content)) } else { fmt.Println("file3.txt not found") } if entry, err := reloaded.GetEntryByIndex(10); err == nil { fmt.Printf("Content of %s: %s\n", entry.Filename, string(entry.Content)) } else { fmt.Println("index 10 not found") } }

exmpelanvändning

Orkar inte skriva en C# version, men detta är ChatGPT översättningen av Go versionen. Den ser hyfsat rimligt ut och givet att det är ChatGPT lär det rätt mycket vara "som C# brukar se ut"...

https://pastebin.com/ZKRXLznH

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:

OK, spenderade _väldigt_ lite tid på detta. Men har ungefär samma API som din C++ kod, är betydligt färre rader kod och kompaktare diskformat

Har du möjlighet att beskriva hur den är lättare.
Jag vet ungefär på vad som är svårt, bl.a. hur du skall radera filer från filen. Hur går de till i din lösning

Permalänk
Datavetare
Skrivet av klk:

Har du möjlighet att beskriva hur den är lättare.
Jag vet ungefär på vad som är svårt, bl.a. hur du skall radera filer från filen. Hur går de till i din lösning

Just nu läser den in det hela i minnet på ett sätt som säkerställer att ordningen på filerna behålls _på disk_. Fast det kanske inte är viktigt, i så fall kan man förenkla ännu mer genom att helt sonika hålla en map från filnamn till fil-innehåll i minnet.

För att svara på frågan: tar man bort en fil tas dels matchande "Entry" bort, samt namn-till-entry strukturen uppdateras också.
När det serialiseras packas saker ihop automatiskt. Skulle vara trivialt att ha filen kvar, men flagga den som "borttagen" (men vad är poängen?).

Den stora skillnaden här är att det finns ett format "in-memory" och ett annat format "on-disk". Enda gången man bryr sig om formatet på disk är när man läser/skriver arkivet. Det som är "in-memory" kan trivialt hantera add, delete, update.

// Reconstruct archive a := New() a.version = hdr.Version for _, e := range entries { if e.NameOffset >= uint64(len(dataBuf)) { return nil, errors.New("invalid name offset") } name := string(dataBuf[e.NameOffset:e.ContentOffset]) endContent := e.ContentOffset + e.ContentSize if endContent > uint64(len(dataBuf)) { return nil, errors.New("invalid content offset") } content := dataBuf[e.ContentOffset:endContent] if err := a.AddFile(name, content); err != nil { return nil, err } } return a, nil }

BTW: var lite onödig kopiering i Load(), detta räcker

Frågan tillbaka är kanske: hur tror DU en Java/C# programmerare skulle angripa detta? Varför skulle de göra något radikalt annorlunda (om vi ignorerar att just detta fall är något som redan finns i standardbiblioteket i Java/C# och även Go).

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:

Just nu läser den in det hela i minnet på ett sätt som säkerställer att ordningen på filerna behålls _på disk_. Fast det kanske inte är viktigt, i så fall kan man förenkla ännu mer genom att helt sonika hålla en map från filnamn till fil-innehåll i minnet.

Förlåt "dumma frågor"

Jag säger först vad som var viktigt för mig.
Ville ha något enkelt index att läsa in över vilka filer som finns där. Därav en representation över filen där varje "entry" har samma storlek. Då blir det så mycket enklare att räkna ut blocket.

Det kommer aldrig bli några mängder med filer ändå.

Denna lista med "entry" är då vad som finns i minnet.

Är det det du menar att varje entry tar för mycket plats?

Skrivet av Yoshman:

Frågan tillbaka är kanske: hur tror DU en Java/C# programmerare skulle angripa detta? Varför skulle de göra något radikalt annorlunda (om vi ignorerar att just detta fall är något som redan finns i standardbiblioteket i Java/C# och även Go).

Jag vet inte eftersom detta är en typ av lösning som inte görs.

Observera att jag inte gjorde lösning för att det är något som inte går i andra språk eller att det skulle vara svårare. Jag gjorde detta för att det inte existerade någon annan lösning. Enda jag hittade var microsofts structured storage men den går inte att använda i linux.

Permalänk
Medlem
Skrivet av Yoshman:

OK, spenderade _väldigt_ lite tid på detta. Men har ungefär samma API som din C++ kod, är betydligt färre rader kod och kompaktare diskformat

Fråga: Är det viktigt med få rader kod och varför är det viktigt i så fall?

Permalänk
Medlem

Ursäkta att jag "spammar" tråden och här kommer OT

Men önskar tips på annan ny uppgift. Skulle behöva kod för att hantera formler likt Excel som förutom kan räkna också kan hantera variabler och gärna utföra hopp.
Det absolut viktigaste är att en formel måste vara blixtsnabb att parsa och göras om till funktionalitet. Samtidigt som "uppstart" av att processa formeln bör vara snabb.

Miljön formler körs i är beräkningar och det körs på många processer, små parallelliserade jobb.

Vad jag vet så är snabbast teknik för att parsa "shunting yard", men det kanske finns något bättre. Den är lite bökigare om man behöver möjlighet att hoppa till kod.

Formelhanteringen behöver logik för att "plugga in" metoder, det räcker inte med att hårdkoda så listor med funktionalitet kopplat till namn är viktigt.

LUA är exempelvis alldeles för tungt att dra igång för att göra en jämförelse. Man kan ha en pool med "beräkningsmotorer" också men det har sina problem.

Permalänk
Medlem

Benchmark av ett antal parsingalgoritmer i C++
https://github.com/ArashPartow/math-parser-benchmark-project
Sen finns källkoden till Linux-kommandot "bc" för vidare inspiration.
Exempel på dess "språk" en bit ifrån slutet https://linux.die.net/man/1/bc

Permalänk
Datavetare
Skrivet av klk:

Jag säger först vad som var viktigt för mig.
Ville ha något enkelt index att läsa in över vilka filer som finns där. Därav en representation över filen där varje "entry" har samma storlek. Då blir det så mycket enklare att räkna ut blocket.

Fast det är fortfarande betydligt krångligare (och mindre effektivt) jämfört med att bara lagra det hela som en vektor (access via index) och hashmap (access via namn).

På disk har man inte direkt nytta av en sådan fix storlek om det bara finns något naturligt sätt att veta vad som är filnamn och vad som är data.

Skrivet av klk:

Det kommer aldrig bli några mängder med filer ändå.

Ok, så egentligen onödigt att ens bry sig om en separat namn-till-content mappning, skulle fungera med enbart en vektor där man linjärsöker med namn (eller så sorterar man det hela och binärsöker).

Skrivet av klk:

Denna lista med "entry" är då vad som finns i minnet.

Exakt, man översätter till det när man laddar och översätter till diskformatet när man sparar.

Skrivet av klk:

Är det det du menar att varje entry tar för mycket plats?

Vet inte om det är "mycket plats", men det är mer plats än nödvändigt samtidigt som det finns onödig komplexitet i form att hålla redan på om man har tillräckligt med sådana entries, när man ska växa resp. när man ska minska den arean.

Har man en lista och/eller en hash-map "in-memory" finns ingen anledning att allokera extra minne på disk. Man vet exakt hur många entries det är när man sparar/läser-in (efter att man läst fix-sized header).

Skrivet av klk:

Jag vet inte eftersom detta är en typ av lösning som inte görs.

Observera att jag inte gjorde lösning för att det är något som inte går i andra språk eller att det skulle vara svårare. Jag gjorde detta för att det inte existerade någon annan lösning. Enda jag hittade var microsofts structured storage men den går inte att använda i linux.

Att det är svårt att hitta något sådant i just Java, C#, Go och Python visar väl ändå att folk inte är idioter? Detta är ett problem som är tillräckligt vanligt förkommande för att alla dessa ska inkludera funktionen i deras respektive standardbibliotek (där man även får det i standardformat och har möjlighet att komprimera data på disk).

Skrivet av klk:

Fråga: Är det viktigt med få rader kod och varför är det viktigt i så fall?

My bad. Håller helt med om att det absolut inte finns något egenvärde i att göra en viss sak på så få rader som möjligt. Gör man det får man rätt ofta "write-only code", då det är rätt lätt att skriva sådan kod men hopplöst svårt att läsa.

Menade att då ett arkiv logiskt sett är en mappning mellan filnamn och ett innehåll så representerar man lämpligen det just så i dess "working-set" då det leder till enkel och effektiv logik.

Man ska absolut införa beskrivande variabler och liknande. Det ger fler rader, men man slipper problemet med "smart kompakt kod". Det man i bästa fall då får är något som är "uppenbart korrekt" istället för något som "inte har några uppenbara fel".

Sedan behövs en lite annan representation för att effektivt lagra det hela på disk, men varför använda det formatet mer än just för disk?

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:

Fast det är fortfarande betydligt krångligare (och mindre effektivt) jämfört med att bara lagra det hela som en vektor (access via index) och hashmap (access via namn).

På disk har man inte direkt nytta av en sådan fix storlek om det bara finns något naturligt sätt att veta vad som är filnamn och vad som är data.

Här förstår jag inte hur du resonerar. Den lösningen jag valde var just för att den är så lätt. Bara att läsa in ett block på vips så är det klart. Ville inte krångla till. "Specialare" eller att optimera får man göra senare, det viktiga är att det fungerar just nu.
Hashmap är visserligen snabbt om det blir en hel del filer, men det går rätt blixsnabbt att hitta ändå och en hashmap drar lite extra minne samt är inte lika cache vänlig som nuvarande lösning är. Menar inte att det är jättecache vänligt nu, det går att förbättra men tror att detta är något processorn fixar.

Den lilla optimering jag själv funderade över är att "headern" eventuellt också skulle kunna ha max längd på varje fil och använda det för att räkna ut storleken på blocket med "entries" men tyckte det var onödigt. Får bli en version två någon gång i framtiden

Skrivet av Yoshman:

Vet inte om det är "mycket plats", men det är mer plats än nödvändigt samtidigt som det finns onödig komplexitet i form att hålla redan på om man har tillräckligt med sådana entries, när man ska växa resp. när man ska minska den arean.

Ok nu är jag med hur du tänker på att förenkla, att antalet entry inte synkar med filen utan att det först görs när det är dags att spara.
Finns det inte en risk att om programmet smäller så skiter sig filen. Det är viktigt att filerna inte "försvinner". Det var annars något jag la lite tid på att lägga in logik för för så att entry och filer alltid synkar. Det och temporära filen som genereras för att mellanlagra när/om entry behöver växa. Viktigt är att filen är "hel".

Radering är manuell men den är inte lika viktig för aktuell lösning (tror inte radering kommer användas men kan vara bra och ha).

Logiken är sådan att det är jobb som körs på noder med 48 kärnor (tror jag vi kör nu, över det är svårt att skala) och ett arbete kan ta flera dagar. Kraschar det vilket händer så vill man inte tappa det som är gjort. Data genereras i filer och det blir en del men inte supermånga (aldrig över 100). Däremot så är det filer från flera noder som skall samlas ihop.

Skrivet av Yoshman:

Att det är svårt att hitta något sådant i just Java, C#, Go och Python visar väl ändå att folk inte är idioter? Detta är ett problem som är tillräckligt vanligt förkommande för att alla dessa ska inkludera funktionen i deras respektive standardbibliotek (där man även får det i standardformat och har möjlighet att komprimera data på disk).

Absolut inte, att bli bra programmerare är inte alls svårt. Det är just det som är så konstigt idag.
Framförallt två orsaker till att programmerare stannar upp och de är ointresse och blockeringar. Många som läst programmering endast för att få ett bra jobb. De bryr sig inte vad som görs bara det trillar in en lön. Blockeringar är sådana som läst "så här gör man" och slutar att tänka själva. Skolan och universitet anser jag förstört mycket.
Vet att mina kombattanter här anser att jag också är blockerad. Men det är nog de sista någon som arbetat med mig skulle beskriva mig som. Skillnad på hur man uppfattas i text och verklighet och tycker det är otroligt viktigt att programmerare får testa och göra saker som inte är "standard".

Programmering är lätt med rätt träning.

Skrivet av Yoshman:

My bad. Håller helt med om att det absolut inte finns något egenvärde i att göra en viss sak på så få rader som möjligt. Gör man det får man rätt ofta "write-only code", då det är rätt lätt att skriva sådan kod men hopplöst svårt att läsa.

Ja eller jag förstår självklart att det du gjorde var en "quick and dirty" grej för att testa. Men så man inte tänker det är ok för produktion.
Koden jag skrev är mer som en black box men att om det skulle behövas ändras i den så skall den vara lätt att gå in i. Det viktiga då är att den är enkel och debugga och har bra dokumentation samt att jag gjort färdigt, att den inte är halvfärdig.

Permalänk
Datavetare
Skrivet av klk:

Här förstår jag inte hur du resonerar. Den lösningen jag valde var just för att den är så lätt. Bara att läsa in ett block på vips så är det klart. Ville inte krångla till. "Specialare" eller att optimera får man göra senare, det viktiga är att det fungerar just nu.
Hashmap är visserligen snabbt om det blir en hel del filer, men det går rätt blixsnabbt att hitta ändå och en hashmap drar lite extra minne samt är inte lika cache vänlig som nuvarande lösning är. Menar inte att det är jättecache vänligt nu, det går att förbättra men tror att detta är något processorn fixar.

Fast är inte det att förenkla helt fel sak i detta läge?

Ja, lite lättare att läsa in. Fast till kostanden av att vara betydligt krångligare att jobba med i alla de operationer du valt att implementera.

Att använda hashmap/vector i detta fall vore inte för att optimera prestanda (de skulle bara råka bli en trevlig bieffekt), utan det vore för att man då slipper "not invented here syndromen" då majoriteten av logiken implementeras via standardbiblioteket.

För att minska missförstånd, här är ett variant i C++
https://pastebin.com/Xfq7pB7e

#include "archive.cpp" #include <format> #include <print> #include <string> int main() { Archive a; std::string path = "test_repo.arc"; for (int i = 0; i < 20; ++i) { std::string name = std::format("file{}.txt", i); std::string content = std::format("Hello, World! {}", i); auto [success, error] = a.addFile(name, std::vector<uint8_t>(content.begin(), content.end())); if (!success) { std::print("Failed to add file {}: {}\n", name, error); } auto [saveSuccess, saveError] = a.save(path); if (!saveSuccess) { std::print("Save failed: {}\n", saveError); } } auto [loadedArchive, loadSuccess, loadError] = Archive::load(path); if (!loadSuccess) { std::print("Failed to load archive: {}\n", loadError); return 1; } std::print("Archive content:\n"); loadedArchive.listNames(); auto [removeSuccess, removeError] = loadedArchive.removeFile("file2.txt"); if (!removeSuccess) { std::print("Remove failed: {}\n", removeError); } auto [saveSuccessAfterRemove, saveErrorAfterRemove] = loadedArchive.save(path); if (!saveSuccessAfterRemove) { std::print("Save failed: {}\n", saveErrorAfterRemove); } std::print("After removing file2.txt:\n"); loadedArchive.listNames(); auto [reloadedArchive, reloadSuccess, reloadError] = Archive::load(path); if (!reloadSuccess) { std::print("Failed to reload archive: {}\n", reloadError); return 1; } std::print("After reload:\n"); reloadedArchive.listNames(); auto [entryByName, errorByName] = reloadedArchive.getEntryByName("file3.txt"); if (entryByName) { std::print("Content of file3.txt: {}\n", std::string(entryByName->content.begin(), entryByName->content.end())); } else { std::print("file3.txt not found: {}\n", errorByName); } auto [entryByIndex, errorByIndex] = reloadedArchive.getEntryByIndex(10); if (entryByIndex) { std::print("Content of {}: {}\n", entryByIndex->filename, std::string(entryByIndex->content.begin(), entryByIndex->content.end())); } else { std::print("index 10 not found: {}\n", errorByIndex); } return 0; }

Som kan användas så här
Skrivet av klk:

Ok nu är jag med hur du tänker på att förenkla, att antalet entry inte synkar med filen utan att det först görs när det är dags att spara.
Finns det inte en risk att om programmet smäller så skiter sig filen. Det är viktigt att filerna inte "försvinner". Det var annars något jag la lite tid på att lägga in logik för för så att entry och filer alltid synkar. Det och temporära filen som genereras för att mellanlagra när/om entry behöver växa. Viktigt är att filen är "hel".

Den risken finns alltid, men det hantera i detta fall via att "spara till disk" är en explicit metod (save()).

Skrivet av klk:

Logiken är sådan att det är jobb som körs på noder med 48 kärnor (tror jag vi kör nu, över det är svårt att skala) och ett arbete kan ta flera dagar. Kraschar det vilket händer så vill man inte tappa det som är gjort. Data genereras i filer och det blir en del men inte supermånga (aldrig över 100). Däremot så är det filer från flera noder som skall samlas ihop.

Fast varken din eller min kod hanterar access från multiple trådar i detta läge. Är det ens relevant i det här fallet?
Om det vore relevant så "lycka till att fixa det som du valt att representera det hela". Här vore det rätt självklart en concurrent hashmap (typ den C# har i standardbiblioteket, ConcurrentDictionary<> och Go i form av sync.Map, C++ saknar motsvarande i standardbiblioteket men finns i t.ex. Intels Thread Building Block bibliotek).

Om det är bara är access från en enda tråd kvittar det ju om man har 1 tråd eller 200 trådar, inget ändras i logiken och eventuell skalning hänger helt på OS-tjänster.

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

On topic igen: denna rapport försöker gå igen tekniker som man kan använda redan idag för att göra C och C++ lite mindre osäkert.

Inget här hindrar att man gör de allt för vanliga minnes-buggarna, målet är istället att få en effekt mer likt vad man får i "minnes-säkra språk", d.v.s. det är "bara" logisk bugg men inte längre ett säkerhetshål.

"How to Secure Existing C and C++ Software without Memory Safety"
https://arxiv.org/pdf/2503.21145

Inget här är egentligen nytt, "Stack Canaries" tror jag är relativt standard idag.

Tyvärr är detta mer ett plåster än en lösning, dels går dessa fortfarande att gå runt (annars vore bl.a. MacOS och iOS "säkra" även med C och C++ då de använder ARM64e ABI som implementerar alla delar som har HW-stöd i ARM64) och dels kostar det en del prestanda (så t.ex. Rust blir då ett än mer självklart val om man högprioriterar prestanda).

Här är en sammanfattning av vad rapporten går igenom och hur stödet ser ut på x86_64 och ARM64

Säkerhetsteknik

x86_64 Overhead

ARM64 Overhead

Kommentarer

Stackskydd (Stack Canaries)

🔵 ~1–2 %

🔵 ~1–2 %

Minimal påverkan, standard i moderna kompilatorer

Shadow Call Stack (Stack-integritet)

🟡 5–10 % (mjukvara)

🔵 <1 % (hårdvara)

Inbyggt i ARMv8.3+ (t.ex. Apple M1/M2/M3)

Kontrollflödesintegritet (CFI, mjukvara)

🟡 5–15 %

🟡 5–15 %

Mest märkbar i kod med många indirekta hopp

Kontrollflödesintegritet (hårdvara: CET/PAC)

🔵 ~1–3 % (Intel CET)

🔵 ~1–5 % (ARM PAC)

Effektivt hårdvarustöd på nya CPU:er

Memory Tagging Extension (MTE)

N/A

🟡 5–20 %

Endast tillgänglig på ARMv8.5+ (t.ex. Pixel 8, Apple M3)

Address Sanitizer (ASan)

🔴 50–150 %, ~2× minne

🔴 50–150 %, ~2× minne

För test/felsökning, ej för produktion

Pointer Authentication (PAC)

N/A

🔵 ~1–5 %

Skyddar returadresser & pekare från manipulation

Säker malloc (Heap-integritet)

🔵 0–10 %

🔵 0–10 %

Används av bl.a. Android, Chrome och hardened distros

Visa signatur

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

Permalänk

Lite mer från Herb Sutter. Huvudsakligen om UB, men tangerar memory safety.

Permalänk
Medlem
Skrivet av Yoshman:

Fast är inte det att förenkla helt fel sak i detta läge?

Ja, lite lättare att läsa in. Fast till kostanden av att vara betydligt krångligare att jobba med i alla de operationer du valt att implementera.

Nu tror jag vi tar i så vi skiter på oss
Att hitta saker i en vector (array) är inte svårt. Gör jag olika listor så i 9 fall av 10 väljs nästan alltid vector och det har med prestanda att göra. Det skall mycket till innan andra typer av listor slår vector. Ligger man under 100 items så är det i princip inget som slår vector. Dessutom är den normalt snålare i minne (vet att min repository lösning gösslar lite där).

Skrivet av Yoshman:

Att använda hashmap/vector i detta fall vore inte för att optimera prestanda (de skulle bara råka bli en trevlig bieffekt), utan det vore för att man då slipper "not invented here syndromen" då majoriteten av logiken implementeras via standardbiblioteket.

Det är vad jag också trodde (funderade på) att det var din orsak till att välja lösningen. Programmerare idag tänker ofta så och därför är jag (och troligen du med) mycket van att möta det argumentet.
Dagens utvecklare är mycket mer av "pusslare", man letar efter saker som gör det som skall göras istället för att skriva logiken själv. I många fall är det självklart att föredra men det är inte samma sak som att det alltid är bra.

Och framförallt för juniora utvecklare är det mycket viktigt att de själva skriver kod för att träna.

Väljer en utvecklare en annan typ av lista än vector endast för att slippa skriva egen sökhantering och jag är med i teamet så hade jag nog sagt till att välja vectorn och skriva om. Det är inte ett skäl att välja någon annan typ av lista.

Självklart får "repository" klassen funktioner som kapslar in dessa sök i vecktorn. Inget man gör "utanför" klassen.

Skrivet av Yoshman:

Den risken finns alltid, men det hantera i detta fall via att "spara till disk" är en explicit metod (save()).

Det här ser jag faktiskt som allvarligare designfel. Det kräver att utvecklare som använder repository har detaljerad kunskap om hur den fungerar internt. Sådan kunskap skall man enligt mig anstränga sig för att arbeta bort. Krävs sådan kunskap kommer utvecklare bli osäkra på allt för det är svårt och veta vad man måste kunna och vad man inte behöver kunna, därför kommer koden bli mycket mer seg och andra utvecklare lär sig inte skriva kod som "sköter sig själv".

Skrivet av Yoshman:

Fast varken din eller min kod hanterar access från multiple trådar i detta läge. Är det ens relevant i det här fallet?
Om det vore relevant så "lycka till att fixa det som du valt att representera det hela". Här vore det rätt självklart en concurrent hashmap (typ den C# har i standardbiblioteket, ConcurrentDictionary<> och Go i form av sync.Map, C++ saknar motsvarande i standardbiblioteket men finns i t.ex. Intels Thread Building Block bibliotek).

Det behöver den inte göra heller Vad jag vet är diskar fortfarande singelthreaded och även om SSDer är mycket snabbare än vanliga hårddiskar så att ha en sådan flaskhals i trådad kod hade aldrig fungerat.
Vad som sker är att data ligger i minnet och bearbetas/beräknas. När det är klart och går vidare till nästa steg så synkroniseras data och skrivs/läses.

Fråga: Den lösningen du gjorde som jag inte förstod. Hur gör du för att flytta "block" i filen om entry måste växa?
Jag valde att dumpa filens innehåll utom entry till en annan fil. Skriva nytt entry som då går in en bit över filernas innehåll. Sedan läses "dumpen" in igen från den temporära filen men då efter det nya entry blocket som växt i storlek.

Har du en smartare lösning på det?

Permalänk
Datavetare
Skrivet av klk:

Nu tror jag vi tar i så vi skiter på oss
Att hitta saker i en vector (array) är inte svårt. Gör jag olika listor så i 9 fall av 10 väljs nästan alltid vector och det har med prestanda att göra. Det skall mycket till innan andra typer av listor slår vector. Ligger man under 100 items så är det i princip inget som slår vector. Dessutom är den normalt snålare i minne (vet att min repository lösning gösslar lite där).

Du hävdar alltså seriöst att det inte är någon relevant skillnad på dessa

Addera fil

Variant #1

std::pair<bool, std::string> repository::add(const std::string_view& stringName, const void* pdata_, uint64_t uSize) { assert( stringName.length() < 260 ); assert( m_pFile != nullptr ); if(!m_pFile || stringName.length() >= sizeof(entry::m_piszName)) { return {false, std::string("Invalid file or name too long: ") + stringName.data()}; } // if( m_header.size_free() == 0 ) { uint64_t uGrowTo = m_header.size(); if( uGrowTo > 4 ) uGrowTo += (uGrowTo >> 1); else uGrowTo += 4; expand(uGrowTo, 65536); // 2^16 = 65536 } auto uStartOffset = calculate_first_free_content_position_s(*this); // Calculate the start offset for the new data (end of the file) fseek_64_(m_pFile, 0, SEEK_END); // Move to the end of the file uint64_t uOffset = ftell(m_pFile); // Get the current file position as the offset assert( uOffset == uStartOffset ); auto uBytesWritten = fwrite(pdata_, 1, uSize, m_pFile); if( uBytesWritten != uSize ) { return { false, "Failed to write data to file" }; } // ## Subtract the first position of content in repository file with the size of reposytory header and entry block auto uFirstPosition = calculate_first_content_position_s(*this); uStartOffset -= uFirstPosition; // Create a new entry and add it to the repository entry entry_( stringName.data(), uStartOffset, uSize, eEntryFlagValid ); m_vectorEntry.push_back(entry_); m_header.add_entry(); return {true, ""}; }

Variant #2

std::pair<bool, std::string> addFile(std::string_view name, std::vector<uint8_t> content) { if (std::any_of(entries_.begin(), entries_.end(), [&](const Entry &e) { return e.filename == name; })) return {false, "file already exists"}; entries_.emplace_back(Entry{std::string(name), std::move(content)}); return {true, ""}; }

Ta bort fil

Variant #1

std::pair<bool, std::string> repository::remove_entry_from_file(const std::vector<uint64_t>& vectorIndexes) { assert( m_pFile != nullptr ); if(m_pFile == nullptr) { return {false, "File not open"}; } for (const auto& index_ : vectorIndexes) { const entry& entry_ = m_vectorEntry[index_]; if( index_ >= m_header.size() ) { return {false, "Invalid index found"}; } } header headerCopy(get_header()); // Create a copy of the header headerCopy.clear(); repository repositoryCopy( headerCopy ); // Create a copy of the repository // ## Create a temporary file std::filesystem::path pathRepository(m_stringRepositoryPath); pathRepository.replace_extension("tmp"); auto result_ = repositoryCopy.create(pathRepository.string()); if( result_.first == false ) { return result_; } std::vector<uint8_t> vectorBuffer(1024 * 1024); // 1MB buffer for efficiency // ## Copy all valid entries except the one to be removed uint64_t uNewOffset = 0; std::vector<uint64_t> vectorRemove(vectorIndexes.begin(), vectorIndexes.end()); std::sort(vectorRemove.begin(), vectorRemove.end(), std::greater<>()); // Sort in descending order for safe erasure auto uFirstContentPosition = calculate_first_content_position_s(*this); for (size_t u = 0; u < m_vectorEntry.size(); ++u) { bool bRemove = std::find(vectorIndexes.begin(), vectorIndexes.end(), u) != vectorIndexes.end(); if( bRemove == false ) { entry& entry_ = m_vectorEntry[u]; // ### Read the entry's data from the original file auto uOffset = uFirstContentPosition + entry_.offset(); auto iResult = fseek_64_(m_pFile, uOffset, SEEK_SET); assert(iResult == 0); uint64_t uBytesRemaining = entry_.size(); while( uBytesRemaining > 0 ) { size_t uBytesToRead = std::min(static_cast<size_t>(uBytesRemaining), vectorBuffer.size()); size_t uBytesRead = fread(vectorBuffer.data(), 1, uBytesToRead, m_pFile); assert(uBytesRead != 0); if( uBytesRead != uBytesToRead ) { repositoryCopy.close(); std::remove(repositoryCopy.get_path().c_str()); return {false, "Failed to read from original file"}; } size_t uBytesWritten = write_s(repositoryCopy, vectorBuffer.data(), uBytesRead ); if( uBytesWritten != uBytesRead ) { repositoryCopy.close(); std::remove(repositoryCopy.get_path().c_str()); return {false, "Failed to write to temporary file"}; } uBytesRemaining -= uBytesRead; } // Update the entry's offset entry_.set_offset( uNewOffset ); uNewOffset += entry_.size(); repositoryCopy.add_entry( entry_ ); } } repositoryCopy.flush(); repositoryCopy.close(); close(); std::remove(m_stringRepositoryPath.c_str()); // Replace the original file with the temporary file if( std::rename(repositoryCopy.get_path().c_str(), m_stringRepositoryPath.c_str()) != 0 ) { std::remove(repositoryCopy.get_path().c_str()); return {false, "Failed to replace original file"}; } assert( m_pFile == nullptr ); result_ = open(); if( result_.first == false ) { return result_; } flush(); return {true, ""}; }

Variant #2

std::pair<bool, std::string> removeFile(std::string_view name) { auto originalSize = entries_.size(); entries_.erase(std::remove_if(entries_.begin(), entries_.end(), [&](const Entry &e) { return e.filename == name; }), entries_.end()); if (entries_.size() == originalSize) return {false, "file not found"}; return {true, ""}; }

Dold text
Skrivet av klk:

Det är vad jag också trodde (funderade på) att det var din orsak till att välja lösningen. Programmerare idag tänker ofta så och därför är jag (och troligen du med) mycket van att möta det argumentet.
Dagens utvecklare är mycket mer av "pusslare", man letar efter saker som gör det som skall göras istället för att skriva logiken själv. I många fall är det självklart att föredra men det är inte samma sak som att det alltid är bra.

Även om det du hävdar är sant i vissa fall, så är det nästa aldrig sant när folk utnyttjar standardbiblioteket så långt som möjligt.

Att inte använda standardbiblioteket är en väldigt stark signal på att man faktiskt inte kan programspråket ifråga.

Skrivet av klk:

Och framförallt för juniora utvecklare är det mycket viktigt att de själva skriver kod för att träna.

Och en typiskt nybörjarmisstag är att man implementerar saker som redan finns i standardbiblioteket.

Skrivet av klk:

Det här ser jag faktiskt som allvarligare designfel. Det kräver att utvecklare som använder repository har detaljerad kunskap om hur den fungerar internt. Sådan kunskap skall man enligt mig anstränga sig för att arbeta bort. Krävs sådan kunskap kommer utvecklare bli osäkra på allt för det är svårt och veta vad man måste kunna och vad man inte behöver kunna, därför kommer koden bli mycket mer seg och andra utvecklare lär sig inte skriva kod som "sköter sig själv".

Så att hantera minne manuellt, ju mer manuellt och komplex ju bättre, är ett tecken på god design medan att explicit behöva tala om när man vill flusha sin data till disk är ett otänkbart hinder? Är det rätt uppfattat?

Det är trivialt att lägga till "autoflush" om det är önskvärt, har som sagt inte koll på typiskt användarfall här utan har bara din kod som input.

Skrivet av klk:

Det behöver den inte göra heller Vad jag vet är diskar fortfarande singelthreaded och även om SSDer är mycket snabbare än vanliga hårddiskar så att ha en sådan flaskhals i trådad kod hade aldrig fungerat.
Vad som sker är att data ligger i minnet och bearbetas/beräknas. När det är klart och går vidare till nästa steg så synkroniseras data och skrivs/läses.

Now we optimizing like it's 1985 again...

Diskar och framförallt disk I/O-systemet i moderna OS har väldigt hög nivå av "concurrency". Ovanpå det har även kontrollers i moderna SSD en hel del parallellism.

Några axplock på benchmarks

Klicka för mer information
Visa mer
Skrivet av klk:

Fråga: Den lösningen du gjorde som jag inte förstod. Hur gör du för att flytta "block" i filen om entry måste växa?
Jag valde att dumpa filens innehåll utom entry till en annan fil. Skriva nytt entry som då går in en bit över filernas innehåll. Sedan läses "dumpen" in igen från den temporära filen men då efter det nya entry blocket som växt i storlek.

Har du en smartare lösning på det?

Smartare lösning? Håll working-set i RAM, att växa filen blir då likställt med att byta ut/växa en std::vector<byte>.

Att flusha till disk är en separat uppgift (save() i mitt fall), den anropas direkt efter om önskas, eller inte vid batch-updateringar.

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:

Du hävdar alltså seriöst att det inte är någon relevant skillnad på dessa

Addera fil

std::pair<bool, std::string> repository::add(const std::string_view& stringName, const void* pdata_, uint64_t uSize)

Att deklarationen är skriven så beror på överlagring. Du skriver en generell metod som "tar allt". Sedan kan du överlagra med varianter som kallar metoden som tar allt. På det viset får du ett flexiblare API.
Din variant måste ha en vector. Går kan inte skicka in en sträng exempelvis. Och du kan lätt kasta om du har egna objekt.
Du har vant dig för mycket vid rust

I övrigt så dina metoder håller data i minnet. Det fungerar inte för den här lösningen

Filer är stora och i vårt fall kan vi lätt komma upp i över 100 GB. Det är MYCKET data som processas.

Skrivet av Yoshman:

Diskar och framförallt disk I/O-systemet i moderna OS har väldigt hög nivå av "concurrency". Ovanpå det har även kontrollers i moderna SSD en hel del parallellism.

Då tycker jag du skall testa och göra en lösning och se vad flaskhalsen blir om du drar igång en massa trådar.

Skrivet av Yoshman:

Smartare lösning? Håll working-set i RAM, att växa filen blir då likställt med att byta ut/växa en std::vector<byte>.

alldeles för osäkert för aktuell lösning

Permalänk
Medlem

Det är bra att tydligt avgränsa användningsområdet, vilka in och outputs som är tillåtna osv. Nu blir det bara onödiga fram- och tillbakarepliker för att någon minsann glömde implementera det här outtalade kravet...

Permalänk
Medlem
Skrivet av pine-orange:

Det är bra att tydligt avgränsa användningsområdet och vad programmet borde klara av att göra. Nu blir det bara onödiga fram- och tillbakarepliker för att någon minsann glömde implementera det här outtalade kravet...

Så kan det vara men att lagra filer internt i minnet tror jag nog är en självklarhet att man inte gör.

Till och med att skriva en parser som läser av någon fil, där kan det vägas fram och tillbaka om den skall läsas in helt eller i sektioner.

Permalänk
Datavetare
Skrivet av klk:

Så kan det vara men att lagra filer internt i minnet tror jag nog är en självklarhet att man inte gör.

Till och med att skriva en parser som läser av någon fil, där kan det vägas fram och tillbaka om den skall läsas in helt eller i sektioner.

Ah, måste vara därför libzip, libarchive, liblzma m.fl. alla har explicit stöd för att jobba mot RAM och då med stöd för att explicit spara på disk.

Om det är en "självklarhet att inte lagra filer i RAM" så tror jag då får läsa på lite hur disk-systemet fungerar i moderna OS. Guess what? De kommer aggressivt använda RAM som cache och det är inte så att filen garanterat ligger på disk bara för du stängt den.

För att säkerställa att data är på disk behöver man typiskt använda sig av icke-portabla OS-anrop.

Och givet uppgiften: varför använder du inte just libzip, eller någon av de C++-wrappers sin finns?
Om det är 100GB kanske man inte vill komprimera, eller så vill man absolut det (beror ju helt på). Finns även bibliotek som gör exakt det du gör nu, d.v.s. skapar en tar-fil. Skillnaden är att de använder just tar-formatet så det går att använda standardverktyg för att jobba med resultatet.

Det här känns nu rätt "out-of-scope", har man inte rätt speciella use-case som t.ex. att man vill hårdoptimera ett "read-mostly" case för ett arkiv med hyfsat många filer och massor med läsningar från flera samtida trådar (hyfsat lätt att konvertera det jag slängde ihop till ett sådant) finns ju noll anledning att inte använda någon av de extremt mogna biblioteken som finns. Eller bara köra standardbiblioteket om man har möjlighet att använda någon av alla de fall där just detta ingå i standardbiblioteket (i stort sätt alla populära språk från Java och framåt).

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

Så kan det vara men att lagra filer internt i minnet tror jag nog är en självklarhet att man inte gör.

Till och med att skriva en parser som läser av någon fil, där kan det vägas fram och tillbaka om den skall läsas in helt eller i sektioner.

Finns väldigt mycket information från dig som vi saknar nu.

Vilket operativsystem kör du? Både Windows och Unix/Linux har filen i RAM så snart du läst eller skrivit i den och där ligger den tills operativet anser att de där modifierade sidorna kanske borde flyttas till disk, långt efter att du stängt filen. Det är en del av algoritmen kring minnestrycket som du kan påverka. Att köra fsync() i tid och otid kan vara förödande för systemprestandan.

Du nämner (först nu) att det är 100 GB datafiler. Hur lång tid tar det att behandla var och en av dessa och hur många trådar krävs det?

Vilken total IO-mängt per tidsenhet bör vi förvänta oss när vi designar din lösning.

Vad har du för underliggande IO-arkitektur? Är det ett SAN-nätverk eller vanliga M.2 SSD. Det känns inte som att det är en lokal lösning för en Ryzen 7950X som efterfrågas längre.

Permalänk
Medlem
Skrivet av Yoshman:

Ah, måste vara därför libzip, libarchive, liblzma m.fl. alla har explicit stöd för att jobba mot RAM och då med stöd för att explicit spara på disk.

Nästan alla typer av enklare storage lösningar har möjlighet att använda minnet för det är så lätt att lägga till. Jag gjorde det för min lösning också men det är inte default.
Orsaken till att man lägger till en sådan lösning är att få en extra feature som är enkel och på så sätt kan de som har mycket arbetsminne dra nytta av det.

Vad du gjort är en slags serializeringslösning, och det är inte svårt. Serializera data är troligen den enklaste tekniken för att spara information. Det är inte vad min lösning skulle göra.

Skrivet av Yoshman:

Om det är en "självklarhet att inte lagra filer i RAM" så tror jag då får läsa på lite hur disk-systemet fungerar i moderna OS. Guess what? De kommer aggressivt använda RAM som cache och det är inte så att filen garanterat ligger på disk bara för du stängt den.

Det kanske finns, jag kan inte vad alla olika linux distros vi använder har för funktionalitet. Windows känner jag till bra men det är fortfarande så att du kan ställa in det här och det är inte obegränsat med minne.
Kör du i molnet behöver man också vara noga med vad man väljer för hårdvara. Det kan snabbt dra iväg prismässigt om man skall ha något "special" och det kan kosta massor.

Skrivet av Yoshman:

Och givet uppgiften: varför använder du inte just libzip, eller någon av de C++-wrappers sin finns?

Vi har en komprimeringslösning men det fungerar inte för det här, måste ha snabbhet och inte göra det onödigt komplext. Det är inte kostnadsfritt att lägga till bibliotek. Ofta mycket mer jobb än vad man tror.

Skrivet av Yoshman:

Det här känns nu rätt "out-of-scope", har man inte rätt speciella use-case som t.ex. att man vill hårdoptimera ett "read-mostly" case för ett arkiv med hyfsat många filer och massor med läsningar från flera samtida trådar (hyfsat lätt att konvertera det jag slängde ihop till ett sådant) finns ju noll anledning att inte använda någon av de extremt mogna biblioteken som finns. Eller bara köra standardbiblioteket om man har möjlighet att använda någon av alla de fall där just detta ingå i standardbiblioteket (i stort sätt alla populära språk från Java och framåt).

Jag köper visserligen inte den förklaringen men däremot är det inte helt ovanligt att se utvecklare som har taktiken och har svårt att ändra sig.
Vanligt bland python. Ofta argumentationen "det där kan jag slänga ihop på några timmar". Och de kan ofta slänga ihop något snabbt som kanske täcker en lösning som fungerar på 90% eller mer. Men de glömmer av edge cases.
Det som ofta seniora utvecklare används till, de bör förstå alla de udda fall och det som försvårar utveckling ganska rejält.
De utvecklare som inte förstår edgcases brukar snabbt bli sittande i en sörja om det ger sig på sig större saker

Den absolut "bästa" lösningen om det får lov att bli komplext för mitt exempel är enligt det jag lyckades fundera ut att när filen behöver växa så kan man ta filen närmast entry eller om den är liten och man behöver växa över den så får man räkna ut hur många filer som behöver flyttas och läggas sist i filen. Att man roterar filerna. Då slipper man göra en temporär fil men måste räkna om positionerna för för de filer som ligger i filen.

Det blir alldeles för komplicerat för den här lösningen för då kommer andra utvecklare ha svårt att förstå koden.

Permalänk
Medlem
Skrivet av mc68000:

Finns väldigt mycket information från dig som vi saknar nu.

Det tycker inte jag för att jag skrev nog ganska tydligt att det handlade om en lösning för att spara filer. Då kan man inte "anta" att det fungerar att hantera det i minnet. Default är att det handlar om filer. Tvärtom så borde utvecklare efterfråga extra information för att se om det finns möjlighet att lagra i minnet.

Hade jag givit en utvecklare i uppgift att göra en fil-lösning men den gör något annat, exempelvis minneslösning. Då har utvecklaren inte gjort vad den blev tillsagd att göra.

Skrivet av mc68000:

Du nämner (först nu) att det är 100 GB datafiler. Hur lång tid tar det att behandla var och en av dessa och hur många trådar krävs det?

Vilken total IO-mängt per tidsenhet bör vi förvänta oss när vi designar din lösning.

Vad har du för underliggande IO-arkitektur? Är det ett SAN-nätverk eller vanliga M.2 SSD. Det känns inte som att det är en lokal lösning för en Ryzen 7950X som efterfrågas längre.

Lösningen skall inte hårdkodas efter en specifik hårdvara utan skall vara en lösning som inte behöver röras, kanske fungera 20 år framåt.
Det är olika vilken hårdvara óch operativ som körs. Du känner säkert till att i molnlösningar väljs ofta vad som passar bäst för vad man skall göra. Det kan vara allt för olika nästan gratis servrar med svag hårdvara till mycket snabba noder med väldigt många kärnor. Och det kostar

Permalänk
Skrivet av klk:

Det tycker inte jag för att jag skrev nog ganska tydligt att det handlade om en lösning för att spara filer.

Ehh, jo, men det var ganska vaga specifikationer i övrigt. Du pratade om ett arkiv, men inte hur det skulle användas. Exempel på saker som skulle kunna vara relevanta är

  • Hur stora är filerna?

  • Hur ofta skrivs det nya filer?

  • Hur ofta ändras existerande filer?

  • Hur ofta läses det ur befintliga filer?

  • Är det tidskritiskt?

Har du 100GB stora datafiler tror jag du inte kommer märka av om du har handoptimerat skiten ur precis allting. Då kan du lika gärna köra Pythons tarfile-modul för den lilla prestandavinsten du åstadkommer genom att skriva koden så att den "passar processorn" kommer drunkna i tiden det tar att skriva 100GB.

Kör enklast möjliga lösning (tarfile?). Om det inte är tillräckligt snabbt, mät, identifiera vad som är flaskhalsen och fundera på om du kan göra något åt den. Går det fortare att skriva 100GB till disk om du byter till C++? Att direkt hitta på ett eget filformat och skriva kod för att hantera detta är bara slöseri med din (arbetsgivares) tid.

Permalänk
Medlem
Skrivet av Ingetledigtnamn:

Ehh, jo, men det var ganska vaga specifikationer i övrigt. Du pratade om ett arkiv, men inte hur det skulle användas. Exempel på saker som skulle kunna vara relevanta är

  • Hur stora är filerna?

  • Hur ofta skrivs det nya filer?

  • Hur ofta ändras existerande filer?

  • Hur ofta läses det ur befintliga filer?

  • Är det tidskritiskt?

Tack, innan man kritiserar så fråga

Kravet på lösningen är att det skall vara smidigt att hantera en hög med filer. Inte tusentals med mer än 10 (inte varit med att det blir över 100 filer)
Filerna kan bli mycket stora, lätt över 10 GB, vid extremfall strax över 100 GB. Teoretiskt kan de bli över TB men då tror inte jag det är aktuellt med att flytta filer.
Operativsystem varierar. Att "hyra" snabba servrar med snabba diskar är dyrt, därför används de bara så länge det är tidskritiskt och sedan flyttas data till billigare lösningar.

Snabbhet: Bör vara minst dubbelt så snabbt som att lagra information in en sqlite databas. Går det att få 10x så är det bra med.
Prestanda är dock en bonus, inte det viktigaste. Poängen är att det skall vara smidigt för att hålla med med stora filer är tidsödande, spelar ingen roll om de zippas. Blir jobbigt

Filer används bara som mellanlagring, det går inte att lägga in så mycket data i minnet (från början var lösningen sådan men det slog i taket ganska snabbt)

Med det sagt så gör det inte "ont" om lösningen fungerar smidigt med det mesta. Att man slipper fundera över vad som hanteras.

Permalänk
Medlem
Skrivet av klk:

Det tycker inte jag för att jag skrev nog ganska tydligt att det handlade om en lösning för att spara filer. Då kan man inte "anta" att det fungerar att hantera det i minnet. Default är att det handlar om filer. Tvärtom så borde utvecklare efterfråga extra information för att se om det finns möjlighet att lagra i minnet.

Hade jag givit en utvecklare i uppgift att göra en fil-lösning men den gör något annat, exempelvis minneslösning. Då har utvecklaren inte gjort vad den blev tillsagd att göra.

Lösningen skall inte hårdkodas efter en specifik hårdvara utan skall vara en lösning som inte behöver röras, kanske fungera 20 år framåt.
Det är olika vilken hårdvara óch operativ som körs. Du känner säkert till att i molnlösningar väljs ofta vad som passar bäst för vad man skall göra. Det kan vara allt för olika nästan gratis servrar med svag hårdvara till mycket snabba noder med väldigt många kärnor. Och det kostar

En lärdom jag har haft med mig är att om man har med mycket IO att göra så lönar det sig inte att parallellisera mer än vad IO-kanalen orkar med, det blir bara en negativ utveckling med ökande latenser från operativet. Visst kan man gödsla med pengar på hårdvara för sådant, men det är oftast ekonomiskt ohållbart. Ett projekt jag jobbat med hade 24 timmars deadline innan nya data ramlade in. Det var aldrig aktuellt att bättra på hårdvaran så då började man titta på hur lösningen var kodad. Efter ett par veckors kodande så var körningarna klara till lunch, och konsultarvodet hanterbart.

Om du gett en utvecklare en uppgift att göra en fil-lösning och hen hanterar filen helt i minnet så har du inte berättat tillräckligt för att hen skall lyckas med sitt uppdrag sett vad projektet kräver. Hen har gjort sitt jobb och löst problemet på bästa sätt enligt de premisser hen blivit presenterad. Exakt det som framkommit i denna tråd. Det är dåligt ledarskap och leder bara till misslyckade projekt som vi så ofta läser om i etermedia.

Utöver kvarvarande okända parameter som du ännu inte berätta om så blir jag osäker på hur filerna används? Är det indata till körningen som är 100 GB och det reducerade slutresultatet som lagras i "arkivet". Eller pratar vi om okända indata som resulterar i arkiverade filer om 100 GB ? Körtiden per 100 GB vore intressant att veta. Någon typ av benchmark eller löpande tidsloggning har ni väl ändå i er verksamhet? Sådana prestandasiffror i driften tar ju en promilles promille att ta fram löpande och borde sparas med resultatfilen.

EDIT: I mitt mindset så tänker jag mig att vi på något sätt gör beräkningar på de inblandade filerna, men det kanske bara handlar om att skyffla data från en punkt till en annan?