Hacker News

Зошто првата C++ (m)распределба е секогаш 72 KB?

Коментари

1 min read Via joelsiks.com

Mewayz Team

Editorial Team

Hacker News

Тајната зад вашата прва распределба на C++

Пишувате едноставна програма C++. Единствена нова инт. Четири бајти. Го активирате strace или вашиот омилен мемориски профил, и тука е - вашиот процес бараше приближно 72 KB од оперативниот систем. Не 4 бајти. Не 64 бајти. Целосни 72 KB. Ако некогаш сте зјапале во таа бројка и сте се запрашале дали ве лаже вашиот алат, не сте сами. Ова навидум бизарно однесување е едно од најчесто поставуваните прашања меѓу програмерите на C++ кои за прв пат копаат по внатрешните делови на меморијата, а одговорот не води на фасцинантно патување низ слоевите што се наоѓаат помеѓу вашиот код и вистинскиот хардвер.

Што се случува кога ќе повикате ново

За да ја разберете бројката од 72 KB, треба да го следите целиот синџир на распределба. Кога вашиот C++ код ќе изврши нов int, компајлерот го преведува тоа во повик до оператор нов, кој на повеќето системи на Linux делегира на malloc од glibc. Но, malloc не бара директно од кернелот 4 бајти меморија. Јадрото работи на страници - обично 4 KB на x86_64 - и цената на системскиот повик е огромна во однос на едноставниот пристап до меморијата. Повикувањето на brk() или mmap() за секоја поединечна распределба ќе ја запре секоја нетривијална програма.

Наместо тоа, распределувачот на меморија на glibc - имплементација наречена ptmalloc2, која самата потекнува од класичниот dlmalloc на Даг Леа - делува како посредник. Бара големи блокови на меморија од кернелот однапред, а потоа ги резба на помали делови како што и се потребни на вашата програма. Ова е основната причина зошто вашата прва распределба од 4 бајти предизвикува многу поголемо барање до оперативниот систем. Алокаторот не е расипнички. Тоа е стратешко.

Растрчување на 72 KB: Каде одат бајтите

Почетната распределба над главата доаѓа од неколку различни компоненти кои траењето мора да ги иницијализира пред да може да ви предаде дури и еден бајт употреблива меморија. Разбирањето на секоја компонента објаснува зошто бројот слетува таму каде што доаѓа.

Прво, malloc на glibc ја иницијализира главната арена - примарната книговодствена структура која ги следи сите распределби на главната нишка. Оваа арена вклучува метаподатоци за купот, покажувачи на слободна листа и структури за ѓубре за различни големини на распределба. Алокаторот ја продолжува паузата на програмата преку sbrk(), а почетната екстензија е управувана од внатрешен параметар наречен M_TOP_PAD, кој стандардно е 128 KB полнење. Сепак, вистинското почетно барање е приспособено за усогласување на страниците и постоечката позиција на прекин, што често резултира со помало прво барање - обично се спушта блиску до таа бројка од 72 KB на ново започнатиот процес.

Второ, од glibc 2.26, алокаторот иницијализира локален кеш со низа (tcache) при првата употреба. Tcache содржи 64 канти (еден по класа со големина на мала распределба), секоја способна да собере до 7 кеширани парчиња. Самата tcache_perthread_struct троши околу 1 KB, но чинот на нејзино иницијализирање го активира поширокото поставување на арената. Трето, времето на извршување на C++ веќе изврши распределба пред да се изврши вашата main() - статични конструктори, иницијализација на баферот на iostream за std::cout и пријателите, како и поставување на локација, сите придонесуваат за тој почетен отпечаток на грамада.

Системот на арената и зошто е паметна претходна распределба

Одлуката однапред да се додели значителен дел од меморијата наместо да се бара поединечно, не е случајна имплементација. Тоа е намерна инженерска компромиса која е вкоренета во децениското искуство во програмирање на системи. Секој повик до brk() или mmap() вклучува контекстно префрлување од кориснички простор во простор на јадрото, модификација на мапирањата на виртуелната меморија на процесот и потенцијални ажурирања на табелата на страници. На модерен хардвер, еден системски повик чини приближно 100-200 наносекунди - тривијално изолирано, катастрофално во обем.

