Čtení: 30 minHodnotí se: zkouška

6 Výkon, copy elision, SSO a vocabulary typy

Po přečtení umíš
  • vysvětlit, proč je dynamická alokace drahá a větvení závisí na predikci
  • rozlišit povinné a nepovinné vynechání kopie
  • popsat SSO
  • vybrat mezi optional, expected a variant

6.1 Ukázka: krátký řetězec nealokuje

#include <cassert>
#include <cstdint>
#include <string>

int main() {
  std::string short_text = "ahoj";
  std::string long_text(100, 'x');
  auto inside = [](const std::string& s) {
    auto object = reinterpret_cast<std::uintptr_t>(&s);
    auto data = reinterpret_cast<std::uintptr_t>(s.data());
    return data >= object && data < object + sizeof(s);
  };
  assert(inside(short_text));
  assert(!inside(long_text));
}

Text je přímo v objektu, není alokace ani pomalý nepřímý přístup. To je Short String Optimization. V libstdc++ má std::string 32 bajtů a pojme 15 znaků.

6.2 Kde se ztrácí čas

6.3 Copy elision

Definice 6.2 (Povinné a nepovinné)

Od C++17 prvalue nevytváří dočasný objekt, dokud to není nutné. T a = make(); s prvalue tedy nekopíruje nikdy a stačí, aby funkce vracela T. NRVO u pojmenovaného lokálního objektu je jen povolená optimalizace.

#include <cassert>

struct Counter {
  static inline int copies = 0;
  Counter() = default;
  Counter(const Counter&) { ++copies; }
  Counter(Counter&&) noexcept { ++copies; }
};

Counter prvalue() { return Counter{}; }

int main() {
  Counter a = prvalue();
  (void)a;
  assert(Counter::copies == 0);  // zaručeno od C++17
}

Proto nepiš return std::move(local);: změníš prvalue na xvalue a zakážeš NRVO.

6.4 Empty Base Optimization

Prázdná třída má jako samostatný objekt velikost 1. Jako předek ani s [[no_unique_address]] místo nezabere. Proto má std::unique_ptr s bezstavovým deleterem stejnou velikost jako ukazatel.

#include <memory>

static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*));

6.5 Výběr vocabulary typu

Typ Kdy Příklad
std::optional<T> hodnota může chybět hledání v seznamu
std::expected<T, E> chyba s důvodem parser
std::variant<A, B> jedna z několika možností stavy
výjimka výjimečná situace nedostatek paměti
#include <cassert>
#include <expected>
#include <string>

std::expected<int, std::string> parse_digit(char c) {
  if (c < '0' || c > '9') return std::unexpected("není číslice");
  return c - '0';
}

int main() {
  assert(parse_digit('7').value() == 7);
  assert(parse_digit('x').error() == "není číslice");
}

Všechny tyto typy mají úložiště přímo v objektu, takže nealokují. Stojí na stejném principu jako kapitola 1.

6.6 Rozhraní knihovny

Soukromé členy jsou v hlavičce, takže změna jejich typu mění velikost třídy a rozbije binární kompatibilitu. PIMPL přesune detaily do .cpp za ukazatel. Cena je alokace a nepřímý přístup navíc.

6.7 Nedefinované chování

Překladač smí předpokládat, že k němu nedojde. Podle as-if pravidla smí udělat cokoli, co nezmění pozorovatelné chování platného programu. Přetečení int proto může smazat kontrolu v kódu.

6.8 Kvíz

Kvíz

1. Co je zaručené od C++17?
2. Proč je return std::move(local); chyba?
3. Parser má vrátit číslo, nebo popis chyby. Co vybereš?
4. Co platí o SSO? Může být správně více možností, ale také žádná.
5. Proč std::vector často porazí std::list i na vkládání doprostřed?
Ukázat řešeníHledání místa v seznamu je pomalý průchod rozptýlenými uzly a cache misses. Posun souvislé paměti je rychlý a alokace je jen občas.

6.9 Příklady na procvičení

Cvičení 6.3

Má T f() { T t; return t; } zaručené vynechání kopie? A T f() { return T{}; }?

Řešení

První je NRVO, jen povolené. Když se neuplatní, vezme se přesun. Druhé je prvalue, tam je to od C++17 zaručené.

Cvičení 6.4

Jak velký je std::unique_ptr s bezstavovým lambda deleterem?

Řešení

Stejně jako ukazatel, protože prázdný deleter nezabírá místo.

#include <memory>

auto deleter = [](int* p) { delete p; };
static_assert(sizeof(std::unique_ptr<int, decltype(deleter)>) == sizeof(int*));

6.10 Tahák

Tahák
  • Alokace a nepředvídatelné větvení jsou nejdražší.
  • Prvalue: bez kopie zaručeně. NRVO: nezaručené.
  • Nikdy return std::move(local).
  • EBO: prázdný předek nezabírá místo.
  • SSO: krátké řetězce v objektu.
  • optional chybí, expected chyba, variant jedna z.
  • PIMPL: stabilní ABI za cenu nepřímosti.

Zdroje

  • ni_epc_01: as-if pravidlo strany 15, jednotky překladu 18–25, nedefinované chování 26–32
  • ni_epc_04: std::optional strany 4–22, std::expected 23–30, std::variant 31–35
  • ni_epc_11: režie alokací strany 3–11, větvení 12–18, copy elision 19–38, EBO 39–43
  • ni_epc_12: std::string a SSO strany 3–50
  • NI-EPC cvičení 1: inlining, ABI, PIMPL