Umíme to s Delphi: 117. díl – pozor na přetečení!

Dnešní článek je zaměřen na nastavování některých dalších voleb překladače. Hlavní náplň článku pak tvoří popis problematiky, která nám může způsobit mnoho bezesných nocí a obtížných chvil: na přetečení. Vysvětlíme si, o co jde, čím je způsobeno a jak jej snadno odhalit.

V minulém článku jsem se zabývali optimalizací programů, kterou provádí integrované prostředí Delphi. Podrobně jsme si vysvětlili (a na praktických příkladech a ukázkách demonstrovali), jakým způsobem je optimalizace prováděna, co optimalizátor udělá a co ne. Rozborem vygenerovaných instrukcí jsme se přesvědčili, že optimalizace probíhá takřka výhradně na úrovni proměnných, to znamená, že optimalizátor vždy používá a zpracovává pouze ty proměnné, u nichž existuje nebezpečí, že by jejich hodnota mohla být v nejbližší době potřeba.

Dnes se dostaneme k popisu některých dalších možností, voleb a nastavení překladače a překladu.

Nastavení překladače

Pokud máte v Delphi otevřený nějaký projekt a zvolíte Project – Options, otevře se známý dialog, v jehož záložce Compiler budeme dnes vypínat a zapínat jednotlivé položky a tím modifikovat vlastnosti výsledného zdrojového kódu. V minulých dílech jsme se zabývali optimalizací, přesněji řečeno zatrhávacím polem Optimization, které je k dispozici hned v první skupině zatrhávacích polí – ve skupině Code generation. Pojďme se podívat na další položky v této skupině.

Ještě předtím však připomeňme, že každé z těchto zatrhávacích polí odpovídá nějaké direktivě překladače. Co to znamená? Pokud se nám nechce zatrhávat v projektu pole v tomto dialogu, můžeme vše potřebné nastavit i programově – ve zdrojovém kódu. Pokud se rozhodneme pro tuto možnost, stačí si zapamatovat jednoduché pravidlo: v případě, že chceme některou možnost zapnout (což by odpovídalo zatržení příslušného zatrhávacího pole v dialogu Project Options), použijeme direktivu {$X+}, kde X je písmenko odpovídající příslušnému zatrhávacímu poli (v následujícím textu bude vždy pro každé zatrhávací pole uvedeno). Naopak chceme-li příslušnou možnost deaktivovat, použijeme direktivu {$X-}, kde X je opět odpovídající písmenko.

Skupina Code generation

Zatrhávací pole Aligned Recorded Fields (odpovídá direktivě {$A}) Dalším políčkem, které můžeme v této skupině zapínat nebo vypínat, je Aligned Recorded Fields. Zapnutí tohoto parametru bude znamenat, že použité struktury se budou v paměti zarovnávat jako pole a záznamy do 32-bitových bloků. Důsledkem by mělo být částečné zlepšení šetření pamětí.

Zatrhávací pole Stack Frames (odpovídá direktivě {$W})

Tato volba slouží k ovlivnění způsobu, jakým překladač bude ukládat adresy funkcí a procedur do zásobníku.

Zatrhávací pole Pentium-safe FDIV (odpovídá direktivě {$U})

Pokud zapnete tuto položku, bude překladače generovat kód odolný vůči chybě způsobované při dělení v pohyblivé řádové čárce na některých ranných verzích procesoru Pentium.

Skupina Runtime errors

Tím jsme „vyřídili“ první skupinu přepínačů – Code generation. V další skupině (Runtime errors) můžeme ovlivňovat, jaké chyby se mohou objevit za běhu aplikace a také nastavovat způsoby reakce na tytí chyby.

Zatrhávací pole Range checking (odpovídá direktivě {$R})

U této volby se zastavíme déle. Pokud ji zapneme, bude za běhu aplikace kontrolováno, zda indexy v polích a v řetězcích spadají do stanovených (povolených) mezí. Kromě toho je hlídáno i překročení rozsahu proměnných apod. Nastane-li kterýkoliv z uvedených jevů (překročení rozsahu proměnné, překročení indexu pole apod.), hlásí program chybu.

Dodejme, že testování rozsahů proměnných způsobuje zpomalení běhu programu. Z toho důvodu je vždy nutné pečlivě zvážit, zda potřebujeme, aby toto testování bylo zapnuté či zda se bez něho dokážeme obejít (a v kritických místech případně otestovat potřebné hodnoty sami, „ručně“).

Ukážeme si jednoduchý příklad. Vytvořte novou aplikaci, na formulář umístěte komponenty ListBox a Button a do obsluhy události OnClick tlačítka Button zapište jednoduchý cyklus for, viz následující zdrojový kód:

procedure TForm1.Button1Click(Sender: TObject);
var
  I: Integer;
  J: Byte;

begin
  J := 0;

  for I := 1 to 260 do begin
    J := I;
    ListBox1.Items.Add(`I = ` + IntToStr(I) + `, J = ` + IntToStr(J));
    ListBox1.TopIndex := I - round(ListBox1.Height/ListBox1.ItemHeight);
  end;
end;

Co tento kód dělá? Jednoduše provede 260 průchodů cyklem, a v každém z nich vypíše do seznamu ListBox hodnoty proměnnéch I a J. Proměnná I je celočíselná a je typu Integer, proměnná J je také celočíselná, ale pouze typu Byte, což je typ zabírající v paměti pouze jeden bajt a umožňující tak uložit (uchovat) pouze čísla z rozsahu 0 až 255.

V poslední řádce v těle cyklu pouze zařídíme, aby se v seznamu ListBox nezobrazovaly pouze první položky, nýbrž aby se obsah seznamu posouval podle toho, jak se nové položky přidávají na jeho konec. Použijeme k tomu vlastnosti seznamu Height (výška seznamu v pixelech), ItemHeight (výška jedné položky v pixelech) a TopIndex (nastavení indexu položky, která bude v seznamu zobrazena „nahoře“ – jako první.

Pokud ponecháme standardní nastavení projektu (v jehož rámci je přepínač Range checking vypnutý), proběhne cyklus bez chyb a nakonec bude zobrazovat následující hodnoty proměnných (viz obrázek):

Klepněte pro větší obrázek

Jinak řečeno: proměnná I má hodnotu 260 (což je logické, neb tak zněla zastavovací podmínka cyklu for) a proměnná J má hodnotu 4. To už tak logické není, protože jsme do ní pořád přiřazovali stejné číslo, jako je v proměnné I.

Pokud vám není jasný důvod, stručně si jej vysvětlíme. Řekli jsme si, že rozsah proměnné typu Byte je jeden bajt (což je osm bitů). Aplikace tedy má pro proměnnou typu Byte k dispozici osm bitů, do nichž může ukládat hodnotu této proměnné. Všechny hodnoty, které do proměnné zapíšeme (a obecně samozřejmě do jakékoliv proměnné), se ukládají v dvojkové soustavě, tedy posloupností nul a jedniček. Nastavíme-li do této proměnné hodnotu 0, bude její dvojkové vyjádření 0, a pokud máme k dispozici osm bitů, bude vyjádření hodnoty 0 následující:

00000000

Jakmile začneme do proměnné ukládat nějaké větší údaje, budou vždy převedeny do dvojkové soustavy a uloženy jako posloupnost nul a jedniček. Takže přiřadíme-li do této proměnné hodnotu 100 (jejíž dvojkové – binární – vyjádření vypadá takto: 1100100), bude paměťový obraz proměnné vypadat takto:

01100100

Údaje v proměnné typu Byte budou zkrátka vždy uloženy na 8 bitech s tím, že pokud vyjádření čísla nevyužije celých 8 pozic (což platí pro všechny hodnoty menší než 128), budou na úvodních (tzv. nevýznamných) pozicích zapsány nuly. Při zpracovávání proměnné počítač přečte zprava osm bitů a „poskládá“ z nich hodnotu proměnné. Jak prosté, že?

Tímto způsobem se v paměti uchovávají hodnoty proměnné typu Byte. Pojďme se podívat, jak to vypadá s vyššími hodnotami. Nejprve dejme tomu, že do proměnné typu Byte přiřazujeme hodnotu 254. Její binární hodnota je 11111110 a stejně také bude vypadat paměťový obraz této hodnoty. Pokud k tomuto číslu přičteme 1, vznikne číslo 255, jehož binární vyjádření vypadá takto: 11111111 (tedy osm jedniček).

Nyní nastává klíčový okamžik pro pochopení jevu zvaného „přetečení“. Rozhodneme-li se k této hodnotě přičíst opět jedničku (což jsme učinili v předchozím příkladu v dvoustémpadesátémpátém průchodu cyklem), vznikne číslo 256. Pojďme se podívat na binární vyjádření. Zopakujme že hodnota 255 má vyjádření

11111111

Přičteme-li zmíněnou jedničku, dostaneme číslo 256, jehož binární vyjádření vypadá takto:

100000000

Všimněte si prosím, že toto číslo zabírá devět pozic, a k jeho uložení by tedy byla potřeba proměnná disponující přinejmenším devíti bity operační paměti (devítibitový takový datový však samozřejmě neexistuje, takže bychom museli použít některý vyšší typ, například SmallInt, který zabírá dva bajty a ukládá tedy na 16 bitů).

Co se tedy stane? Proměnná by měla obsahovat číslo 100000000, ale má k dispozici pouze osm bitů. Obecně navíc platí, že při zkoumání paměťových údajů (a vůbec při práci s nimi) se postupuje od nultého bitu (od bitu, který je v údaji „nejvíc napravo“). Takže řešení je prosté: počítač ví, že proměnná zabírá osm bitů a při práci s proměnnou tedy postupuje tak, že vezme zprava osm bitů a ty prohlásí za hodnotu dané proměnné. V našem případě tedy vezme zprava osm nul (což je samozřejmě binárním vyjádřením nuly) a vrátí je jako hodnotu proměnné. Přetečení je na světě: namísto hodnoty 256 jsme získali nulu.

Pokud bychom tedy vzali jakýkoliv (zde celočíselný) datový typ, definovali jeho proměnnou a v nekonečném cyklu do ní pořád dokola přičítali jedničku, bude hodnota proměnné pořád dokola růst od minimální k maximální zobrazitelné hodnotě; jakmile dosáhneme maxima, vrátíme se zpět na minimum a rosteme znovu.

V důsledku tohoto jevu také mohou vznikat velmi nepříjemné chyby: pokud si přetečení nejsme vědomi a pokud si včas nevšimneme, že k němu došlo, máme zaděláno na potíže. Aplikace běží bez jakýchkoliv problémů (bez výjimek, bez chyb apod.), ale výsledky jsou špatné. A protože bezpochyby spolehlivě zapracuje známý a osvědčený Murphy, budou výsledky špatné jen někdy, a to typicky poprvé v okamžiku, kdy začnete hotový produkt předvádět zákazníkovi (/učiteli).

Aplikace totiž s většinou datových vstupů bude pracovat správně, ale jednou za čas (při obzvláště vypečených vstupech) někde v jejím vnitřku nějaký údaj přeteče, takže následné výpočty budou probíhat už s úplně nesmyslnými údaji. Jistě si dovedete představit, že odhalení místa, v němž k takové chybě došlo, může být skutečnou a nefalšovanou noční můrou.

Standardní chování aplikací (tj. takové, při němž není zapnuto Range checking) je takové, že aplikaci „je jedno“, že v jejím vnitřku došlo k přetečení. Důsledky jsme si vysvětlili v předchozím odstavci. Nicméně díky této volbě je také možno zapnout hlídání rozsahů, takže při přetečení bude hlášena chyba.

Přímo se nabízí, jak toho využít: ve fázi ladění (případně v okamžiku, kdy si všimneme nějaké zvláštní, nečekané a nejednoznačné chyby) tuto volbu zapneme; pak máme jistotu, že v okamžiku přetečení dostaneme jednoznačnou informaci o tom, že k němu došlo (a provádění programu bude také zastaveno v místě, kde k němu došlo).

Toto řešení je jistě pohodlnější , než kdybychom museli sledovat a hlídat hodnoty všech proměnných pomocí nějakých Watches a dalších ladicích nástrojů.

Takže pokud zapnete hlídání rozsahu proměnných (Range checking, případně pomocí direktivy {$R}), bude se program rázem chovat jinak. Po klepnutí na tlačítko Start na formuláři dojde k tomu, že proběhne prvních 255 průběhů cyklu, a pak dojde k ohlášení chyby vzniklé díky přetečení:

Klepněte pro větší obrázek

Výhodou zapnutého hlídání rozsahů je tedy skutečnost, že se okamžitě dozvíme, že došlo k přetečení a dozvíme se také, kde k němu došlo. Na druhou stranu jsme si uvedli i jednu nevýhodu tohoto přístupu – běh programu se zpomalí, protože testování mezí zabírá strojový čas a prostředky.

Abych ale nemluvil jen teoreticky, provedl jsem opět několik měření. Upravil jsem trochu zdrojový kód tak, aby zbytečně nezabíraly čas operace související s grafickým překreslováním seznamu a vytvořil jsem ještě jednu proceduru, v jejímž rámci je tisíckrát spuštěn předchozí cyklus. Také jsem upravil meze cyklu: namísto 1 – 260 jsem zvolil pouze 1 – 200, aby vykonávání aplikace nebylo pořád přerušováno chybovými hlášeními o přetečení.

Následně jsem aplikaci třikrát spustil s vypnutou kontrolou mezí a třikrát se zapnutou kontrolou mezí. Abych předešel výtkám v diskusi, předesílám, že si uvědomuji, že šest měření není jistě dostatečné množství pro vynesení statisticky korektních výsledků; na druhou stranu věřím, že pro naše účely a pro otestování, zda dojde ke zpomalení a o kolik, tento přístup postačuje.

Takže jak dopadly výsledky? Shrnuje je následující tabulka:

Range checking zapnuto Range Checking vypnuto
Měření číslo 1 [s] 92.37
Měření číslo 2 [s] 92.342
Měření číslo 3 [s] 92.407
Měření číslo 4 [s] 93.314
Měření číslo 5 [s] 86.071
Měření číslo 6 [s] 85.734
Průměr [s] 92,373 88,373

Jinak řečeno: zapnuté hlídání rozsahů zpomalilo vykonávání aplikace průměrně o 4 vteřiny, což je zhruba o 4,5 %. Uvedu raději hned i všechny zbývající interpretace, vyberte si z nich prosím tu, která je vašemu programátorskému srdci nejbližší :-) Pokud zapneme hlídání přetečení, poběží aplikace průměrně o 4,5 % pomaleji; naopak pokud hlídání přetečení vypneme, poběží aplikace průměrně o 4,3 % pomaleji. Věřím, že rozumíte, čím je způsoben udánlivý nesoulad mezi těmito dvěma tvrzeními.