Размислете програма која прави 10.000 мали алокации за време на иницијализацијата. Без претходна распределба, тоа би значело 10.000 системски повици, што чинат приближно 1-2 милисекунди чисто надземни трошоци. Со алокатор базиран на арена, првата распределба активира еден системски повик, а последователните 9.999 алокации се сервисираат целосно во корисничкиот простор преку операциите за аритметика со покажувачот и поврзаните листа - секоја одзема околу 10-50 наносекунди. Математиката е недвосмислена: пред-распределбата победува по редови на големина.

72 KB што ги гледате на вашата прва распределба не се залудно потрошена меморија - тоа е инвестиција за перформанси. Алокаторот се обложува дека вашата програма наскоро ќе направи повеќе распределби, и практично во секое реално сценарио, тој облог одлично се исплати. Цената на неискористениот виртуелен адресен простор е во суштина нула кај модерните 64-битни системи.

Виртуелна меморија наспроти физичка меморија: зошто не е важно

Заедничка загриженост кај програмерите кои се соочуваат со ова однесување за прв пат е губењето ресурси. Ако ми требаат само 4 бајти, зошто мојата програма троши 72 KB? Критичкиот увид е дека виртуелната меморија не е физичка меморија. Кога glibc го продолжува прекинот на програмата за 72 KB, кернелот ги ажурира мапирањата на виртуелната меморија на процесот, но не ги поддржува веднаш тие страници со физичка RAM меморија. Вистинските физички страници се распределуваат на барање преку грешки на страницата - само кога вашата програма пишува на одредена адреса, кернелот му доделува вистинска страница од меморијата.

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

Ова значи дека иако виртуелната големина на вашиот процес се зголемува за 72 KB, неговата големина на резидентни множества (RSS) - количината на физичка RAM меморија што всушност се троши - се зголемува само за страниците што всушност ги допирате. За една нова ознака, тоа е обично една страница од 4 KB, плус без оглед на страниците што ги зафаќаат метаподатоците на арената. Преостанатиот виртуелен простор се наоѓа таму, подготвен за употреба, не чини ништо друго освен адресен простор - од кои имате 128 TB на 64-битен Linux систем.

Оваа разлика е критична при профилирање и следење на производствените апликации. Ако градите софтвер кој треба да ја следи вистинската потрошувачка на ресурси - без разлика дали е SaaS заднина, микросервис или аналитички канал како оние што работат на платформи како што се Mewayz за деловни операции - секогаш треба да ја следите RSS наместо виртуелна големина. Алатките како што се /proc/[pid]/smaps, valgrind --tool=massif и pmap може да ви дадат точни отпечатоци од физичката меморија наместо да погрешат бројки за виртуелната меморија.

Како различни алокатори се справуваат со првата распределба

