Skrivet av zonezoon:
Då gör jag nog som du säger. Jag har både hört och läst att många författare har svårt att lära ut minneshantering i sina böcker. Vissa förklarar bara väldigt ytligt, andra väldigt dåligt och ibland helt fel också. Om jag har uppfattat det rätt så är minneshantering en stor del i både C/C++ och man vill lära sig det rätt. Jag vet inte exakt vad jag vill göra inom spelutveckling, jag hade nog kunnat tänka mig göra lite allt möjligt, just för att jag tycker spel är intressant. Jag kör nog på ”Head First C”!
Återigen, tack för dina utförliga svar! Det hjälper oerhört mycket och det blev mycket enklare att välja. 😁
Har programmerat en hel del C++, både "gammel-versionen" och "modern C++" (C++11 och senare). En sak jag tyvärr måste nämna här är: att förstå C++ väldigt väl kan till viss del ställa till det i en spelmotor som UE4/UE5!!!
Minneshanteringen i UE4/UE5 är mer likt hur man jobbar i språk som Java/C#, så ur den aspekten är Unity och Unreal Engine rätt lika, än hur man jobbar med minne normalt sett i "modern" C++ (som i sin tur är väldigt lika hur man jobbar med minne i språk som Rust och Swift, skillnaden är att i C++ har man långt fler sätt att skjuta av sig både fötterna och huvudet ).
Om du primärt är ute efter att lära dig C++ specifikt för att jobba med spel och redan kan programmera i något annat språk kan du nog mer fokusera på just C++ i spel. Varför inte sätta dig ned med t.ex. UE4/5, det är en riktigt trevlig miljö som är gratis att komma igång med!
I UE4/5 kan du initialt jobba i Blueprints, en form av visuellt skriptspråk, och senare skriva om de delar där prestanda är kritiskt med C++ (då med UE-varianten av C++14/17, språkmässigt samma men ett annorlunda bibliotek och väldigt annorlunda minneshantering, det finns GC!).
Skrivet av macher:
Problemet jag observerat är dock att många aldrig fattar minneshanteringen med stack och heap, processen med kompilering och sen länkning och funktionspekare. Resultatet blir att man kan skriva C++-kod som kompilerar och går att köra, men man förstår sällan varför man ex. får vissa kompilerings- eller länkningsfel eller sidoeffekter av sin kod (varför programmet går x gånger fortare när man skickar runt const-referenser till en stor vector och inte skickar den by-value).
Sett många rekommendera att man bör föredra "by-value" nästan rakt av i "modern C++". Man lägger mer ansvar på kompilatorn om man skickar saker som std::vector<> "by-value", men om de faktiskt implementerar de möjligheter som finns blir t.ex. detta exakt samma sak och by-value variant är lättare att läsa/resonera runt
#include <numeric>
#include <vector>
int sum_by_val_vec(std::vector<int> v) {
return std::accumulate(v.begin(), v.end(), 0);
}
int sum_by_ref_vec(const std::vector<int>& v) {
return std::accumulate(v.begin(), v.end(), 0);
}
Kompilerar man det t.ex. med clang 11.0 för ARM64 (ger mer lättläst assembler än x86_64, men även där blir det samma resultat i båda fallen)
sum_by_val_vec(std::vector<int, std::allocator<int> >):
ldr x1, [x0]
ldr x3, [x0, 8]
cmp x3, x1
beq .L4
mov w0, 0
.L3:
ldr w2, [x1], 4
add w0, w0, w2
cmp x1, x3
bne .L3
.L1:
ret
.L4:
mov w0, 0
b .L1
sum_by_ref_vec(std::vector<int, std::allocator<int> > const&):
ldr x1, [x0]
ldr x3, [x0, 8]
cmp x3, x1
beq .L9
mov w0, 0
.L8:
ldr w2, [x1], 4
add w0, w0, w2
cmp x1, x3
bne .L8
.L6:
ret
.L9:
mov w0, 0
b .L6
Med saker som link-time-optimization (LTO), vilket de flesta moderna C++ kompilatorer implementerar, kan man rätt mycket helt skita i att fippla med inline. Kompilatorn gör ofta ett bättre jobb än människor på att reda ut vad som bäst bör göras, med LTO kan kompilatorn göra "inline" även på lokala funktioner.
I vissa lägen kan även kompilatorer idag få bort indirekta hopp via vtbl, med LTO kan kompilator/länkare inse att det bara kan finnas en möjlig slutdestination för ett anrop
Självklart kan man inte helt ignorera lågnivå effekter om man vill pressa ur absolut allt ur ett system, men det som var sanningar för ett decennium sedan är inte alls lika självklart längre. Är man osäker på vad något resulterar i är det idag ofta långt snabbare att gå till Compiler Explorer och testa lite olika varianter än att försöka hålla varje "optimal" metod i huvudet (vad som är optimalt med dagen kompilatorer är ibland allt annat än självklart...).
Edit: och för att se hur icke-logiskt saker kan vara idag, testade exemplet ovan med Google-benchmark
#include <algorithm>
#include <benchmark/benchmark.h>
#include <numeric>
#include <vector>
constexpr uint32_t VEC_SZ = 10000;
// Generate vector so the compiler cannot figure out the content compile time...
std::vector<uint32_t> make_vec() {
std::vector<uint32_t> v(VEC_SZ);
std::iota(v.begin(), v.end(), 0);
return v;
}
uint32_t sum_by_val_vec(std::vector<uint32_t> v) {
return std::accumulate(v.begin(), v.end(), 0);
}
uint32_t sum_by_ref_vec(const std::vector<uint32_t>& v) {
return std::accumulate(v.begin(), v.end(), 0);
}
// Apply sum to a "global side-effekt" so the function call cannot be optimised away
uint32_t g = 0;
void by_val(benchmark::State &state) {
auto v = make_vec();
while (state.KeepRunning()) {
g += sum_by_val_vec(v);
}
}
BENCHMARK(by_val);
void by_ref(benchmark::State &state) {
auto v = make_vec();
while (state.KeepRunning()) {
g += sum_by_ref_vec(v);
}
}
BENCHMARK(by_ref);
BENCHMARK_MAIN();
Rimligen borde båda vara lika snabba, men av någon anledning (har inte orkat kolla exakt orsak) så är by_val versionen konsekvent något snabbare! (Körde på min Linux dator som har en i7-8559U, är en NUC)
Running ./vec_bench
Run on (8 X 4500 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x4)
L1 Instruction 32 KiB (x4)
L2 Unified 256 KiB (x4)
L3 Unified 8192 KiB (x1)
Load Average: 0.08, 0.07, 0.03
-----------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------
by_val 3932 ns 3932 ns 178388
by_ref 4804 ns 4804 ns 145770