Hacker News

Piešķiršana uz kaudzes

komentāri

16 min read Via go.dev

Mewayz Team

Editorial Team

Hacker News

Kāpēc steku piešķiršana joprojām ir svarīga mūsdienu programmatūras inženierijā

Katru reizi, kad jūsu lietojumprogramma apstrādā pieprasījumu, izveido mainīgo vai izsauc funkciju, aizkulisēs tiek pieņemts kluss lēmums: kur šiem datiem jāatrodas atmiņā? Gadu desmitiem steku piešķiršana ir bijusi viena no ātrākajām un paredzamākajām programmētājiem pieejamajām atmiņas stratēģijām, tomēr tā joprojām tiek plaši pārprasta. Pārvaldītu izpildlaiku, atkritumu savācēju un mākoņdatošanas arhitektūru laikmetā izpratne par to, kā un kad veikt sadali steksā, var nozīmēt atšķirību starp lietojumprogrammu, kas apkalpo 10 000 vienlaicīgus lietotājus, un lietojumprogrammu, kas nodrošina mazāk nekā 500 lietotāju. Uzņēmumā Mewayz, kur mūsu platforma apkalpo vairāk nekā 138 000 uzņēmumu ar 207 mikrosekundēm integrētiem pārvaldības moduļiem.

Steck vs. Heap: fundamentāls kompromiss

Lielākajā daļā programmēšanas vidi atmiņa ir sadalīta divos primārajos reģionos: kaudze un kaudze. Stacks darbojas kā pēdējā ienākošā, pirmā ārā (LIFO) datu struktūra. Kad funkcija tiek izsaukta, stekā tiek nospiests jauns "rāmis", kas satur vietējos mainīgos, atgriešanas adreses un funkcijas parametrus. Kad šī funkcija atgriežas, viss rāmis tiek nekavējoties noņemts. Nav ne meklēšanas, ne grāmatvedības, ne sadrumstalotības — tikai viena rādītāja pielāgošana.

Turpretim kaudze ir liels atmiņas kopums, kurā piešķiršana un sadalīšana var notikt jebkurā secībā. Šādai elastībai ir jāmaksā: sadalītājam ir jāseko, kuri bloki ir brīvi, jāapstrādā sadrumstalotība un daudzās valodās jāpaļaujas uz atkritumu savācēju, lai atgūtu neizmantoto atmiņu. Kaudzītes piešķiršana tipiskā C programmā aizņem aptuveni 10 līdz 20 reizes ilgāk nekā steka piešķiršana. Atkritumu savākšanas valodās, piemēram, Java vai C#, pieskaitāmās izmaksas var būt vēl lielākas, ja tiek ņemtas vērā savākšanas pauzes.

Izpratne par šo kompromisu nav tikai akadēmiska. Ja veidojat programmatūru, kas apstrādā tūkstošiem darījumu sekundē — neatkarīgi no tā, vai tas ir rēķinu izrakstīšanas dzinējs, reāllaika analīzes informācijas panelis vai CRM, kas apstrādā lielapjoma kontaktpersonu importēšanu, pareizas piešķiršanas stratēģijas izvēle karstajiem ceļiem tieši ietekmē reakcijas laiku un infrastruktūras izmaksas.

Kā faktiski darbojas steku piešķiršana

Aparatūras līmenī lielākā daļa procesoru arhitektūru izmanto reģistru (steka rādītāju), lai izsekotu pašreizējai steka augšdaļai. Atmiņas piešķiršana stekā ir tikpat vienkārša kā šī rādītāja samazināšana par nepieciešamo baitu skaitu. Atdalīšana notiek otrādi: palieliniet rādītāju. Nav metadatu galvenes, nav brīvu sarakstu, nav blakus esošo bloku apvienošanas. Tāpēc steka piešķiršana bieži tiek raksturota kā tāda, kurai ir O(1) nemainīga laika veiktspēja ar niecīgām pieskaitāmām izmaksām.

