Hvorfor er den første C++ (m)allokeringen alltid 72 KB? | Mewayz Blog
Hacker News

Hvorfor er den første C++ (m)allokeringen alltid 72 KB?

Kommentarer

9 min read Via joelsiks.com

Mewayz Team

Editorial Team

Hacker News

Mysteriet bak din første C++-tildeling

Du skriver et enkelt C++-program. En enkelt ny int. Fire byte. Du fyrer opp strace eller din favorittminneprofiler, og der er den – prosessen din ba bare om omtrent 72 KB fra operativsystemet. Ikke 4 byte. Ikke 64 byte. Hele 72 KB. Hvis du noen gang har stirret på det tallet og lurt på om verktøyet ditt løy for deg, er du ikke alene. Denne tilsynelatende bisarre oppførselen er et av de mest stilte spørsmålene blant C++-utviklere som graver inn i minnet for første gang, og svaret tar oss med på en fascinerende reise gjennom lagene som sitter mellom koden din og den faktiske maskinvaren.

Hva skjer når du ringer ny

For å forstå tallet på 72 KB, må du spore hele tildelingskjeden. Når C++-koden din kjører new int, oversetter kompilatoren det til et kall til operator new, som på de fleste Linux-systemer delegerer til malloc fra glibc. Men malloc ber ikke kjernen direkte om 4 byte minne. Kjernen opererer i sider - typisk 4 KB på x86_64 - og kostnadene for et systemanrop er enorme i forhold til enkel minnetilgang. Å ringe brk() eller mmap() for hver individuelle tildeling ville få ethvert ikke-trivielt program til å stoppe opp.

I stedet fungerer glibcs minneallokator – en implementering kalt ptmalloc2, som i seg selv stammer fra Doug Leas klassiske dlmalloc – som en mellommann. Den ber om store minneblokker fra kjernen på forhånd, og deler dem deretter i mindre biter etter hvert som programmet ditt trenger dem. Dette er den grunnleggende grunnen til at din første 4-byte-allokering utløser en mye større forespørsel til operativsystemet. Tildeleren er ikke sløsing. Det er strategisk.

Dissecting the 72 KB: Where the Bytes Go

Den første allokeringsoverheaden kommer fra flere forskjellige komponenter som kjøretiden må initialiseres før den kan gi deg en enkelt byte med brukbart minne. Å forstå hver komponent forklarer hvorfor tallet lander der det gjør.

For det første initialiserer glibcs malloc hovedarenaen – den primære bokføringsstrukturen som sporer alle allokeringer på hovedtråden. Denne arenaen inkluderer metadata for heapen, frilistepekere og bin-strukturer for forskjellige tildelingsstørrelser. Tildeleren forlenger programpausen via sbrk(), og den første utvidelsen styres av en intern parameter kalt M_TOP_PAD, som er standard på 128 KB utfylling. Den faktiske første forespørselen justeres imidlertid for sidejustering og eksisterende pauseposisjon, noe som ofte resulterer i en mindre første forespørsel – som vanligvis lander i nærheten av det tallet på 72 kB i en nystartet prosess.

For det andre, siden glibc 2.26, initialiserer allokatoren en tråd-lokal cache (tcache) ved første gangs bruk. Tcachen inneholder 64 hyller (én per størrelsesklasse med liten tildeling), hver kan inneholde opptil 7 bufrede biter. Selve tcache_perthread_struct bruker rundt 1 KB, men handlingen med å initialisere den utløser det bredere arenaoppsettet. For det tredje har C++-kjøretiden allerede utført allokeringer før main() til og med kjører – statiske konstruktører, initialisering av iostream-buffer for std::cout og venner, og lokaliseringsoppsett bidrar alle til det første heap-fotavtrykket.

Arenasystemet og hvorfor forhåndstildeling er smart

Beslutningen om å forhåndstildele en betydelig del av minnet i stedet for å be om det stykkevis er ikke en tilfeldighet ved implementering. Det er en bevisst ingeniørmessig avveining forankret i flere tiår med erfaring med systemprogrammering. Hvert kall til brk() eller mmap() involverer en kontekstbytte fra brukerplass til kjerneplass, modifikasjon av prosessens virtuelle minnetilordninger og potensielle sidetabelloppdateringer. På moderne maskinvare koster et enkelt systemanrop omtrent 100–200 nanosekunder – trivielt isolert sett, katastrofalt i stor skala.