Ale závěr je zřejmě jednoznačný – hlídání přetečení nám může usnadnit práci, ale zároveň způsobí mírnou ztrátu výkonu.

Proto se občas doporučuje jednoduché pravidlo – ve fázích ladění je vhodné volbu zapnout a pro šíření (pro finální distribuci) je lepší ji vypnout s tím, že když by nedejbože zákazníci hlásili chybné chování, Range checking si doma opět zapneme a vrhneme se na další testy.

Zdrojový kód

Závěrem uvedu celý zdrojový kód aplikace, která byla použita k měření rychlosti:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.Button1Click(Sender: TObject);
var
  I: Integer;
  J: Byte;

begin
  Self.Caption := `Range checking`;
  Button1.Caption := `Start`;
  ListBox1.Clear;

  J := 0;

  for I := 1 to 200 do begin
    J := I;
    ListBox1.Items.Add(`I = ` + IntToStr(I) + `, J = ` + IntToStr(J));
  end;
end;


procedure TForm1.Button2Click(Sender: TObject);
var
  X, Y: Integer;
  I: Integer;

begin
  X := GetTickCount;

  for I := 0 to 1000 do begin
    Button1Click(Self);
  end;

  Y := GetTickCount;
  ShowMessage(`Doba výpočtu: ` + FloatToStr((Y - X) / 1000) + ` vteřin.`);
