Az optimalizálás ebben a könyvben nem attól számít sikernek, hogy egy
mérési szám kisebb lett, hanem attól, hogy a változtatás a helyességi
szerződéseket érintetlenül hagyta, és a javulás reprodukálható módon,
dokumentált workload mellett mutatható ki. Ez az alfejezet azt a labort
írja le, amelyben a jelölt optimalizálást először correctness kapukon
engedjük át, és csak utána értékeljük teljesítményét. A korábbi
részekből hozott fogalmakra építünk: a template, virtual és
std::function költségmodelljére (Template, virtual és std::function költsége
(37.5)), a ranges- és callback-élettartam szabályaira (Ranges- és callback-élettartam labor (30.6)),
valamint a shared ownership és konkurens reclamation review
szempontjaira (shared ownership, reclamation és
konkurens review (34.6)). A labornak nem célja az univerzális
„gyorsabb lett” kijelentés, és nem garantál zero-cost viselkedést
egyetlen nyelvi eszközre sem.
Optimalizálási labornak nevezzük azt a reprodukálható eljárást, amely egy jelölt változtatást (patch) ugyanazon a workloadon, ugyanazon build- és gépprofil mellett hasonlít össze a kiinduló állapottal, miközben a mérés előtt és után ugyanazokat a helyességi ellenőrzéseket futtatja. Correctness kapu (helyességi kapu) minden olyan automatizált ellenőrzés, amelynek sikertelensége esetén a teljesítményeredménytől függetlenül a változtatás elutasítandó. A kapu akkor érvényes, ha determinisztikus vagy legalább reprodukálható, ha a workload reprezentatív a célhasználatra, és ha a küszöbét a mérés előtt rögzítettük.
A labornak legalább négy kapuja van. Az első a funkcionális ekvivalencia: a kiinduló és a jelölt build ugyanarra az inputra ugyanazt a megfigyelhető kimenetet adja, hibakezelési ágakban is. A második a memória- és élettartam-kapu: a jelölt kód nem tartalmaz dangling hivatkozást, a tulajdonos és a nézet élettartama nem cserélődött fel, és a sanitizerek nem jeleznek. A harmadik a konkurencia-kapu: a szálas viselkedés nem lett versenyhelyzetessé, és a szinkronizációs szerződés változatlan. A negyedik az allokációs és komplexitási kapu: a változtatás nem növelte a kért allokációk számát vagy a dokumentált műveleti költséget anélkül, hogy ezt a tradeoffot külön indokolnánk.
A kapuk közös jellemzője, hogy nem a benchmark eredményéből következtetünk rájuk, hanem külön futnak. A mérés a kapuk után jön, és a mért szám önmagában soha nem írja felül a kapu eredményét. A gyakorlatban ez azt jelenti, hogy a jelölt buildből legalább két bináris készül: egy ellenőrző (sanitizeres vagy debug) és egy mérő (optimalizált). A kettő eredménye nem keverhető.
Az allokációs költségvetés (allocation budget) a
workload egy egységére, például egy feldolgozott kérésre vagy egy
parse-hívásra megengedett allokációk maximális száma és összes
byte-mennyisége. A költségvetés nem azt mondja meg, hogy egy allokáció
drága, hanem azt, hogy egy adott útvonalon hány allokáció történhet,
mielőtt a viselkedés a szerződésen kívül esik. A reserve
ennek egyik eszköze: előre lefoglalja a kapacitást, így a növekedés nem
jár ismételt újraallokációval és elem-mozgatással. A
reserve azonban nem kötelező és nem ingyenes: a túlbecsült
kapacitás memóriát foglal, az alulbecsült továbbra is újraallokál.
A költségvetés mérésének legegyszerűbb módja egy számláló
memory_resource, amely a
new_delete_resource-ra delegál, de közben számol. Ez a
C++17-es std::pmr felületre épül, és ugyanabban a kódban
használható a naiv és az optimalizált változat összehasonlítására.
#include <cstddef>
#include <memory_resource>
struct counting_resource final : std::pmr::memory_resource {
std::size_t allocations = 0;
std::size_t bytes = 0;
private:
void* do_allocate(std::size_t n, std::size_t a) override {
++allocations;
bytes += n;
return std::pmr::new_delete_resource()->allocate(n, a);
}
void do_deallocate(void* p, std::size_t n, std::size_t a) override {
std::pmr::new_delete_resource()->deallocate(p, n, a);
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
};A költségvetés ellenőrzése nem az, hogy „kevesebb allokáció jó”,
hanem hogy a mért szám egy előre rögzített korlát alatt marad, miközben
a funkcionális kapu zöld. Ha a reserve bevezetése csökkenti
az allokációk számát, de a csúcs-RSS nő, akkor a tradeoffot külön kell
dokumentálni, mert a költségvetés csak az allokációk számát korlátozta,
a memóriacsúcsot nem.
A std::span és a ranges view-k nem tulajdonolnak; egy
meglévő tárolóra mutatnak. Ezért olcsók, de élettartam-szerződésük
szigorú: a mögöttes tárolónak túl kell élnie a nézetet. A
materializáció ezzel szemben új tárolót hoz létre,
jellemzően allokálva, de a létrehozott objektum önálló tulajdonos lesz.
A döntés nem esztétikai: a nézet akkor jó, ha a fogyasztó élettartama
biztosan a tároló élettartamán belül van; a materializált másolat akkor,
ha a nézet túlélhetné a tárolót, vagy ha a pipeline-t többször,
egymástól függetlenül kell bejárni.
A tipikus hiba a lusta pipeline többszöri bejárása. Ha egy
views::transform láncot minden fogyasztónál újra
végigfuttatunk, a számítás megismétlődik; ha viszont materializálunk, a
számítás egyszer történik, de allokálunk és memóriát foglalunk. A
laborban ezt a döntést nem elméletben, hanem a két változat mérésével
kell meghozni, és a döntést a workloadhoz kell kötni. C++20-ban a
std::ranges::to még nem része a szabványnak, ezért a
materializálást kézi ciklussal vagy a tartomány két iterátorával kell
megírni; a C++23-as ranges::to használatát külön jelölni
kell, és a laborprofilban ellenőrizni kell a library támogatását.
A std::pmr::monotonic_buffer_resource egy
aréna: előre vagy növekvő blokkokban ad memóriát, és a
felszabadítást nem egyenként, hanem az erőforrás megszűnésekor végzi.
Ezért a belőle allokált objektumok élettartama nem haladhatja meg az
erőforrás élettartamát. Ez a szabály akkor is áll, ha a pmr
konténer vagy string látszólag önálló objektum: a háttértárolója az
arénában van, és a destruktora csak a saját logikai állapotát bontja le,
a memóriát nem adja vissza egyenként.
A helyes minta szerkezeti: az aréna legyen a legkülső, legkorábban létrehozott és legkésőbb megsemmisülő objektum, a belőle allokáló konténerek pedig utána jöjjenek létre. Így a destruktor-sorrend automatikusan biztosítja, hogy a konténer előbb szűnjön meg, mint az aréna. A fordító ezt nem ellenőrzi, a sanitizer pedig csak akkor jelez, ha a hibás hozzáférés tényleg megtörténik; ezért kell a szerkezeti szabályt a kódban is láthatóvá tenni, és a laborban egy negatív tesztben szándékosan előidézni a hibát.
A false sharing (hamis megosztás) akkor lép fel, amikor két szál különböző változókat ír, de ezek ugyanabba a cache line-ba esnek. A szálak nem versenyeznek ugyanazért az objektumért, tehát nincs adatverseny a nyelvi memóriamodell értelmében, a cache-koherencia miatt mégis folyamatosan érvénytelenítik egymás cache line-ját. A jelenség teljesítményhiba, nem helyességi hiba; a TSan nem fogja jelezni, mert nincs benne adatverseny. A mérés mégis fontos, mert a látszólag független számlálók egy structban ugyanabba a line-ba kerülhetnek.
A javítás tipikusan a változók külön cache line-ba helyezése. A C++17
erre szánt konstansa a <new> fejlécben:
std::hardware_destructive_interference_sizeEzt nem minden implementáció definiálja, ezért a laborprofilban rögzíteni kell, hogy a konkrét célon mi a használt érték; x86-64-en a 64 bájt tipikus, de ez nem hordozható garancia. A padding hatását teljesítménymérés mutatja, a helyességet továbbra is a TSan és a funkcionális tesztek őrzik.
A labor három buildprofilt használ, és a három profil három különböző
kérdésre válaszol. A debug profil (-O0 -g)
a referencia-szemantika és az állítások profilja: itt a lépésenkénti
viselkedés a fontos, a futásidő nem. A sanitizer profil
(-O1 -g -fsanitize=address,undefined, illetve külön
futásban -fsanitize=thread) a memória-, UB- és versenyhibák
profilja; a sanitizer erősen torzítja a futásidőt, ezért benchmarkra nem
használható. A release profil (-O2 vagy
-O3, -DNDEBUG) a teljesítménymérés profilja,
de önmagában nem elégséges helyességi referencia, mert az
assert eltűnik belőle, és az optimalizáló a nem definiált
viselkedésre támaszkodva „javíthat” is.
| Profil | Fő kapcsolók | Szerep | Korlát |
|---|---|---|---|
| debug | -O0 -g |
referencia-viselkedés, assert | nem benchmark |
| sanitizer | -O1 -g -fsanitize=address,undefined |
memória- és UB-hibák | torzított futásidő |
| sanitizer (szál) | -O1 -g -fsanitize=thread |
adatverseny | nem kombinálható ASan-nel |
| release | -O2 -DNDEBUG |
teljesítmény | nem egyedüli helyességi kapu |
A release profilban a helyességi kaput nem assert-re
kell építeni, mert a -DNDEBUG eltávolítja. Helyette
if (!condition) return 1; vagy a tesztkeretrendszer
nem-assert alapú ellenőrzése használható. A három profil eredménye soha
nem keveredik: a sanitizer futásidejéből nem következtetünk a release
teljesítményére, és a release zöld tesztje nem pótolja a sanitizeres
futást.
A példa egy kulcs=érték sorokból álló szöveget dolgoz
fel. A kiinduló változat minden kulcshoz külön
std::string-et allokál, a jelölt változat pmr
arénát és pmr::string-eket használ. A két változat ugyanazt
a logikát futtatja, a különbség az allokátor. A kód illusztratív, nem
mért eredmény: a laborban kell lefuttatni.
// pipeline.cpp
#include <charconv>
#include <cstddef>
#include <cstdio>
#include <memory_resource>
#include <string>
#include <string_view>
#include <vector>
namespace {
struct owned_record {
std::string key;
int value;
};
struct pmr_record {
std::pmr::string key;
int value;
};
std::vector<owned_record> parse_owned(std::string_view in) {
std::vector<owned_record> out;
std::size_t pos = 0;
while (pos < in.size()) {
std::size_t nl = in.find('\n', pos);
if (nl == std::string_view::npos) nl = in.size();
std::string_view line = in.substr(pos, nl - pos);
std::size_t eq = line.find('=');
if (eq != std::string_view::npos && eq + 1 < line.size()) {
int v = 0;
auto [p, ec] = std::from_chars(line.data() + eq + 1,
line.data() + line.size(), v);
if (ec == std::errc{} && p == line.data() + line.size())
out.push_back({std::string{line.substr(0, eq)}, v});
}
pos = nl + 1;
}
return out;
}
std::pmr::vector<pmr_record> parse_pmr(std::string_view in,
std::pmr::memory_resource* mr) {
std::pmr::vector<pmr_record> out{mr};
std::size_t pos = 0;
while (pos < in.size()) {
std::size_t nl = in.find('\n', pos);
if (nl == std::string_view::npos) nl = in.size();
std::string_view line = in.substr(pos, nl - pos);
std::size_t eq = line.find('=');
if (eq != std::string_view::npos && eq + 1 < line.size()) {
int v = 0;
auto [p, ec] = std::from_chars(line.data() + eq + 1,
line.data() + line.size(), v);
if (ec == std::errc{} && p == line.data() + line.size())
out.push_back(pmr_record{
std::pmr::string{line.substr(0, eq), mr}, v});
}
pos = nl + 1;
}
return out;
}
bool equivalent(const std::vector<owned_record>& a,
const std::pmr::vector<pmr_record>& b) {
if (a.size() != b.size()) return false;
for (std::size_t i = 0; i < a.size(); ++i) {
if (std::string_view{a[i].key} != std::string_view{b[i].key}) return false;
if (a[i].value != b[i].value) return false;
}
return true;
}
} // namespace
int main() {
const std::string_view input =
"alpha=1\nbeta=22\ngamma=333\ndelta=not-a-number\nepsilon=5\n";
auto baseline = parse_owned(input);
// Az arena legyen kívül, a belőle allokáló konténer utána.
std::pmr::monotonic_buffer_resource arena{4096};
auto candidate = parse_pmr(input, &arena);
// NDEBUG mellett is lefutó ellenőrzés (nem assert).
if (baseline.size() != 4 || !equivalent(baseline, candidate)) {
std::fprintf(stderr, "pipeline: eltérés a baseline-tól\n");
return 1;
}
for (const auto& r : candidate) {
if (r.key.get_allocator().resource() != &arena) {
std::fprintf(stderr, "pipeline: rossz resource\n");
return 1;
}
}
std::printf("pipeline: %zu rekord egyezik\n", candidate.size());
return 0;
}A pmr_record aggregátum, nem allokátortudatos típus,
ezért a vektor nem adja át neki a resource-ot; a kulcs
std::pmr::string-jét kifejezetten az mr-rel
kell létrehozni. A rekord ezután a vektorba mozog, és a mozgatott string
megtartja az allokátorát. Az ellenőrzés nem assert, hogy
NDEBUG mellett se tűnjön el; a kulcsok resource-át is
vizsgálja.
Buildjegyzőkönyv (lab-gcc profil). A
program g++ -std=c++20 -Wall -Wextra -Wpedantic
beállítással figyelmeztetés nélkül fordult, és
pipeline: 4 rekord egyezik kimenettel, 0 kóddal futott,
-DNDEBUG mellett is. A javítás előtti
out.emplace_back(line.substr(0, eq), mr) sor nem fordult,
és a beolvasott v értéket sem tárolta volna.
A példában az arena hamarabb jön létre, mint a
candidate, ezért a candidate előbb semmisül
meg. Ez a helyes sorrend. Ha az aréna egy belső blokkban jönne létre, a
visszaadott pmr konténer a blokk végén dangling hivatkozást
tartalmazna. A parse_pmr visszatérési értéke move-ol, az
allokátor pointere nem változik, ezért a visszaadott vektor továbbra is
az arénára mutat.
A buildparancsok a laborprofilhoz kötöttek; a fordító és a library konkrét verzióját rögzíteni kell.
# Release profil: teljesítménymérés
g++ -std=c++20 -O2 -g -Wall -Wextra -Wpedantic pipeline.cpp -o pipeline-release
# Sanitizer profil: memória és UB
g++ -std=c++20 -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer \
pipeline.cpp -o pipeline-asan
# TSan profil külön binárison, a szálas példára (nem kombinálható ASan-nel)
g++ -std=c++20 -O1 -g -fsanitize=thread -pthread false_sharing.cpp -o false_sharing-tsanA false_sharing.cpp a következő minta: két szál két
külön atomikus számlálót növel, amelyek egy structban, de
alignas-szal külön cache line-ban vannak. A rossz
változatból az alignas elhagyható, és a mérés megmutatja a
különbséget; a TSan egyik változatban sem jelez adatversenyt, mert
nincs.
// false_sharing.cpp
#include <atomic>
#include <cstdint>
#include <thread>
struct counters {
alignas(64) std::atomic<std::uint64_t> a{0};
alignas(64) std::atomic<std::uint64_t> b{0};
};
int main() {
counters c;
std::thread t1{[&] {
for (int i = 0; i < 1'000'000; ++i)
c.a.fetch_add(1, std::memory_order_relaxed);
}};
std::thread t2{[&] {
for (int i = 0; i < 1'000'000; ++i)
c.b.fetch_add(1, std::memory_order_relaxed);
}};
t1.join();
t2.join();
}Egy előtte-utána összehasonlítás akkor tekinthető bizonyított változásnak az adott workloadon, ha a következő feltételek együtt teljesülnek. Először is a két oldal forrása és inputja ugyanaz, a különbség csak a vizsgált patch. Másodszor a fordító, annak verziója, a kapcsolók, a gép és a terhelés azonos; a mérés a release profilban történik. Harmadszor a metrika és a küszöb előre rögzített, például a medián falidő, az allokációk száma vagy a csúcs-RSS. Negyedszer legalább öt ismétlés történik, és a mediánok különbsége nagyobb, mint a mért szórás; a kiugró futásokat nem elrejteni, hanem dokumentálni kell. Ötödször minden correctness kapu zöld a jelölt builden. Hatodszor a változás reprodukálható: megismételve, hidegindítással vagy más napon is hasonló irányt mutat. Végül a tradeoff dokumentált: memória, kódméret, komplexitás, hordozhatóság és a workload érvényességi tartománya.
Az előtte-utána táblázat sablonja a következő; a cellák a laborban töltendők ki, nem mért eredmények.
| Metrika | Kiinduló | Jelölt | Ellenőrzés |
|---|---|---|---|
| allokációk / hívás | kitöltendő | kitöltendő | counting_resource |
| összes allokált byte | kitöltendő | kitöltendő | counting_resource |
| medián falidő | kitöltendő | kitöltendő | 5+ futás, release |
| csúcs-RSS | kitöltendő | kitöltendő | /usr/bin/time -v |
| kódméret | kitöltendő | kitöltendő | size |
A „bizonyított” szó itt nem azt jelenti, hogy a változtatás minden workloadon gyorsabb, hanem azt, hogy az adott, dokumentált workloadon a javulás a mérési zajon túl van, és a helyességi szerződés nem sérült. Az univerzális benchmark és a zero-cost garancia nem cél; a döntés workloadhoz kötött.
Az egyik gyakori hiba, hogy a benchmarkot sanitizeres buildben futtatják. Az ASan és a TSan jelentős többletmunkát végez, ezért az így kapott idő nem hasonlítható a release időhöz. A javítás: a mérés mindig a release binárison történik, a sanitizer csak a kaput futtatja.
A második hiba a reserve túlméretezése: a kód kevesebb
allokációt végez, de a csúcs-RSS jelentősen nő, és ezt a jelentés nem
tartalmazza. A javítás: a költségvetésben az allokációk száma mellett a
byte- és a csúcs-RSS-korlátot is rögzíteni kell.
A harmadik hiba a nézet és a tároló élettartamának felcserélése. Egy
std::span vagy egy ranges view a megszűnt tárolóra mutat,
és a hiba csak akkor jelenik meg, ha a teszt a tároló megszűnése után is
használja a nézetet. A javítás: a nézetet csak a tároló élettartamán
belül adjuk tovább, vagy materializáljunk; a laborban negatív teszt is
legyen, amely a hibás mintát szándékosan előidézi, és a sanitizerrel
igazolja a hibát.
A negyedik hiba az aréna élettartamának elrejtése egy függvényen
belül. Ha a pmr konténer visszatér a függvényből, de az
aréna a függvény lokális változója, a konténer dangling. A javítás: az
aréna a hívó felelőssége, a visszatérési típus pedig dokumentálja, hogy
a konténer az arénára mutat.
Az ötödik hiba a false sharing és az adatverseny összekeverése. A TSan a versenyt detektálja, a false sharinget nem; a javulás mérése és a helyesség ellenőrzése két külön kapu. A javítás: a paddinget teljesítménymérés igazolja, a szálak közötti helyességet továbbra is a TSan és a funkcionális teszt.
Magyarázó feladat. Definiálja a saját workloadjára a bizonyított változás öt feltételét, és adja meg, melyik metrika melyik kapuhoz tartozik. Készítsen egy előtte-utána táblázatot üres cellákkal és a kitöltés szabályaival.
Kódolási feladat. Egészítse ki a
pipeline.cpp példát egy
counting_resource-szal, és mérje meg a két parse-változat
allokációszámát és byte-mennyiségét. Állítson be egy költségvetést, és
fogalmazza meg, milyen feltétel mellett tekinti a költségvetést
teljesültnek.
Diagnosztikai feladat. Az alábbi vázlatban az
aréna a függvényen belül jön létre, a visszatérési érték viszont
pmr konténer. Mutassa meg, miért dangling, és javítsa a
kódot a hívó felelősségének átadásával.
std::pmr::vector<pmr_record> bad_parse(std::string_view in) {
std::pmr::monotonic_buffer_resource arena{4096};
return parse_pmr(in, &arena);
}Kódolási feladat. A
false_sharing.cpp-ben távolítsa el az
alignas(64)-eket, mérje meg a futásidőt, majd tegye vissza
őket. Magyarázza meg, miért nem jelez a TSan egyik változatban sem
adatversenyt, és mitől függ a mért különbség.
Elemző feladat. Hasonlítson össze egy
std::span-alapú nézetet és egy materializált
std::vector-t ugyanarra a feldolgozásra. Adja meg, melyik
esetben nő az allokációk száma, melyik esetben a számítás ismétlődik, és
melyik élettartam-szerződés szigorúbb.
1. megoldás. A bizonyított változás öt feltétele a saját workloadra: (a) azonos forrás és input, csak a patch a különbség; (b) azonos fordító, verzió, kapcsolók, gép és terhelés, mérés release profilban; (c) előre rögzített metrika és küszöb; (d) legalább öt ismétlés, a mediánok különbsége nagyobb a szórásnál; (e) minden correctness kapu zöld, és a változás reprodukálható. A metrika és a kapu hozzárendelése: a funkcionális ekvivalencia a kimenet-összehasonlítás kapuja; az allokációszám a költségvetés kapuja; a medián falidő a teljesítmény metrikája; a csúcs-RSS a memória-kapu; a sanitizeres futás a memória- és konkurencia-kapu. Az előtte-utána táblázatot a fenti sablon szerint kell kitölteni, a cellákban a mért értékekkel és a futások számával. Ellenőrzési feltétel: a táblázat minden sora mellett szerepel a mérés módja és a küszöb.
2. megoldás. A counting_resource a
new_delete_resource-ra delegál, és a
do_allocate-ban növeli az allocations és
bytes számlálót. A naiv változatot
counting_resource-szal kell futtatni, de a
std::vector<owned_record> default allokátora nem pmr,
ezért vagy a naiv változatot is pmr-esíteni kell a méréshez, vagy a két
változatot külön mérjük, és a különbséget a pmr
bevezetésének tulajdonítjuk. A költségvetés például: egy parse-hívásra
legfeljebb annyi allokáció, amennyi a sorok száma plusz a vektor
növekedési lépései, és legfeljebb a bemenet méretének konstansszorosa
byte-ban. Ellenőrzési feltétel: a jelölt allokációszáma nem nagyobb a
kiindulónál, a funkcionális kapu zöld, és a byte-mennyiség a rögzített
korlát alatt van.
3. megoldás. A bad_parse az arénát a
saját scope-jában hozza létre, majd visszaad egy
pmr::vector-t, amelynek minden eleme és a vektor puffere is
az arénából származik. A függvény visszatérésekor az aréna megszűnik, a
visszaadott vektor pedig dangling pointereket tartalmaz. A javítás: az
aréna a hívóé legyen, a függvény paraméterként kapja a
memory_resource*-t, és a visszatérési típus dokumentálja,
hogy a konténer az arénára mutat.
std::pmr::vector<pmr_record> parse_into(std::string_view in,
std::pmr::memory_resource* mr) {
return parse_pmr(in, mr);
}
// hívó:
std::pmr::monotonic_buffer_resource arena{4096};
auto rows = parse_into(input, &arena);Ellenőrzési feltétel: az aréna deklarációja megelőzi a konténerét, a konténer az aréna megszűnése előtt megszűnik, és az ASan-es futás nem jelez. A hibás változatot negatív tesztként érdemes megtartani, de csak izoláltan, dokumentáltan.
4. megoldás. Az alignas(64)
eltávolításával a két atomikus számláló valószínűleg ugyanabba a cache
line-ba kerül, és a két szál írásai folyamatosan érvénytelenítik egymás
line-ját; a futásidő nő. A TSan azért nem jelez, mert a két szál
különböző objektumokat ír, nincs köztük adatverseny a nyelvi
memóriamodell szerint. A mért különbség a cache line méretétől, a szálak
számától, a magok elhelyezkedésétől és a háttérterheléstől függ; ezért a
mérés csak a rögzített laborprofilra érvényes. Ellenőrzési feltétel: a
funkcionális eredmény változatlan, a TSan mindkét változatban tiszta, és
a paddingolt változat mediánja legalább a mért szórásnál nagyobb
mértékben jobb a nem paddingolt változatnál.
5. megoldás. A std::span-alapú nézet
nem allokál, de a mögöttes tároló élettartamához kötött; ha a tároló
megszűnik, a nézet dangling. A materializált std::vector
allokál, de önálló tulajdonos, ezért túlélheti a forrástárolót. Ha
ugyanazt a lusta pipeline-t több fogyasztó járja be, a számítás
ismétlődhet; a materializálás ezt egyszeri költséggé alakítja, cserébe
allokál. A szigorúbb élettartam-szerződés a nézeté: a nézet csak a
tároló élettartamán belül érvényes. Ellenőrzési feltétel: a döntés
mellett szerepel a mért allokációszám, a nézet élettartamának indoklása
és a sanitizeres teszt eredménye.
Az optimalizálási labor sorrendje rögzített: először a funkcionális,
memória- és konkurencia-kapuk, aztán a mérés. Az allokációs költségvetés
a reserve-dzsel együtt az allokációk számát és
byte-mennyiségét korlátozza, de a csúcs-RSS-t külön kell figyelni. A
span és a ranges view olcsó, de élettartamhoz kötött; a
materializáció allokál, de önálló tulajdonos. Az aréna és a
pmr esetében az élettartam-szerződés szerkezeti: az aréna
legyen a legkülső objektum. A false sharing teljesítményhiba, nem
adatverseny; a TSan nem fogja meg, a mérés igen. A három profil három
különböző kérdésre válaszol, és az eredményeik nem keverhetők. A
bizonyított változás feltételei az azonos környezet, az előre rögzített
metrika, az ismételt mérés, a zöld kapuk és a dokumentált tradeoff. Az
univerzális benchmark és a zero-cost garancia nem része a labornak; a
döntés mindig a dokumentált workloadhoz kötött.