De ce cele mai multe limbi de nivel înalt sunt lente

De ce cele mai multe limbi de nivel înalt sunt lente

Ultimele două luni, am întrebat de multe ori această întrebare, așa că am decis să-l răspundă la articol.

Motivele pentru care cele mai multe limbaje de programare de nivel înalt sunt lente, de obicei, două:

  • ei nu funcționează bine cu memoria cache;
  • de colectare a gunoiului nevoie de o mulțime de resurse.

De fapt, ambii factori sunt reduse la unul - în astfel de limbi sunt un număr mare de alocări de memorie.

Este necesar să se clarifice faptul că eu vorbesc în primul rând despre aplicațiile client, în cazul în care viteza de executie importanta locala. În cazul în care cererea este de cheltuieli de 99,9% din timpul de așteptare pentru răspunsul rețelei este puțin probabil să conteze cât de lent este problema limbii pentru a optimiza rețeaua este mai importantă.

Voi lua ca exemplu C #, din două motive: în primul rând, un limbaj de nivel înalt pe care îl folosesc de multe ori în ultima vreme, și în al doilea rând, dacă aș fi luat Java, fanii C # ar putea susține că utilizarea de valoare tipuri de C # elimină majoritatea problemelor (nu este).

În plus, voi continua pe presupunerea că codul idiomatică scris în același stil cu bibliotecile standard și ținând seama de acordurile adoptate. Nu va acoperi cârje urâte ca o „soluție“ a problemei. Da, în unele cazuri, vă permite să ocolească caracteristicile lingvistice, dar nu elimină problema originală.

Lucrul cu cache

Pentru început, să ne uităm la ce este important de a utiliza cache-ul corect. Aici este o diagramă de latență de memorie pentru procesoare Haswell, pe baza acestor date.

De ce cele mai multe limbi de nivel înalt sunt lente

Întârzierea în citirea dintr-un teanc de aproximativ 230 de cicluri, în timp ce, ca și citirea memoriei cache L1 - 4 cicluri. Se pare că nu funcționează corespunzător cu cache-ul poate face codul de aproape 50 de ori mai lent. Mai mult decât atât, datorită procesorului moderne multi-tasking, citirea din memoria cache poate să apară în timpul lucrului cu registre, astfel încât o întârziere în citirea din L1 poate fi luată în considerare.

Pur și simplu pune, în plus față de dezvoltarea algoritmului, cache rateaza - principala problemă a productivității. Odată ce ați decide problema accesului efectiv de date, vor exista doar optimizări minore care, în comparație cu un dor cache, nu are un efect atât de mult asupra performanței.

O veste bună pentru dezvoltatorii de limbă: nu aveți nevoie pentru a dezvolta cel mai eficient compilator și puteți economisi timp prin utilizarea unor abstractizare. Tot ce trebuie - să organizeze accesul efectiv la date și programe pe limba ta va fi capabil de a concura în viteza de execuție cu C.

De ce se utilizează C # conduce la ratări cache frecvente

Pur și simplu pune, C # este conceput în așa fel încât să nu țină seama de realitățile moderne de lucru cu o memorie cache. Încă o dată, eu nu vorbesc despre limitările impuse de limbă și de „presiunea“ pe care acestea trebuie să fie un programator, ceea ce face scrisul nu este cel mai eficient cod. Teoretic, există o soluție, deși nu cel mai convenabil pentru multe din aceste lucruri. Vorbesc despre codul, care ne încurajează să scrie limba ca atare.