Фигурата од 72 KB е специфична за ptmalloc2 на glibc. Другите распределувачи прават различни компромиси, а првичните трошоци за распределба се разликуваат соодветно. Разбирањето на овие разлики е вредно при изборот на распределувач за апликации чувствителни на перформанси.

  • jemalloc (се користи од Facebook, FreeBSD) — Користи повеќе грануларна структура на арената со нишки локални кешови. Почетните трошоци обично се повисоки (често 200+ KB), но обезбедуваат подобри перформанси со повеќе нишки поради намалената борба за заклучување.
  • tcmalloc (Google's Thread-Caching Malloc) — Стандардно доделува кеш по нишка од приближно 2 MB, со агресивна предраспределба. Почетните трошоци се повисоки, но последователните мали алокации се исклучително брзи.
  • musl libc's malloc — Користи многу поедноставен дизајн базиран на mmap за сите распределби. Почетните трошоци се минимални (често само 4 KB по распределба), но трошоците за распределба се повисоки поради почестите системски повици.
  • mimalloc (Microsoft) — Користи распределба базирана на сегменти со сегменти од 64 MB. Првата распределба активира виртуелна резервација од 64 MB (со минимална физичка посветеност), тргување со адресен простор за исклучителна локација и пропусност.

Изборот помеѓу овие распределувачи целосно зависи од вашиот обем на работа. За долготрајни серверски апликации со тешка распределба со повеќе нишки, jemalloc или tcmalloc обично ги надминува стандардните на glibc. За вградените системи со ограничена меморија, поедноставниот пристап на musl може да се претпочита и покрај помалата пропусност. За повеќето десктоп и серверски апликации за општа намена, почетните трошоци на ptmalloc2 од 72 KB претставуваат разумно стандардно кое работи добро без подесување.

Подесување на однесувањето на почетната распределба

Ако стандардните почетни трошоци од 72 KB се навистина проблематични за вашиот случај на употреба - можеби создавате илјадници краткотрајни процеси, од кои секој прави само неколку алокации - glibc обезбедува неколку приспособливи преку mallopt() и семејството на променливи на околината MALLOC_

.

Параметарот M_TOP_PAD контролира колку дополнителна меморија бара алокаторот над она што е веднаш потребно. Поставувањето на 0 со mallopt(M_TOP_PAD, 0) му кажува на алокаторот да го побара само она што е потребно, намалувајќи ги првичните трошоци значително. Параметарот M_MMAP_THRESHOLD ја контролира големината над која алокациите користат mmap наместо арената. M_TRIM_THRESHOLD контролира кога ослободената меморија се враќа во ОС. И од glibc 2.26, приспособите glibc.malloc.tcache_count и glibc.malloc.tcache_max ви дозволуваат да го контролирате однесувањето на кешот на низата.

Сепак, претпазливост: подесувањето на овие параметри без внимателен бенчмаркинг речиси секогаш ги влошува работите. Стандардните поставки беа избрани врз основа на опширно реалниот свет профилирање, и тие претставуваат слатка точка за огромното мнозинство на оптоварувања. Освен ако немате цврсти докази од профилирањето на производството дека надземните трошоци на malloc се тесно грло - и сте го измериле влијанието на вашите промени - оставете ги стандардните вредности. Прераната оптимизација на алокаторот е особено подмолна форма на бричење јак што потроши безброј инженерски часови за незначителна корист.

Што нè учи ова за системско програмирање

Тајната за првата распределба од 72 KB е, во својата суштина, лекција за слоеви на апстракција. C++ ви дава илузија дека new int доделува 4 бајти. Јазичниот стандард го кажува тоа. Вашиот ментален модел го кажува тоа. Но, помеѓу вашиот код и хардверот се наоѓа куп софистицирани системи - времетраење на C++, распределувач на библиотека C, потсистем за виртуелна меморија на кернелот и MMU и TLB на хардверот - секој додава свои однесувања, оптимизации и надземни трошоци.

Ова не е мана. Тоа е целата поента на системскиот софтвер. Секој слој постои за да реши вистински проблем: алокаторот постои за да не морате да правите системски повици за секоја распределба. Системот за виртуелна меморија постои за да не морате директно да управувате со физичката меморија. Управувачот со грешки на страницата постои, така што меморијата се користи мрзеливо и ефикасно. Секој слој заменува мала количина на транспарентност за голема количина на перформанси и практичност.

Програмерите кои ги градат најсигурните системи со највисоки перформанси се оние кои ги разбираат овие слоеви - не затоа што треба постојано да размислуваат за нив, туку затоа што кога ќе се случи нешто неочекувано (како мистериозна распределба од 72 KB), тие имаат ментален модел да разберат зошто. Без разлика дали градите систем за тргување во реално време, мотор за игри или деловна платформа која опслужува илјадници корисници, способноста да размислувате што всушност прави вашиот код на ниво на системот е она што ги одвојува компетентните развивачи од исклучителните. 72 KB не е баг. Тоа е вашиот алокатор брилијантно ја врши својата работа.

Изградете го вашиот бизнис оперативен систем денес

Од хонорарци до агенции, Mewayz напојува над 138.000 бизниси со 207 интегрирани модули. Започнете бесплатно, надградете кога ќе пораснете.

Креирај