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í, Následující aplikace, Jednička, Tito, Potřebné povolení, Nečekané použití, Přetečení, Byte, Chybné chování, Měření, DEL, Pole, Nepříjemná vlastnost, Nesmyslný údaj, Jednoduchý typ, Jasný důvod, Klíčový okamžik, Předchozí měření, Datový vstup, Překladače, Jednoduchá pozice, Toto, Měření času, Software na Heureka.cz



Sex manželských párů? Jen výjimečně. Ložnice ovládnou roboti s umělou inteligencí

Sex manželských párů? Jen výjimečně. Ložnice ovládnou roboti s umělou inteligencí

** Sex manželských párů jen při zvláštních příležitostech. ** Ložnice ovládnou sexuální roboti s umělou inteligencí. ** I to je jeden ze závěrů Mezinárodní robotické konference.

Filip KůželJiří Liebreich
RobotiSexUmělá inteligence
Druhotné softwarové licence. Kdo by nechtěl legální Windows za stovku…

Druhotné softwarové licence. Kdo by nechtěl legální Windows za stovku…

Koupě originálního programového vybavení přímo od výrobce není jedinou možností, jak sehnat pro váš počítač legální software. Další možností je koupit licenci od toho, kdo ji už nepotřebuje nebo nevyužije. Můžete ušetřit nemalou částku, zároveň si však dávejte pozor na podvodníky.

Jan Spěšný
Nový hit. Tahle appka vám udělá profilovku jako od pouličního ilustrátora

Nový hit. Tahle appka vám udělá profilovku jako od pouličního ilustrátora

** Aplikace NewProfilePic se na Androidu stala hitem ** Můžete si v ní vytvořit profesionálně vypadající profilovky ** Pozor ale na agresivní cenovou politiku za Pro verzi

Martin Chroust
FotografieUmělá inteligenceMobilní aplikace
25 nejlepších filmových parodií, které můžete vidět. Víme, kde je najdete online

25 nejlepších filmových parodií, které můžete vidět. Víme, kde je najdete online

Filmové parodie jsou divácky velmi vděčné a mezi filmaři oblíbené. Tvůrci v nich mohou totiž zcela beztrestně vykrádat cizí díla a v jejich nápodobě popustit uzdu své fantazii. Vybrali jsme nejlepší zahraniční i české parodie.

Marek Čech
Filmy, které musíte vidět
18 tipů a triků pro WhatsApp, které možná neznáte

18 tipů a triků pro WhatsApp, které možná neznáte

** WhatsApp je jedna z nejrozšířenějších komunikačních aplikací ** Obsahuje mnoho skrytých funkcí, které vylepší používání ** Zde najdete tipy na ty nejužitečnější

Adam Kos
WhatsAppTipy a triky
Nejlepší aplikace pro tento týden: Lenivé kosti, topografické mapy a preventivka

Nejlepší aplikace pro tento týden: Lenivé kosti, topografické mapy a preventivka

**V marketech najdete množství titulů, ale jak objevit ty kvalitní? **Každý týden je vybírá Apple, ale řada z nich má i verzi pro Android. **Výsledkem jsou kolekce těch nejlepších aplikací svého druhu.

Adam Kos
Nejlepší aplikace
Světu hrozí „vlhká žárovka.“ Měla dorazit až v polovině století, ale už je tu

Světu hrozí „vlhká žárovka.“ Měla dorazit až v polovině století, ale už je tu

** Nejteplejší místo na Zemi nemusí být to nejnebezpečnější ** Největším zabijákem se stane až mix tepla a vlhkosti ** Asie se nebezpečně přiblížila hraniční hodnotě 35 °C twb

Jakub Čížek
MeteorologieGlobální oteplování
Google na obchodu Play pustil do oběhu malware obřích rozměrů. Dostal se do více než 60 milionů smartphonů

Google na obchodu Play pustil do oběhu malware obřích rozměrů. Dostal se do více než 60 milionů smartphonů

** Doslova každý týden se na Google Play objeví nový malware ** Ten nejnovější si do mobilů stáhlo na 60 milionů uživatelů ** Vývojáři aplikací vydělávají na přeprodeji citlivých dat

Martin Chroust
Google PlayMalwareAndroid
Jak promítnout displej telefonu na počítač s Windows 10

Jak promítnout displej telefonu na počítač s Windows 10

Chcete jednoduše ukázat známým fotky z dovolené a displej vašeho telefonu vám přijde malý? Promítněte si jej na obrazovku počítače, bez nutnosti kopírování nebo připojení přes kabel.

Jan Spěšný
SmartphoneWindows 10Android
Nandali jste to Putinovi? Rusko zveřejnilo IP adresy, které se zapojily do obřího DDoS. Možná je tam i ta vaše

Nandali jste to Putinovi? Rusko zveřejnilo IP adresy, které se zapojily do obřího DDoS. Možná je tam i ta vaše

** Zapojte se do útoků, lákají lidé na Twitteru ** Mnohé weby Moskvy skutečně nejedou ani v Rusku ** Pozor na podvodníky, toto je příležitost pro phishing

Jakub Čížek
KyberválkaRusko