Apsveriet iespēju izveidot funkciju, kas aprēķina rēķina rindas vienības kopējo summu. Tas var deklarēt dažus vietējos mainīgos: daudzuma veselu skaitli, vienības cenas mainīgo vērtību, nodokļa likmes mainīgo vērtību un rezultāta peldošo vērtību. Visas četras vērtības tiek iespiestas kaudzē, kad funkcija tiek ievadīta, un automātiski tiek atgūta, kad tā tiek izvadīta. Viss dzīves cikls ir deterministisks, un nav nepieciešama programmētāja vai atkritumu savācēja iejaukšanās.

Galvenais ieskats: steku piešķiršana nav tikai ātra — tā ir paredzama. Veiktspējai kritiskās sistēmās paredzamība bieži vien ir svarīgāka par neapstrādātu ātrumu. Funkcija, kas konsekventi tiek pabeigta 2 mikrosekundēs, ir vērtīgāka nekā tāda, kas vidēji ir 1 mikrosekunde, bet reizēm palielinās līdz 50 mikrosekundēm atkritumu savākšanas paužu dēļ.

Kad dot priekšroku steka piešķiršanai

Ne katrs datu vienums ietilpst kaudzē. Steka atmiņa ir ierobežota (parasti no 1 MB līdz 8 MB vienam pavedienam atkarībā no operētājsistēmas), un stekam piešķirtie dati nevar kalpot ilgāk par funkciju, kas to izveidoja. Tomēr ir skaidri gadījumi, kad steka piešķiršana ir labākā izvēle.

  • Īslaicīgi lokālie mainīgie: skaitītāji, akumulatori, pagaidu buferi, kas mazāki par dažiem kilobaitiem, un cilpas indeksi ir dabiski piemēroti stekam. Tie tiek izveidoti, izmantoti un izmesti vienas funkcijas ietvaros.
  • Fiksēta izmēra datu struktūras: masīvus ar zināmu kompilēšanas laiku lielumu, mazām struktūrām un vērtību veidiem var ievietot kaudzē bez pārpildes riska. 256 baitu buferis datuma virknes formatēšanai ir ideāls variants.
  • Veiktspējai svarīgas iekšējās cilpas: ja funkcija tiek izsaukta miljoniem reižu sekundē, piemēram, cenu aprēķina programma, kas atkārtojas produktu katalogos, kaudzes sadales likvidēšana cilpas pamattekstā var nodrošināt caurlaidspējas uzlabojumus 3 –10 reizes.
  • Reāllaika vai latentuma jutīgi ceļi: maksājumu apstrāde, tiešraides informācijas paneļa atjauninājumi un paziņojumu nosūtīšana dod labumu, izvairoties no nedeterministiskām atkritumu savākšanas pauzēm.
  • Rekursīvie algoritmi ar ierobežotu dziļumu: ja varat garantēt, ka rekursijas dziļums paliek drošās robežās, steka piešķirtie kadri nodrošina ātru un vienkāršu rekursīvo funkciju izpildi.

Praksē mūsdienu kompilatori lieliski spēj optimizēt steka lietojumu. Metodes, piemēram, aizbēgšanas analīze programmā Go un Java JIT kompilators, var automātiski pārvietot kaudzes piešķīrumus uz steku, kad kompilators pierāda, ka dati neietilpst funkciju darbības jomā. Izprotot šīs optimizācijas, varat rakstīt tīrāku kodu, vienlaikus gūstot labumu no steka veiktspējas.

Biežāk sastopamās nepilnības un kā no tām izvairīties

Visbēdīgi slavenākā ar steku saistītā kļūda ir steka pārpilde — tiek piešķirts vairāk datu, nekā steka var saturēt, parasti izmantojot neierobežotu rekursiju vai pārmērīgi lielus lokālos masīvus. Ražošanas vidē steka pārpilde parasti avarē pavedienu vai visu procesu bez gracioza atkopšanas ceļa. Tāpēc ietvari un operētājsistēmas nosaka steka lieluma ierobežojumus.

