Skrivet av klk:
börjar med std::vector och då jämför med rusts vector som kallas "vec"
Om du skall hämta minne säkert behöver man kontrollera.
C++
std::vector<int> numbers = {10, 20, 30, 40, 50};
std::cout << "First element: " << numbers[0] << "\n"; // 10
std::cout << "First element: " << numbers[999] << "\n"; // PANG (Undefined Behavior)
Rust
let mut numbers = vec![10, 20, 30, 40, 50];
println!("First element: {}", numbers[0]); // 10
println!("First element: {}", numbers[999]); // panic (gissar att det liknar exceptions i c++)
Här har du exempelkod när kompilatorn optimerar med simd
https://godbolt.org/z/a7dovoP3s
Nu vet jag inte riktigt vad detta inlägg har med optimeringar att göra. Det är inte en kompilatoroptimering att C++ inte gör range checks. Och, som andra tidigare har påpekat, så har inte Rust GC så ditt svar är inte ett svar på min fråga. Men jag nappar på detta för att lära dig lite om vad kompilatorn, denna mytiska best, faktiskt kan göra.
Om vi kikar på X86-koden som genereras för Rust-koden @Yoshman postade så är det inga problem att lägga ut AVX-kod även i ett språk med range checks. (Rad 120)
Men hur kan kompilatorn låta bli att göra range check här? Jo, optimeringar
Vi kör C++ för enkelheten skull, där vi båda två känner oss mer hemma. Din funktion innehåller inte tillräckligt med information för att man skall kunna göra en range check, så låt oss byta ut dina råa pekare till vektor-referenser:
void add(const std::vector<float> &p1_, const std::vector<float> &p2_, std::vector<float> &pResult_, size_t uSize) {
for(size_t u = 0; u < uSize; u++)
{
pResult_[u] = p1_[u] + p2_[u];
}
}
Sedan utökar vi C++ till att även göra range checks. Det borde ge kod liknande denna om man inline-expanderat range checken. Lite mer precis info borde skickas till panic-anropet, men det är inte den intressanta delen här.
void add(const std::vector<float> &p1_, const std::vector<float> &p2_, std::vector<float> &pResult_, size_t uSize) {
for(size_t u = 0; u < uSize; u++)
{
if (u >= p1_.size_) panic();
if (u >= p2_.size_) panic();
if (u >= pResult_.size_) panic();
pResult_[u] = p1_[u] + p2_[u];
}
}
Det krävs inte särskilt avancerad analys för att se att index-variablen u kommer gå från 0 till uSize. Om range check vore ett vanligt mönster i språket skulle det finnas optimeringar som letade efter just detta: loop med känd trip count och range checks i loop kroppen. Här kan kompilatorn välja att lägga ut två loopkroppar och välja vilken som körs baserat på loopens slutvärde:
void add(const std::vector<float> &p1_, const std::vector<float> &p2_, std::vector<float> &pResult_, size_t uSize) {
if (uSize >= p1_.size_ || uSize >= p2_.size_ || uSize >= pResult_.size_)
{
for(size_t u = 0; u < uSize; u++)
{
if (u >= p1_.size_) panic();
if (u >= p2_.size_) panic();
if (u >= pResult_.size_) panic();
pResult_[u] = p1_[u] + p2_[u];
}
}
else
{
for(size_t u = 0; u < uSize; u++)
{
if (u >= p1_.size_) panic();
if (u >= p2_.size_) panic();
if (u >= pResult_.size_) panic();
pResult_[u] = p1_[u] + p2_[u];
}
}
}
Om uSize skulle vara ett ogiltigt index i någon av arrayerna kommer den inledande testen vara sann och man kommer köra den första loopen. I den andra loopen vet kompilatorn att uSize inte kommer trigga någon av range checkerna och den kan göra exakt samma optimeringar den skulle kunna göra på motsvarande C++-kod. Exempelvis använda SIMD-instruktioner.
Du kan argumentera att det läggs ut kod i onödan, men GCC gör liknande tricks för att kunna lägga ut SIMD-kod. Om du tittar på koden i din Godbolt-länk så inleds funktionen add med ett antal tester som säkerställer att pResult_ inte överlappar med p1_ och p2_. Utan dessa tester skulle inte GCC kunna lägga ut AVX-instructioner då det finns risk för aliasing mellan dina råa pekare.
Om vi går ett steg till och blandar in function inlining så blir det ännu bättre kod. I lekmannens ögon handlar inliningen om att bli av med overheaden förknippad med att göra ett funktionsanrop. Man blir av med overheaden, men det är långt viktigare att kompilatorn kan göra optimeringar över funktionsanropet. Efter inlining har man fått en lokal kopia av funktionskroppen som kan skräddarsys för de parametrar som skickades i just det här anropet.
Om både vektor-konstruktorerna och add inline-expanderas i main vet kompilatorn att vector1, vector2, och result har storleken 1024. På samma sett vet kompilatorn att p1_, p2_ och pResult_ i adds kropp refererar till vector1, vector2 och result (alla med storleken 1024) och att uSize kommer ha värdet 1024. Ingen rangetesterna kommer lösa ut och den kan plocka bort både testerna och loop-kroppen med testerna i. En trip count på 1024 är nice power of 2 så man kan rulla upp loopen precis så mycket man vill utan att behöva lägga ut en extra loop för att utföra de "rest"-iterationer som den upprullade loopen inte hanterade.
Bara för att språket upprätthåller en viss nivå av minnessäkerhet så behöver inte det betyda att den genererade koden blir långsam. Med kompilatoroptimeringar blir den kritiska koden ofta precis lika effektiv som den du skriver i C++. Det är dags att göra dig av med dina fördomar om andra språk och programmerare som använder dem.