Vurder et program som gjør 10 000 små tildelinger under initialisering. Uten forhåndstildeling vil det bety 10 000 systemanrop, som koster omtrent 1-2 millisekunder med ren overhead. Med en arenabasert allokator utløser den første allokeringen et enkelt systemanrop, og de påfølgende 9 999 allokeringene betjenes utelukkende i brukerområdet gjennom pekeraritmetikk og lenkelisteoperasjoner - hver tar omtrent 10-50 nanosekunder. Regnestykket er entydig: forhåndstildeling vinner i størrelsesordener.

De 72 KB du ser på din første tildeling er ikke bortkastet minne – det er en ytelsesinvestering. Tildeleren satser på at programmet ditt vil gjøre flere tildelinger snart, og i praktisk talt alle virkelige scenarier lønner denne innsatsen seg godt. Kostnaden for ubrukt virtuell adresseplass er i hovedsak null på moderne 64-bits systemer.

Virtuelt minne vs. fysisk minne: hvorfor det ikke spiller noen rolle

En vanlig bekymring blant utviklere som møter denne oppførselen for første gang er ressurssløsing. Hvis jeg bare trenger 4 byte, hvorfor bruker programmet mitt 72 KB? Den kritiske innsikten er at virtuelt minne ikke er fysisk minne. Når glibc utvider programpausen med 72 KB, oppdaterer kjernen prosessens virtuelle minnetilordninger, men den sikkerhetskopierer ikke umiddelbart disse sidene med fysisk RAM. De faktiske fysiske sidene tildeles på forespørsel gjennom sidefeil - bare når programmet ditt skriver til en spesifikk adresse, tildeler kjernen en ekte side med minne til den.

💡 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 →

Dette betyr at selv om prosessens virtuelle størrelse øker med 72 KB, øker dens resident set size (RSS) – mengden fysisk RAM som faktisk forbrukes – med bare sidene du faktisk berører. For en enkelt ny int er det vanligvis én side på 4 KB, pluss hvilke sider arenametadataene opptar. Den gjenværende virtuelle plassen sitter der, klar til bruk, og koster ingenting annet enn adresseplass – hvorav du har 128 TB på et 64-bits Linux-system.

Denne forskjellen er avgjørende ved profilering og overvåking av produksjonsapplikasjoner. Hvis du bygger programvare som trenger å spore reelt ressursforbruk – enten det er en SaaS-backend, en mikrotjeneste eller en analysepipeline som de som kjører på plattformer som Mewayz for forretningsdrift – bør du alltid overvåke RSS i stedet for virtuell størrelse. Verktøy som /proc/[pid]/smaps, valgrind --tool=massif og pmap kan gi deg nøyaktige fysiske minnefotavtrykk i stedet for villedende virtuelle minnetall.

Hvordan ulike tildelere håndterer den første tildelingen

72 KB-tallet er spesifikt for glibcs ptmalloc2. Andre allokatorer gjør forskjellige avveininger, og den første allokeringsoverheaden varierer deretter. Det er verdifullt å forstå disse forskjellene når du velger en allokator for ytelsessensitive applikasjoner.

  • jemalloc (brukt av Facebook, FreeBSD) — Bruker en mer detaljert arenastruktur med trådlokale cacher. Den innledende overhead har en tendens til å være høyere (ofte 200+ KB), men gir bedre flertråds ytelse på grunn av redusert låsestrid.
  • tcmalloc (Googles Thread-Caching Malloc) — Tildeler en per-thread cache på omtrent 2 MB som standard, med aggressiv forhåndstildeling. Innledende overhead er høyere, men påfølgende små tildelinger er ekstremt raske.
  • musl libcs malloc — Bruker en mye enklere design basert på mmap for alle tildelinger. Startkostnadene er minimale (ofte bare 4 KB per tildeling), men kostnadene per tildeling er høyere på grunn av hyppigere systemanrop.
  • mimalloc (Microsoft) — Bruker segmentbasert tildeling med 64 MB segmenter. Den første tildelingen utløser en virtuell reservasjon på 64 MB (med minimal fysisk forpliktelse), handelsadresseplass for eksepsjonell lokalitet og gjennomstrømning.