end;

end.

Závěrem

Dnešní díl byl zaměřen na popis některých dalších voleb překladače s tím, že jsme se zaměřili především na hlídání rozsahu proměnných a zabývali jsme se problematikou přetečení. Příště budeme pokračovat dalšími volbami a nastaveními.

Diskuze (4) Další článek: NPD Group: Stahování znovu na vzestupu

Témata článku: Software, Windows, Programování, Pole, Nesmyslný údaj, Jasný důvod, Jednoduchá pozice, Překladače, Díl, Následující aplikace, Jednoduchý typ, Přetečení, DEL, Měření času, Předchozí měření, Toto, Tito, Jednička, Nečekané použití, Klíčový okamžik, Byte, Nepříjemná vlastnost, Potřebné povolení, Měření, Datový vstup


Určitě si přečtěte

Jak v prohlížeči vypnout oznámení zasílaná webovými stránkami

Jak v prohlížeči vypnout oznámení zasílaná webovými stránkami

** Obtěžují vás neustálé dotazy webů, zda chcete zobrazovat oznámení? ** Můžete je zakázat, a to jak kompletně, tak i pro jednotlivé stránky ** Připravili jsme návody pro Chrome, Firefox, Edge a Operu

Karel Kilián | 11

Šmírování kamerami Googlu: Koukněte, co šíleného se objevilo na Street View

Šmírování kamerami Googlu: Koukněte, co šíleného se objevilo na Street View

Google stále fotí celý svět do své služby Street View. A novodobou zábavou je hledat v mapách Googlu vtipné záběry. Podívejte se na výběr nejlepších!

redakce | 4

10 míst na mapách Googlu, která nesmíte vidět. Nahradily je čtverečky

10 míst na mapách Googlu, která nesmíte vidět. Nahradily je čtverečky

** Deset míst, které nesmíte vidět ve webových mapách ** Jsou to letiště, základny i elektrárny ** Nejvíce míst tají Francie

Jakub Čížek | 21


Aktuální číslo časopisu Computer

Velký test fitness náramků

Levné záložní zdroje

Jak si zabezpečit domov

Nejlepší monitory na trhu