Rozložení paměti, zarovnání a reprezentace objektů
Pochopte požadavky na odsazení struktur a zarovnání, optimalizujte řazení členů podle velikosti, zjistěte o řádcích mezipaměti a falešném sdílení a používejte funkce C++20, jako je [[no_unique_address]] pro kompaktní rozvržení.
Požadavky na vyrovnání
Každý typ v C++ má požadavek na zarovnání — adresa, kam musí být objekt tohoto typu umístěn, musí být násobkem jeho zarovnání. Jedná se o hardwarový požadavek: většina CPU nemůže efektivně přistupovat k nesprávně zarovnaným datům a některé architektury (ARM, starší SPARC) budou mít chybu v nesprávném přístupu.
Běžná zarovnání na x86-64:
-char: 1 bajt
-short: 2 bajty
-int,float: 4 bajty
-double,long long, ukazatele: 8 bajtů
-long double: 16 bajtů (na některých platformách)
Zarovnání struktury je největší zarovnání ze všech jejích členů. Kompilátor vkládá bajty výplně mezi členy a na konec struktury, aby splnil požadavky na zarovnání.
Struct polstrování v praxi
Kompilátor vloží neviditelnou výplň, aby zarovnal každý člen. Uspořádání členů může dramaticky ovlivnit velikost struktury.
#include <iostream>
#include <cstddef> // offsetof
// BAD: wasteful ordering — 24 bytes
struct BadLayout {
char a; // offset 0, size 1
// 7 bytes padding (align double to 8)
double b; // offset 8, size 8
char c; // offset 16, size 1
// 3 bytes padding (align int to 4)
int d; // offset 20, size 4
// 0 bytes tail padding
}; // total: 24 bytes
// GOOD: optimized ordering — 16 bytes
struct GoodLayout {
double b; // offset 0, size 8
int d; // offset 8, size 4
char a; // offset 12, size 1
char c; // offset 13, size 1
// 2 bytes tail padding (struct alignment = 8)
}; // total: 16 bytes
int main() {
std::cout << "sizeof(BadLayout): " << sizeof(BadLayout) << '\n'; // 24
std::cout << "sizeof(GoodLayout): " << sizeof(GoodLayout) << '\n'; // 16
// Verify offsets
std::cout << "BadLayout::a offset: " << offsetof(BadLayout, a) << '\n'; // 0
std::cout << "BadLayout::b offset: " << offsetof(BadLayout, b) << '\n'; // 8
std::cout << "BadLayout::c offset: " << offsetof(BadLayout, c) << '\n'; // 16
std::cout << "BadLayout::d offset: " << offsetof(BadLayout, d) << '\n'; // 20
// Savings: 33% smaller per object
// With 1 million objects: 8 MB saved
}alignas a alignof
alignof(T)se dotazuje na zarovnání typu.alignas(N)vyžaduje specifické zarovnání pro proměnnou nebo typ. Ty jsou nezbytné pro SIMD, DMA buffery a zarovnání cache-line.
#include <iostream>
#include <new> // std::hardware_destructive_interference_size
// Query alignment
static_assert(alignof(int) == 4);
static_assert(alignof(double) == 8);
// Force alignment
struct alignas(64) CacheAligned {
int data[16]; // 64 bytes of data on a 64-byte boundary
};
static_assert(alignof(CacheAligned) == 64);
static_assert(sizeof(CacheAligned) == 64);
// Align a variable
alignas(256) char simd_buffer[256];
// Over-aligned allocation (C++17)
struct alignas(128) BigAlign {
double values[8];
};
int main() {
// C++17: hardware_destructive_interference_size
// Minimum offset to avoid false sharing (typically 64 on x86)
// NOTE: not available on all compilers
// std::cout << std::hardware_destructive_interference_size << '\n';
auto* p = new CacheAligned; // C++17 guarantees aligned allocation
std::cout << "alignof(CacheAligned): " << alignof(CacheAligned) << '\n';
std::cout << "address: " << p << '\n';
std::cout << "aligned? " << (reinterpret_cast<std::uintptr_t>(p) % 64 == 0
? "yes" : "no") << '\n';
delete p;
}Řádky mezipaměti a falešné sdílení
Moderní CPU nemají přístup k paměti bajt po bajtu – načítají celé řádky mezipaměti (typicky 64 bajtů na x86). Když dvě vlákna upravují proměnné, které jsou náhodou na stejné lince mezipaměti, hardware musí tuto linku mezi jádry neustále znehodnocovat a znovu načítat. Toto se nazývá falešné sdílení a může snížit výkon více vláken 10–100x.
K falešnému sdílení dochází, když:
- Dvě vlákna zapisují do různých proměnných
- Tyto proměnné jsou na stejné 64bajtové lince mezipaměti
- Protokol koherence mezipaměti (MESI) CPU vynucuje neustálé poskakování řádku mezipaměti
Oprava: vložte nebo zarovnejte horké proměnné do samostatných řádků mezipaměti. Použijtealignas(64)nebostd::hardware_destructive_interference_size(C++17, ale není podporováno na všech kompilátorech).
Naopak skutečné sdílení je, když se přistupuje k více souvisejícím proměnným společně. Jejich umístění na stejnou linku mezipaměti zlepšuje výkon – to jestd::hardware_constructive_interference_size.
Příklad falešného sdílení a oprava
Tento příklad ukazuje falešné sdílení: dva čítače na stejné lince mezipaměti způsobují masivní zpomalení, když jsou inkrementovány různými vlákny.
#include <atomic>
#include <thread>
#include <chrono>
#include <iostream>
constexpr int ITERATIONS = 100'000'000;
// BAD: counters on the same cache line — false sharing
struct BadCounters {
std::atomic<int> counter1{0};
std::atomic<int> counter2{0}; // likely on same 64-byte cache line
};
// GOOD: each counter on its own cache line
struct GoodCounters {
alignas(64) std::atomic<int> counter1{0};
alignas(64) std::atomic<int> counter2{0}; // guaranteed separate cache lines
};
template <typename Counters>
void benchmark(const char* label) {
Counters c;
auto start = std::chrono::high_resolution_clock::now();
std::thread t1([&] { for (int i = 0; i < ITERATIONS; ++i) ++c.counter1; });
std::thread t2([&] { for (int i = 0; i < ITERATIONS; ++i) ++c.counter2; });
t1.join();
t2.join();
auto elapsed = std::chrono::high_resolution_clock::now() - start;
std::cout << label << ": "
<< std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count()
<< " ms\n";
}
int main() {
std::cout << "sizeof(BadCounters): " << sizeof(BadCounters) << '\n'; // 8
std::cout << "sizeof(GoodCounters): " << sizeof(GoodCounters) << '\n'; // 128
benchmark<BadCounters>("False sharing ");
benchmark<GoodCounters>("No false sharing");
}Bitová pole a [[no_unique_address]] (C++20)
Bitová pole určují přesný počet bitů, které člen zabírá – užitečné pro hardwarové registry a hlavičky protokolů, ale jejich rozložení je definováno implementací. [[no_unique_address]] (C++20) umožňuje prázdným členům (bezstavové odstraňovače, alokátory) zabírat nula bajtů překrýváním s jinými členy, čímž nahrazuje starší vzor optimalizace prázdné báze.
#include <iostream>
#include <cstdint>
// Bit fields: pack flags tightly
struct TCPFlags {
uint8_t fin : 1;
uint8_t syn : 1;
uint8_t rst : 1;
uint8_t psh : 1;
uint8_t ack : 1;
uint8_t urg : 1;
uint8_t ece : 1;
uint8_t cwr : 1;
};
static_assert(sizeof(TCPFlags) == 1); // 8 flags in 1 byte
// [[no_unique_address]]: eliminate empty member overhead
struct Empty {};
struct Before { int x; Empty e; }; // sizeof == 8 (padding)
struct After { int x; [[no_unique_address]] Empty e; }; // sizeof == 4
static_assert(sizeof(Before) == 8);
static_assert(sizeof(After) == 4);
int main() {
TCPFlags tcp{};
tcp.syn = 1;
tcp.ack = 1;
std::cout << "SYN: " << tcp.syn << ", ACK: " << tcp.ack << '\n';
// Cannot take address of bit field: &tcp.syn is a compiler error
std::cout << "Before: " << sizeof(Before) << '\n'; // 8
std::cout << "After: " << sizeof(After) << '\n'; // 4
}1. Nikdy nepoužívejtememcpyk serializaci struktur napříč platformami – výplň a zarovnání členů se mezi kompilátory a architekturami liší.
2. Přenositelnost bitových polí: Pořadí bitů v rámci bajtu a výplň mezi bitovými poli jsou definované implementací. Nepoužívejte bitová pole pro multiplatformní drátové formáty – použijte explicitní bitovou manipulaci.
3.#pragma packje nestandardní: Struktury balení pro odstranění výplně může způsobit nesprávný přístup (ztráta výkonu nebo pády na architekturách s přísným zarovnáním, jako je ARM).
4. Členové pořadí od největšího po nejmenší:double, ukazatele (8 bajtů), potomint(4 bajty), potomshort(2 bajty), potomchar(1 bajt). Tím se minimalizuje vnitřní polstrování. Pro horké struktury použijtestatic_assert(sizeof(MyStruct) <= 64)k vynucení velikostí vhodných pro mezipaměť.
- Every type has an alignment requirement — the compiler inserts padding to satisfy it
- Uspořádejte členy od největšího k nejmenšímu zarovnání, abyste minimalizovali výplň
- Cache lines are 64 bytes on x86 — false sharing between threads is a major performance trap
- Pomocí
alignas(64)umístěte data na vlákno na samostatné řádky mezipaměti [[no_unique_address]](C++20) eliminates overhead of empty members- Bit fields pack data tightly but have implementation-defined layout — don't use for wire formats
Kvíz — Otestujte své znalosti
(15 XP)1. Given`struct S { char a; double b; char c; };`, what is the likely sizeof(S) on a 64-bit platform?
2. Co je falešné sdílení?
3. Co dělá`[[no_unique_address]]`(C++20)?