Valget mellom disse allokatorene avhenger helt av arbeidsmengden din. For langvarige serverapplikasjoner med tung flertråds-allokering, overgår jemalloc eller tcmalloc vanligvis glibcs ​​standard. For hukommelsesbegrensede innebygde systemer kan musls enklere tilnærming være å foretrekke til tross for lavere gjennomstrømning. For de fleste generelle desktop- og serverapplikasjoner representerer ptmalloc2s 72 kB innledende overhead en rimelig standard som fungerer bra uten tuning.

Justere den opprinnelige tildelingsatferden

Hvis standard innledende overhead på 72 KB virkelig er problematisk for ditt bruk – kanskje du skaper tusenvis av kortlivede prosesser, som hver enkelt utfører bare en håndfull allokeringer – gir glibc flere tunables via mallopt() og MALLOC_-familien av miljøvariabler.

M_TOP_PAD-parameteren kontrollerer hvor mye ekstra minne allokatoren ber om utover det som umiddelbart trengs. Å sette den til 0 med mallopt(M_TOP_PAD, 0) forteller tildeleren å be om bare det som er nødvendig, noe som reduserer den innledende overhead betraktelig. M_MMAP_THRESHOLD-parameteren kontrollerer størrelsen over hvilke tildelinger bruker mmap i stedet for arenaen. M_TRIM_THRESHOLD kontrollerer når frigjort minne returneres til operativsystemet. Og siden glibc 2.26, lar tunablene glibc.malloc.tcache_count og glibc.malloc.tcache_max deg kontrollere oppførselen til trådbufferen.

Men et ord til forsiktighet: Justering av disse parameterne uten nøye benchmarking gjør nesten alltid ting verre. Standardinnstillingene ble valgt basert på omfattende profilering i den virkelige verden, og de representerer et godt sted for de aller fleste arbeidsbelastninger. Med mindre du har sterke bevis fra produksjonsprofilering på at malloc overhead er en flaskehals – og du har målt effekten av endringene – la standardverdiene være i fred. For tidlig optimalisering av allokatoren er en spesielt lumsk form for yakbarbering som har brukt utallige ingeniørtimer til ubetydelig nytte.

Hva dette lærer oss om systemprogrammering

Den første tildelingsmysteriet på 72 kB er i sin kjerne en leksjon om abstraksjonslag. C++ gir deg en illusjon om at ny int tildeler 4 byte. Språkstandarden sier det. Din mentale modell sier det. Men mellom koden din og maskinvaren sitter en stabel med sofistikerte systemer – C++-kjøretiden, C-bibliotekallokatoren, kjernens virtuelle minneundersystem og maskinvarens MMU og TLB – som hver legger til sin egen atferd, optimaliseringer og overhead.

Dette er ikke en feil. Det er hele poenget med systemprogramvare. Hvert lag eksisterer for å løse et reelt problem: allokatoren eksisterer slik at du ikke trenger å foreta systemanrop for hver allokering. Det virtuelle minnesystemet eksisterer slik at du ikke trenger å administrere fysisk minne direkte. Sidefeilbehandleren eksisterer, så minnet blir begått dovent og effektivt. Hvert lag bytter en liten mengde åpenhet for en stor mengde ytelse og bekvemmelighet.

Utviklerne som bygger de mest pålitelige systemene med høyest ytelse, er de som forstår disse lagene – ikke fordi de trenger å tenke på dem hele tiden, men fordi når noe uventet skjer (som en mystisk 72 KB-tildeling), har de den mentale modellen for å forstå hvorfor. Enten du bygger et sanntidshandelssystem, en spillmotor eller en forretningsplattform som betjener tusenvis av brukere, er muligheten til å resonnere om hva koden din faktisk gjør på systemnivå det som skiller kompetente utviklere fra eksepsjonelle. 72 KB er ikke en feil. Det er fordeleren din som gjør jobben sin på strålende måte.

Bygg bedriftens operativsystem i dag

Fra frilansere til byråer, Mewayz driver 138 000+ bedrifter med 207 integrerte moduler. Start gratis, oppgrader når du vokser.

Opprett gratis konto →

Try Mewayz Free

All-in-one platform for CRM, invoicing, projects, HR & more. No credit card required.

Start managing your business smarter today

Join 30,000+ businesses. Free forever plan · No credit card required.

Ready to put this into practice?

Join 30,000+ businesses using Mewayz. Free forever plan — no credit card required.

Start Free Trial →

Ready to take action?

Start your free Mewayz trial today

All-in-one business platform. No credit card required.

Start Free →

14-day free trial · No credit card · Cancel anytime