Problema principală a C # este că este foarte slab susținută de valoare tipuri. Da, are structuri care reprezintă o valoare, stocată în același loc și a anunțat (de exemplu, stivă sau în interiorul altor obiecte). Dar există unele probleme destul de serioase, din care, deoarece aceste structuri sunt mai cârje decât soluțiile.

  • Vi se cere să declare tipurile de date în avans, ceea ce înseamnă că, dacă aveți vreodată nevoie pentru a obține obiectul de acest tip a existat într-o grămadă, toate obiectele de acest tip vor fi întotdeauna alocate pe heap. Desigur, puteți scrie o clasă de înveliș pentru tipurile dorite de date și de utilizare în fiecare caz, pentru înfășurare dorit, dar este destul de dureros mod. Ar fi mai bine dacă și clasele de structuri au declarat întotdeauna la fel, dar utilizarea lor a fost posibilă în moduri diferite și este așa cum ar trebui, în acest caz. . Aceasta este atunci când ai nevoie de ceva pe stivă, ai declara ca valoare, iar când heap - ca un obiect. Acesta funcționează atât de C ++, de exemplu. Acolo, nu sunt obligați să facă toate obiectele, în cazul în care numai pentru că există unele lucruri pe care ar trebui să fie pe heap (din diverse motive).
  • Posibilitatea de link-uri sunt foarte limitate. Puteți trece nimic pe link-ul de la funcția, dar asta este. Nu poți pur și simplu du-te și a obține un link la element din lista. de exemplu. Pentru a face acest lucru, trebuie să păstrați lista și link-uri și valori simultan. Este imposibil de a obține un pointer la ceva alocat pe stiva sau situate în interiorul obiectului. Puteți copia numai toate sau se referă la funcția de legătură. Motivele pentru care un astfel de comportament, desigur, sunt destul de clare. În cazul în care siguranța de tip o prioritate, este destul de dificil (dacă nu imposibil) să mențină un sistem flexibil de link-uri și, în același timp, pentru a asigura o astfel de securitate. Dar chiar și caracterul rezonabil al acestor restricții nu schimbă faptul că aceste restricții sunt încă acolo.
  • Tampoane cu o dimensiune fixă ​​nu acceptă tipuri definite de utilizator și necesită directiva nesigure.
  • subarrays funcționalitate limitată. Acolo ArraySegment clasa. dar el a folosit-mic. Dacă aveți nevoie pentru a trece un set de elemente de matrice, trebuie să creați IEnumerable. și, prin urmare, alocarea de memorie. Chiar dacă API acceptă ArraySegment. acest lucru nu este încă suficient, nu se poate folosi pentru orice listă. nici pentru matrice pe stivă. numai pentru gama obisnuita.

Ca urmare, limbajul tuturor, dar cazurile cele mai simple, împinge în utilizarea activă a haldei. În cazul în care toate datele în heap, probabilitatea unui dor este mai mare (pentru că nu se poate decide cum să aranjeze datele). În timp ce scrieți un program în C ++ programator trebuie să se gândească la modul în care să asigure în mod eficient accesul la date, C # încurajează locația lor, în unele părți ale memoriei. Programator pierde controlul asupra dispunerii de date, ceea ce duce la greșeli inutile și, în consecință, scăderea performanței. Iar faptul că puteți compila un program C # în codul nativ. nu contează. Creșterea productivității nu compensează pentru localitatea cache slabă.

Desigur, există soluții. De exemplu, puteți utiliza structura și puneți-le în piscina din Lista. Acest lucru va permite, de exemplu, lista de by-pass, și să actualizeze toate obiectele din loc. Din păcate, acum, pentru a obține o referință la un obiect, aveți nevoie pentru a obține o referință la piscina și indicele, și în viitor, pentru a utiliza indexare matrice. Spre deosebire de C ++, C # este un truc arata teribil, limba nu este pur și simplu proiectat pentru acest lucru. Mai mult decât atât, accesați acum un singur element al unui bazin de mai scump decât folosind un link - poate fi acum două sancțiuni, deoarece va fi necesar să se dereference piscina în sine. Puteți, desigur, duplicat Lista funcționalitate o structură pentru a preveni acest lucru. Am scris o mulțime de cod pentru o astfel de practică: este urât, se lucrează la un nivel scăzut și nu este imun la greșeli.

