Diskuze: C/C++ dynamická alokace paměti
V předchozím kvízu, Online test znalostí C++, jsme si ověřili nabyté zkušenosti z kurzu.

Člen

Zobrazeno 10 zpráv z 10.
//= Settings::TRACKING_CODE_B ?> //= Settings::TRACKING_CODE ?>
V předchozím kvízu, Online test znalostí C++, jsme si ověřili nabyté zkušenosti z kurzu.
Dynamická alokace (malloc, calloc, realloc, new...) znamená, že požádáš tzv. správce haldy o blok paměti určité velikosti. Správce ti jej přidělí, pokud takový najde či dokáže vytvořit. Pak ty alokační funkce vrátí adresu různou od NULL.
Správce haldy ale nepracuje ve vzduchoprázdnu – sám si obstarává paměť, jejíž části pak přiděluje, od operačního systému. Operační systém mu ji ale dokáže dát pouze po větších kusech (tzv. stránkách či skupinách stránek, minimálně 4 KB, ale může být i víc), takže správce si v nich musí ještě udržovat nějaká metadata, aby věděl, jaké a jak velké bloky paměti přidělil již někomu dalšímu.
To v zásadě znamená, že pokud si od správce vyžádáš blok paměti o velikosti sizeof(int), tak bys neměl zapisovat mimo něj, protože můžeš (a obvykle se to stane) správci přepsat metadata o tom, jaké části oněch velkých kusů paměti (,které má přímo od jádra operačního systému) již přidělil. Správce z toho většinou není zrovna nadšený, čehož výsledkem je pád programu. Může se ale stát, že tebou přepsaná metadata bude potřebovat až někdy později, takže program nespadne hned.
Tzn. to, co tam děláš, je z té kategorie totálně špatně. Chyby tohoto typu se velmi špatně hledají právě proto, že se projevují o hodně později. Lze ale obětovat (někdy značnou) část výkonu a např. po každé (de)alokaci správce haldy požádat, aby se podíval, zda-li jsou jeho metadata v pořádku.
Jinak, implementace operátoru new v C++ bývá založena právě na malloc. Podobně, std::vector v případě, že se mu data nevejdou, své interní pole přealokuje na větší a původní obsah překopíruje.
Samozřejmě, paměť, kterou si od správce haldy vyžádáš, bys mu měl i vrátit.
Dost už toho bylo řečeno v předchozím příspěvku, přesto ještě doplním pár informací.
Jazyk C vznikl jako náhrada Assembleru a tak Ti bez jakýchkoli varovných hlášení umožní přístup do paměti, která ovšem nemusí být tvoje. Toto může způsobit pád programu (v tom lepším případě). Vysvětlení viz příspěvek výše. To je důvod proč to funguje, ale je to zkrátka celé úplně špatně. Veškeré ošetření chyb nechává na programátorovi.
Funkce malloc() vrací ukazatel na začátek přiděleného bloku paměti. Je třeba si uvědomit, že toto přidělení paměti není automatické a může selhat. Je tedy bezpodmínečně nutné otestovat vracený ukazatel na to zda přidělení paměti proběhlo v pořádku.
Operátor preprocesoru sizeof je využíván v době překladu, je tedy vhodné jej využívat kdykoli to jde a nespoléhat se na to že znáš velikosti datových typů. Pro stanovení konstant je vhodné používat definici maker, popřípadě proměnné obsahující patřičnou hodnotu. V žádném případě by hodnoty neměly být přímo vpisovány do kódu. Vyhneš se případným nepříjemnostem v hledání vzniklé chyby a program bude lépe čitelný.
Pokud ukazatel na nic neukazuje, je neplatný, je dobrým zvykem jeho hodnotu vždy nastavovat na NULL. V žádném případě nezapisovat data do paměti která není vyhrazena pro tvé potřeby. Změna velikosti dynamicky alokovaného pole se v C provádí za pomocí funkce realloc(). Pro samotné odřádkování je vhodnější použít funkci putchar() namísto printf().
Nakonec praktická ukázka. Zde je správný způsob, jak dynamicky alokovat pole, naplnit ho daty a uvolnit alokovanou paměť.
#include <stdio.h>
#include <stdlib.h>
#define N 3
int main(void) {
int *arr = NULL;
arr = (int*)malloc(N * sizeof(int));
if (!arr) {
fprintf(stderr, "Chyba alokace pameti.\n");
exit(1);
}
for (int i = 0; i < N; i++) {
*(arr + i) = i;
printf("%d ", *(arr + i));
}
putchar('\n');
free((void*)arr);
arr = NULL;
return 0;
}
Někdy může být výhodnější alokovat velkou pamět rovnou, než ji postupně zvětšovat. Např. pro načtení celého souboru nejednou do paměti (při jednom čtení, ReadFile(), lze načíst max. 2 501 566 399 bytů). Příklad alokace 5 GB RAM pro práci s daty přímo v ní.
//* Alokujeme pamět pro celý soubor *//
static const SIZE_T giga = 1024 * 1024 * 1024;
static const SIZE_T size = 5 * giga;
ptr = static_cast<BYTE*>(VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE));
bsrc = ptr;
kde
BYTE* ptr = NULL;
BYTE* bsrc = NULL;
Samozřejmě je ještě nutné otestovat, zda se paměť skutečně podařilo alokovat a před ukončením programu ji pak uvolnit.
VirtualFree(bsrc, 0, MEM_RELEASE);
DWORD errkod = GetLastError();
S daty v RAM lze pak pracovat např. takto:
bodu = *(DWORD *)(bsrc + delka + 4 * x + 4 + k * 4);
Např. pro načtení celého souboru nejednou do paměti (při jednom čtení, ReadFile(), lze načíst max. 2 501 566 399 bytů). Příklad alokace 5 GB RAM pro práci s daty přímo v ní.
Pokud se jedná o práci s velkým souborem, možná by bylo lepší jej do paměti namapovat (CreateFile + CreateFileMapping + MapViewOfFile). Pak jej není potřeba explicitně číst.
To máš pravdu, ale při namapování souboru z něj systém stejně musí data nějak po kouscích načítat a určitě je nečte po 2,5 GB. Pokud jde o maximální rychlost načtení a zpracování dat, rychleji než nečíst všechna data do RAM po 2,5 GB a tam je zpracovat, to jiným způsobem nepůjde.
To máš pravdu, ale při namapování souboru z něj systém stejně musí data nějak po kouscích načítat a určitě je nečte po 2,5 GB. Pokud jde o maximální rychlost načtení a zpracování dat, rychleji než nečíst všechna data do RAM po 2,5 GB a tam je zpracovat, to jiným způsobem nepůjde.
Myslím, že systém (nebo některý ovladač na cestě) bude takto velký požadavek rozdělovat na menší (2-4 MB). Nezdá se mi, že by byl disk schopný sám provést takové čtení jako jednu operaci.
Navíc, pokud pro začátek zpracování stačí jenom menší množství dat, je možné provádět čtení asynchronně a mezitím, co se data donačítávají, zpracovávat ta, co již jsou v paměti. Paměťově mapované soubory v tomhle trošku pomáhají, protože data do paměti načítá systém a pro určité scénáře (jako např. sekvenční čtení) se dokáže chovat chytře. V zásadě ale data načítá až v případě potřeby (+ ukládání do cache), takže v případě, že nakonec zjistíš, že třeba všechna data nepotřebuješ, se ta nepotřebná do paměti vůbec nemusí podívat.
Čtení po 2,5 GB není žádný problém, zvlášť z rychlého SSD disku. Načtení 5G je otázka cca dvou sekund.
Příklad jak na to:
const DWORD cti_blok = 2501566399;
delka = FileSize(fname); // 64 bitů
//* Zkusíme otevřít soubor pro čtení *//
HANDLE handle = CreateFile(fname, GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
// if (handle == INVALID_HANDLE_VALUE)
// report_error("Unable to open input file!\n");
a vlastní čtení:
//* Načteme soubor *//
DWORD bytes_read;
cti_znaku = cti_blok;
do {
BOOL e = ReadFile(handle, ptr, cti_znaku, &bytes_read, nullptr);
nacteno += bytes_read;
ptr += bytes_read;
if (delka - nacteno < cti_blok) {
cti_znaku = delka - nacteno;
}
} while (nacteno < delka);
CloseHandle(handle);
Čtení po 2,5 GB není žádný problém, zvlášť z rychlého SSD disku. Načtení 5G je otázka cca dvou sekund.
Pokud takový disk máš, tak samozřejmě ano .
Zobrazeno 10 zpráv z 10.