Vēl viena smalka kļūme ir norādes vai atsauces uz steka piešķirtajiem datiem. Tā kā steka atmiņa tiek atgūta brīdī, kad funkcija atgriežas, jebkurš rādītājs uz šo atmiņu kļūst par karājošu atsauci. Programmā C un C++ tas noved pie nenoteiktas uzvedības, kas var šķist, ka darbojas testēšanā, bet katastrofāli neizdodas ražošanā. Rustas aizņēmuma pārbaudītājs kompilēšanas laikā uztver šīs klases kļūdas, kas ir viens no iemesliem, kāpēc valoda ir iemantojusi sistēmu programmēšanu.

Trešā problēma ir saistīta ar pavedienu drošību. Katrs pavediens iegūst savu steku, kas nozīmē, ka steka piešķirtie dati pēc būtības ir pavedienu lokāli. Daudzos gadījumos tā patiešām ir priekšrocība — lai piekļūtu vietējiem mainīgajiem lielumiem, nav nepieciešamas slēdzenes. Tomēr izstrādātāji dažreiz pieļauj kļūdu, mēģinot koplietot steka piešķirtos datus starp pavedieniem, tādējādi radot sacensību apstākļus vai kļūdas, kas tiek izmantotas pēc brīvas lietošanas. Ja dati ir jākopīgo pa pavedieniem vai tie saglabājas pēc funkcijas izsaukuma, kaudze ir piemērota izvēle.

💡 DID YOU KNOW?

Mewayz replaces 8+ business tools in one platform

CRM · Invoicing · HR · Projects · Booking · eCommerce · POS · Analytics. Free forever plan available.

Start Free →

Steka piešķiršana dažādās valodās un ietvaros

Dažādas programmēšanas valodas apstrādā steku piešķiršanu ar atšķirīgu caurspīdīguma pakāpi. Programmā C un C++ programmētājam ir precīza kontrole: lokālie mainīgie tiek ievietoti kaudzē, un malloc vai new ievieto datus kaudzē. Programmā Go kompilators veic aizbēgšanas analīzi, lai pieņemtu lēmumu automātiski, un goroutines sākas ar niecīgām 2 KB kaudzēm, kas dinamiski pieaug — elegants risinājums, kas līdzsvaro drošību ar veiktspēju. PHP, valodu barošanas sistēmas, piemēram, Laravel, lielāko daļu vērtību piešķir, izmantojot savu iekšējo Zend Engine atmiņas pārvaldnieku, taču pamatprincipu izpratne palīdz izstrādātājiem rakstīt efektīvāku kodu pat lietojumprogrammu līmenī.

Komandām, kas veido sarežģītas platformas, piemēram, Mewayz inženieru komanda, kur viens pieprasījums var šķērsot CRM loģiku, rēķinu izrakstīšanas aprēķinus, algas nodokļa aprēķinus un analītikas apkopojumus, šie zemā līmeņa lēmumi ir apvienoti. Ja 207 moduļiem ir kopīgs izpildlaiks, atmiņas iedalījuma samazināšana pēc pieprasījuma pat par 15% var nozīmēt būtisku servera izmaksu samazinājumu un izmērāmus atbildes laika uzlabojumus galalietotājiem, kuri pārvalda savus uzņēmumus platformā.

JavaScript un TypeScript, kas nodrošina lielāko daļu moderno priekšgalda un Node.js aizmugursistēmas, atmiņas pārvaldībai pilnībā paļaujas uz V8 dzinēja atkritumu savācēju. Izstrādātāji nevar tieši piešķirt stekā, taču V8 optimizējošais kompilators (TurboFan) veic steka piešķiršanu iekšēji vērtībām, kuras var pierādīt, ka tās ir īslaicīgas. Rakstot mazas, tīras funkcijas ar vietējiem mainīgajiem, programma nodrošina vislabāko iespēju lietot šīs optimizācijas.