În cele din urmă, vreau să subliniez că această problemă apare nu numai în locurile înguste. În scris idiomatic tipuri de referință de cod sunt peste tot. Acest lucru înseamnă că, pe tot parcursul întârziere de cod va avea loc întotdeauna în câteva sute de cicluri, acoperind toate optimizările. În cazul în care chiar și optimiza zonele consumatoare de resurse, codul va fi la fel de lent. Deci, dacă nu doriți să scrie programe folosind bazinele de memorie și indicii, de lucru, probabil, la un nivel mai mic decât C ++ (atunci de ce utilizarea C #), abia dacă tot ce se poate face pentru a evita această problemă.

de colectare a gunoiului

Presupun că cititorul înțelege de ce colectarea gunoiului, în sine, este o problemă serioasă pentru performanță. atîrnă aleatorii la momentul construi, este inacceptabil pentru programele de animație. Nu voi insista pe ea și trece la modul de a proiecta limba în sine poate agrava situația.

Datorită limitărilor lingvistice în lucrul cu valoare tipuri, dezvoltatorii au de multe ori în loc de una structuri de date mari (care poate fi chiar pus pe stiva) folosesc o mulțime de obiecte mici, care sunt plasate într-o grămadă. În același timp, trebuie să ne amintim că mai multe obiecte sunt plasate într-o grămadă, mai mult de lucru pentru colectorul de gunoi.

Există anumite teste care C # și Java sunt superioare C ++ în performanță, deoarece alocarea memoriei într-o limbă cu costurile de colectare a gunoiului sunt mult mai ieftine. Cu toate acestea, în practică, acest lucru se întâmplă foarte rar. Scrieti un program pe C # c același număr de alocări de memorie, ca și în punerea în aplicare cele mai naive în C ++, necesită mult mai mult efort. Comparăm codul extrem de optimizat în limba cu managementul automat al memoriei cu programul nativ scris „pe frunte.“ Dacă vom cheltui aceeași cantitate de timp de optimizare cod C ++, am din nou lăsați C # în urmă.

Știu că este posibil să se scrie un colector de gunoi, care oferă performanțe ridicate (de exemplu, incremental, cu un timp fix pentru a colecta), dar acest lucru nu este suficient. Cea mai mare problemă este limbajul de nivel înalt - că acestea încurajează crearea unui număr mare de obiecte. Dacă în idiomatice C # ar fi același număr de alocări de memorie, precum și în programul în C, reprezentat de colectare a gunoiului ar fi mult mai puțin de o problemă. Și dacă utilizați gunoier elementare, de exemplu, sisteme de moale, în timp real, cel mai probabil ar avea nevoie de o barieră la intrarea pe piață l. Dar, indiferent de cât de ieftin nu este tratată, într-o limbă care încurajează utilizarea activă a indicii, ceea ce înseamnă o întârziere suplimentară, atunci când marginile sunt modificate.

Uită-te la Net biblioteca standard - alocare de memorie la fiecare pas. Conform calculelor mele .Net Framework Core conține de 19 ori mai multe clase publice decât structuri. Acest lucru înseamnă că, în timp ce utilizați-l veți întâlni un număr mare de alocare. Chiar și creatorii limbajului nu a putut rezista tentației! Nu știu cum să colecteze statistici, dar folosind biblioteca de clasă de bază, veți observa cu siguranță că o mare cantitate de alocare de memorie are loc pe tot codul. Tot codul este scris bazează pe presupunerea că este o operațiune ieftină. Nici măcar nu poate aduce int pe ecran, fără a crea un obiect! Imaginați-vă: chiar folosind StringBuilder nu se poate pur și simplu, fără a trece de alocare int folosind biblioteca standard. Asta este o prostie.

Acesta se găsește nu numai în biblioteca standard. Chiar și în API-ul Unity (motor de joc. Pentru care performanța este importantă) pretutindeni metode care returnează referințele la obiecte sau tipuri de matrice de referință, sau forțând utilizatorul să aloce memorie pentru a invoca aceste metode. De exemplu, revenind o serie de GetComponents. Dezvoltatorii aloca memorie pentru o matrice doar pentru a vedea ce componente sunt în GameObject. Desigur, există API alternative, dar nu dacă urmați stilul de limbă, care nu sunt necesare alocări de memorie nu pot fi evitate. Cei de la Unitatea pentru a scrie „bun» C #, care are o performanță teribilă.

concluzie

Dacă dezvoltați o nouă limbă, pozhaluycta. Luați în considerare performanța. Nu „compilator suficient de inteligent“, nu se poate oferi după faptul, dacă nu pune această opțiune inițial. Da, este dificil de a oferi siguranță de tip fără de colectare a gunoiului. Da, este dificil de a colecta gunoiul, în cazul în care datele nu este uniformă. Da, este dificil de a rezolva problemele de domeniul de aplicare, în cazul în care puteți obține un pointer la orice locație în memorie. Da, există multe probleme. Dar dezvoltarea unei noi limbi - rezolvarea acestor probleme. De ce să faceți o nouă limbă în cazul în care nu este foarte diferit de cele care au fost create în anii '60?

Chiar dacă nu poate rezolva toate problemele, puteți încerca să se ocupe de cele mai multe dintre ele. Puteți utiliza tipurile de regiune din Rust pentru siguranță. Este posibil să se renunțe la ideea de „siguranță de tip cu orice preț“, creșterea numărului de rulare-controale (în cazul în care nu provoacă ratări suplimentare cache). matrice de covarianță în C # - este exact cazul. De fapt, acest tip de sistem de by-pass, ceea ce duce la eliberarea unei excepții în timpul rulării.

Rezumând, putem spune că, dacă doriți să dezvolte o C ++ alternativă la vysokoprozvoditelnogo cod, trebuie să se gândească la locație de date și localitate.