37.6 Optimalizálási labor correctness kapukkal

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.

37.6.1 A helyességi kapu mint az optimalizálás előfeltétele

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ő.

37.6.2 Allokációs költségvetés és a reserve szerződése

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.

37.6.3 Span, ranges és a materializáció döntése

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.

37.6.4 Arena és pmr: az élettartam a szerződés

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.

37.6.5 Többszálú pipeline és false sharing

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_size

Ezt 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.

37.6.6 Három profil, három kérdés

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.

37.6.7 Kidolgozott példa: ugyanaz az eredmény, kevesebb allokáció

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-tsan

A 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();
}

37.6.8 A bizonyított változás feltételei

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.

37.6.9 Tipikus hibák és javításuk

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.

37.6.10 Feladatok

  1. 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.

  2. 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.

  3. 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);
    }
  4. 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.

  5. 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.

37.6.11 Megoldások

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.

37.6.12 Összefoglaló

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.