Praktiskas stratēģijas kaudzes spiediena samazināšanai

Pat ja strādājat augsta līmeņa valodā, kurā nevarat tieši kontrolēt steku un kaudzes piešķiršanu, varat pieņemt modeļus, kas samazina nevajadzīgu kaudzes spiedienu un ļauj veikt izpildlaika optimizāciju agresīvāk.

  1. Izdodiet priekšroku vērtību veidiem, nevis atsauces veidiem, ja valoda tos atbalsta. Programmā C#, izmantojot struct, nevis class maziem, bieži izveidotiem objektiem, tie tiek saglabāti kaudzē. Programmā Go mazo konstrukciju nodošana pēc vērtības, nevis pēc rādītāja nodrošina tādu pašu efektu.
  2. Izvairieties no piešķiršanas šaurās cilpās. Iepriekš piešķiriet buferus un atkārtoti izmantojiet tos iterācijās. Ja jums ir nepieciešama pagaidu sadaļa vai masīvs cilpas iekšpusē, kas tiek izpildīts 100 000 reižu, piešķiriet to vienreiz pirms cilpas un atiestatiet to katrā iterācijā.
  3. Izmantojiet objektu apvienošanu bieži izveidotiem un iznīcinātiem objektiem. Klasisks piemērs ir datu bāzes savienojumu pūli, taču modelis vienlīdz attiecas uz HTTP pieprasījuma objektiem, serializācijas buferiem un skaitļošanas konteksta struktūrām.
  4. Profils pirms optimizācijas. Tādi rīki kā Go pprof, Java async-profiler vai PHP Blackfire var precīzi noteikt, kur notiek piešķiršana. Optimizējot bez profilēšanas datiem, pastāv risks, ka tiks tērēts auksts ceļš, kas tiek izpildīts reti.
  5. Izmantojiet arēnas sadalītājus pakešu operācijām. Apstrādājot ierakstu grupu, piemēram, ģenerējot 500 rēķinus vai importējot 10 000 kontaktpersonu, arēnas sadalītājs paņem vienu lielu atmiņas bloku un sadala to ar stekam līdzīgu ātrumu, pēc tam atbrīvo visu bloku uzreiz, kad grupa pabeidz.

Šīs stratēģijas nav tikai teorētiskas. Kad SaaS platformas apstrādā reālas darba slodzes — maza uzņēmuma īpašnieks, kas ģenerē ikmēneša rēķinus, personāla vadītājs veic algu sarakstu 200 darbiniekiem, mārketinga komanda analizē kampaņas veiktspēju dažādos kanālos, efektīvas atmiņas pārvaldības kumulatīvais efekts ir patīkamāka, atsaucīgāka pieredze, ko lietotāji izjūt pat tad, ja viņi nekad nedomā par notiekošo.

a.

Veiktspējai apzinātas programmatūras veidošana mērogā

Steku piešķiršana ir viens no daudz lielākas veiktspējas mīklas elementiem, taču tas ir pamats. Izpratne par to, kā atmiņa darbojas zemākajā līmenī, sniedz inženieriem nepieciešamos garīgos modeļus, lai pieņemtu labākus lēmumus katrā steka slānī — no datu struktūru izvēles un API projektēšanas līdz infrastruktūras konfigurēšanai un resursu ierobežojumu iestatīšanai konteinerizētiem pakalpojumiem.

Uzņēmumiem, kas ikdienas darbību veikšanai paļaujas uz tādām platformām kā Mewayz, šo inženiertehnisko lēmumu atmaksāšanās ir jūtama: ātrāka lapu ielāde, vienmērīgāka mijiedarbība un pārliecība, ka maksimālās slodzes laikā sistēma nepasliktināsies. Ja rezervēšanas modulim reāllaikā ir jāpārbauda pieejamība vairākos desmitos kalendāru vai analītikas informācijas panelī tiek apkopoti dati par vairākām uzņēmējdarbības vienībām, pamatā esošajai atmiņas stratēģijai ir lielāka nozīme, nekā vairums lietotāju jebkad sapratīs.

Labākās programmatūras lietošana šķiet bez piepūles, jo tās veidotāji ir pārdomājuši detaļas, kas paliek neredzamas. Steku sadale — ātra, deterministiska un eleganta savā vienkāršībā — ir viena no tām detaļām, kuru ir vērts rūpīgi izprast neatkarīgi no tā, vai rakstāt savu pirmo programmu vai veidojat platformu, kas apkalpo tūkstošiem uzņēmumu visā pasaulē.

Bieži uzdotie jautājumi

Kas ir steka piešķiršana un kāpēc tā ir svarīga?

Steka piešķiršana ir atmiņas pārvaldības stratēģija, kurā dati tiek glabāti pēdējā ienākšanas un pirmās izejas struktūrā, ko automātiski pārvalda programmas izpildes plūsma. Tam ir nozīme, jo steka piešķirtā atmiņa ir ievērojami ātrāka nekā kaudzes piešķiršana — nav atkritumu savācēja, nav sadrumstalotības, un atdalīšana notiek uzreiz, kad funkcija atgriežas. Lietojumprogrammām, kas ir svarīgas veiktspējai, izpratne par steka piešķiršanu var ievērojami samazināt latentumu un uzlabot caurlaidspēju.

Kad man vajadzētu izmantot steka piešķiršanu, nevis kaudzes piešķiršanu?

Izmantojiet steku piešķiršanu maziem, īslaicīgiem mainīgajiem ar zināmu izmēru kompilēšanas laikā, piemēram, lokāliem veseliem skaitļiem, struktūrām un fiksēta izmēra masīviem. Kaudzes piešķiršana ir labāk piemērota lielām datu struktūrām, dinamiska izmēra kolekcijām vai objektiem, kuriem ir jāpārdzīvo funkcija, kas tos izveidoja. Galvenais noteikums: ja datu kalpošanas laiks atbilst funkcijas tvērumam un to lielums ir paredzams, steka gandrīz vienmēr ir ātrākā izvēle.

Vai ražošanas lietojumprogrammās var novērst steka pārpildes kļūdas?

Jā, steka pārpildes kļūdas var novērst ar disciplinētu inženiertehnisko praksi. Izvairieties no dziļas vai neierobežotas rekursijas, ierobežojiet lielu lokālo mainīgo piešķiršanu un, ja iespējams, izmantojiet iteratīvus algoritmus. Lielākā daļa valodu un operētājsistēmu ļauj konfigurēt steka lieluma ierobežojumus. Uzraudzības rīki un platformas risinājumi, piemēram, Mewayz, 207 moduļu biznesa operētājsistēma, kuras cena ir 19 ASV dolāri mēnesī, var palīdzēt komandām izsekot lietojumprogrammu stāvoklim un savlaicīgi uztvert veiktspējas regresijas.

Vai mūsdienu valodas joprojām gūst labumu no steku piešķiršanas?

Pilnīgi. Pat valodas ar pārvaldītiem izpildlaikiem, piemēram, Go, Rust, C# un Java, izmanto atsoļu analīzi, lai noteiktu, vai mainīgos var piešķirt stekā, nevis kaudzītē. Rust, izmantojot savu īpašumtiesību modeli, nodrošina steka pirmās piešķiršanu, un Go kompilators to agresīvi optimizē. Šīs mehānikas izpratne palīdz izstrādātājiem rakstīt kodu, ko kompilatori var optimizēt efektīvāk, tādējādi samazinot atmiņas lietojumu un ātrāku izpildes laiku.