Optymalizacja kodu

Adam Boduch

Początkujący programiści często umieszczają w swoich kodach wiele niepotrzebnych instrukcji. Dzięki temu kod staje się mniej przejrzysty, bardziej zagmatwany i przede wszystkim ? wykonuje się wolniej. Procesor musi wówczas przetworzyć więcej instrukcji do wykonania tego samego zadania. Należy się w tym momencie zastanowić, czy nie dałoby się danego problemu rozwiązać prościej. Często takie pułapki wynikają po prostu z niewiedzy początkujących programistów. W tym artykule znajdziesz kilka praktycznych wskazówek jak zwiększyć wydajność swojej aplikacji oraz większyć przejrzystość kodu.

1 Kilka wskazówek
2 Język Delphi
     2.1 Virtual czy Dynamic ?
     2.2 Instrukcje warunkowe
     2.3 Typ Boolean w tablicach
     2.4 Zbiory
     2.5 Łańcuchy
          2.5.1 Łączenie znaków w ciągach
          2.5.2 Przekazywanie parametrów przez wartość
          2.5.3 Łączenie łańcuchów
     2.6 Zmienne lokalne
     2.7 Procedury zagnieżdżone
     2.8 Liczby całkowite
     2.9 Dynamiczne tworzenie form
3 VCL
     3.10 Dodawanie wielu elementów do list

Optymalizacja to czynność polegająca ogólnie na przyspieszeniu działania aplikacji lub zmniejszenia ilości pamięci potrzebnej do jej działania. Istnieją pewne granice optymalizacji, których nie jesteś w stanie ominąć. Program wykonujący skomplikowane operacje na bazie danych zawierającej setki tysięcy rekordów nie będzie działał szybciej (mimo dobrej optymalizacji) dopóki sprzęt na którym działa nie będzie lepszy lub dopóki wymagane będą aż tak skomplikowane operacje.

Kilka wskazówek

W programie zawsze włączaj optymalizacje kompilatora (dyrektywa {$O+}), co pozwoli na jak największą optymalizacje kodu. Niekiedy może to spowodować problemy z debugowaniem aplikacji tak więc na czas procesu testowania oraz kodowania, możesz wyłączyć tę dyrektywę ({$O-}) i włączyć ją jedynie w momencie kompilowania finalnej wersji.

Możesz zdecydować się na rozpowszechnianie aplikacji bez dodatkowych pakietów. Wówczas, należy z menu Project wybrać pozycję Options, a nastepnie kliknąć zakładkę Package i zaznaczyć opcję Build with runtime package. Wtedy rozmiar aplikacji będzie mniejszy, program będzie działał szybciej jeżeli masz kilka aplikacji wykorzystujących te same pakiety. Jednak minusem jest to, iż w momencie rozpowszechniania aplikacji, na komputerze docelowym muszą znaleźć się biblioteki wymagane przez aplikację.

Jeżeli wykorzysujesz w aplikacji kilka formularzy, wywołuj je w sposób dynamiczny. Domyślnie wszystkie formularze tworzone są w momencie uruchamiania aplikacji co zajmuje więcej miejsca w pamięci komputera. Możesz to zmienić klikając na menu Project ? Options ? Forms. Na zakładce Forms, należy usunąć formularze z listy Auto-create forms.

Rezygnacja z niektórych modułów Delphi może wpłynąc na mniejszy rozmiar aplikacji wykonywalnej i mniejsze wymagania co do pamięci RAM. Przykładowo, moduł SysUtils zwiększa rozmiar aplikacji wykonywalnej o kilkaset kB. Jeżeli chcesz wykorzystać zaledwie kilka funkcji z tego modułu i masz dostęp do kodów źródłowych biblioteki VCL, skopiuj do programu jedynie funkcje które chcesz wykorzystać (zamiast włączania całego modułu).

Zanim zdecydujesz się na pisanie własnej funkcji upewnij się czy nie została już napisana przez kogoś innego; tj. korzystaj z funkcji WinAPI zamiast pisać własne. Upewnij się iż potrzebna Ci funkcja nie znajduje się w bibliotece VCL.

Jeżeli w zasobach Twojej aplikacji znajduje się wiele obrazków, np. pod postacią bitmap, upewnij się że nie posiadają one zbyt dużych rozmiarów. Zastanów się czy nie powinieneś skompresować takich obrazów do postaci pliku JPEG.

Język Delphi

W większości przypadków spowolnienie działania aplikacji wynika z blędnej implementacji algrorytmu. W takich przypadkach na kod aplikacji trzeba spojrzeć indywidualnie. Poniższe wskazówki można traktować bardziej jako ciekawostkę, lecz z pewnością pozwolą Ci pisać lepsze programy.

W większości przypadków kompilator Delphi optymalizuje kod możliwe jak najbardziej, tak, aby program był wydajny. Jednak najlepszy nawet kompilator nie będzie w stanie "obronić" nas przed ewidentnymi błędami programistów.

Virtual czy Dynamic ?

Wykorzystując przedefiniowane metody masz wybór. Możesz skorzystać z dyrektywy Virtual i uczynić metodę wirtualną lub dynamiczną (dyrektywa Dynamic). Informacje na temat przedefiniowanych metod znajdują się w tablicy VMT, dzięki użyciu klauzuli Virtual, wywołanie metody będzie szybsze. Użycie klauzuli Dynamic powoduje, iż metody będą wykonywane wolniej, lecz będą również wykorzystywały mniej pamięci.

Instrukcje warunkowe

Częstym niedociągnięciem (nie można nazwać tego błędem) jest sprawdzanie w warunku If, czy zmienna typu Boolean ma wartość True:

if X = True then { kod } 

Taki zapis nie jest konieczny, gdyż domyślnie warunek w tym przypadku sprawdza, czy zmienna nie ma wartości True. Można więc taki kod zastąpić następującym:

if X then { kod }

Zwiększana jest tym samym czytelność kodu.

Spójrzmy natomiast na poniższy kod:

if X = True then
  Y := True
else
  Y := False;

Czytelnikowi taki fragment kodu może wydać się śmieszny, ale niestety wielu początkujących programistów stosuje taki niepotrzebny zapis, który spowalnia wykonywanie programu. Procesor musi bowiem wykonać instrukcję porównującą zawartość danej zmiennej i dopiero później przypisać nową wartość do zmiennej. Taki zapis, choć z punktu widzenia kompilatora jest poprawny, może zostać uproszczony do takiej postaci:

Y := X;

Sytuacja odwrotna:

if X = True then
  Y := False
else
  Y := True;

Taki z kolei zapis można skrócić do jednej linii kodu, używając operatora Not:

Y := not X;

W obu przypadkach zmienne muszą być oczywiście typu Boolean.

Wyobraźmy sobie program, w którym użytkownik musi podać hasło dostępu (wpisać je do kontrolki Edit) i nacisnąć przycisk TButton, aby się zalogować. Chcemy jednak, aby przycisk pozostał nieaktywny (właściwość Enabled), dopóki użytkownik nie wpisze w kontrolce choćby jednego znaku. Oto przykładowy kod z użyciem instrukcji If:

var
  Name: String;
  Accept: Boolean;

begin
  Accept := False;
  Writeln('Podaj imię');
  Readln(Name);  // odczyt wartości

  if Length(Name) = 0 then // sprawdzenie długości ciągu
    Accept := False
  else
    Accept := True;
end.

Funkcja Length w tym przykładzie sprawdza, czy rzeczywiście długość wprowadzonego tekstu równa się 0. Taki kod można skrócić do takiej postaci:

var
  Name: String;
  Accept: Boolean;

begin
  Accept:= False;
  Writeln('Podaj imię');
  Readln(Name);  // odczyt wartości

  Accept := (Length(Name) <> 0);
end.

Taki zapis może wydawać się niezrozumiały. Warto jednak wiedzieć, że Delphi daje takie możliwości. Zmienna Accept zmieni wartość na True, jeśli długość zmiennej Name będzie różna od 0. W ten sposób osiągamy taki sam rezultat jak w poprzednim przykładzie.

Jeżeli w instrukcji warunkowej znajduje się kilka warunków logicznych, może warto zamienić te warunki miejscami. Na początku umieść warunek, którego prawdopodobieństwo wystąpienia jest większe. W przypadku, gdy posiadasz wiele warunków do sprawdzenia, być może korzystniejsze będzie rozbicie takiej jednej instrukcji warunkowej na kilka mniejszych.

Typ Boolean w tablicach

Według wielu opinii programowanie jest sztuką, umiejętnym tworzeniem kodu. W Delphi co prawda nie mamy tylu możliwości prezentowania ?sztuczek? co w języku C/C++, lecz warto skorzystać z paru ułatwień. Oto przykład kodu nawiązującego do poprzedniego listingu:

program Foo;

{$APPTYPE CONSOLE}

var
  Name: String;
  Accept: Boolean;

begin
  Accept := False;
  Writeln('Podaj imię');
  Readln(Name);  // odczyt wartości

  if Length(Name) = 0 then
  begin // sprawdzenie długości ciągu
    Accept := False;
    Writeln('Imię jest za krótkie!');
  end else
  begin
    Accept := True;
    Writeln('Imię ma odpowiednią długość!');
  end;

  Readln;
end.

Oprócz zmiany wartości zmiennej Accept na ekranie konsoli jest wyświetlany tekst informacyjny. Korzystając z tablic, można skrócić ten kod do kilku linijek. Przede wszystkim należy zadeklarować tablicę dwuelementową:

const
  Msg : array[Boolean] of String = ('Imię jest za krótkie!', 'Imię ma odpowiednią długość!');

Konstrukcja tej tablicy jest specyficzna, bo zamiast zakresu podałem typ Boolean #_. Oznacza to, że tablica może mieć dwa elementy, True lub False, tak więc odwołanie się do danego elementu wygląda następująco:

Writeln(Msg[True]);

Łatwo już się domyślić, jak będzie wyglądał zapis naszego przykładu w zmienionej formie:

program Foo;

{$APPTYPE CONSOLE}

var
  Name : String;
  Accept : Boolean;

const
  Msg : array [Boolean] of String = ('Imię jest za krótkie!', 'Imię ma odpowiednią długość!');

begin
  Accept := False;
  Writeln('Podaj imię');
  Readln(Name);  // odczyt wartości

  Accept := (Length(Name) <> 0);
  Writeln(Msg[Accept]);

  Readln;
end.

Dzięki temu skróciliśmy zapis kodu, zwiększyliśmy jego czytelność oraz przyspieszyliśmy działanie programu.

Zbiory

Zastosowanie zbiorów często upraszcza zapis związany z instrukcjami warunkowymi. Przykładowo, trzeba sprawdzić, czy wartość zawarta w zmiennej jest liczbą czy ciągiem znakowym. Można także sprawdzić, czy dany ciąg zawiera określone znaki:

  if ((Znak = 'A') or (Znak = 'a') or (Znak = 'B')) then
  { kod }

Jeżeli chcemy porównać wiele znaków, to taki zapis jest mało profesjonalny. Właśnie dzięki zbiorom oraz operatorowi In można go zastąpić w ten sposób:

  if Znak in ['A', 'a', 'b', 'B'] then { kod }

Niestety, z użyciem zbiorów i operatora In nie można porównywać całych ciągów, lecz tylko pojedyncze znaki. I tak jednak daje to pewne ułatwienie w porównaniu z zastosowaniem operatora And.

Innym prostym sposobem na sprawdzenie, czy podany znak jest alfanumeryczny, jest zastosowanie poniższego kodu:

  if Key in ['a'..'z', 'A'..'Z', '0'..'9'] then
   { kod }

Warto też pamiętać o możliwości stosowania zakresów w przypadku zbiorów! W takim przypadku następuje sprawdzenie, czy zmienna Key należy do przedziału znaków od a do z lub od A do Z, a także, czy zmienna Key zawiera liczbę.

W celu dodania lub usunięcia elementu ze zbioru, unikaj stosowania operatorów + oraz -. O wiele szybsze działanie zapewnią procedury Include oraz Exclude.

Łańcuchy

Łańcuchy w Delphi są niemalże najczęściej wykorzystywanym typem danych. W tym tekście nie będziemy omawiać typu z zerowym ogranicznikiem (PChar), skupimy się raczej na typie String. Łańcuch danych w rzeczywistości jest tablicą znaków. W Delphi nie musimy skupiać się na długości łancucha oraz ograniczeniach z tego wynikających. Długość łańcucha jest ograniczona teoretycznie zasobami komputera, kompilator zajmuje się przydzielaniem oraz zwalnianiem pamięci.

Co najciekawsze typ danych String zmieniał się z upływem czasu, dlatego też warto wiedzieć jak jego użycie w różnych wersjach kompilatora, wpływa na prędkość działania aplikacji. W Delphi 1 typ String wskazywał na krótki łańcuch ograniczony 255 znakami (ShortString). W Delphi dla Win32, typ String jest urożsamiany z typem AnsiString, natomiast w Delphi dla .NET - z typem WideString.

Typ WideString wskazuje na klasę .NET - System.String.

Każda zmienna typu String przy deklarowaniu jest inicjowana przez kompilator jako pusty ciąg (chyba, że nadajemy domyślną wartość zmiennej), nie ma więc konieczności przypisywania w bloku Begin pustego ciągu do naszej zmiennej.

Łączenie znaków w ciągach

Może nie jest to zbyt pomocna wskazówka, ale pozwala na zaoszczędzenie miejsca w kodzie źródłowym poprzez usunięcie operatora +. Chodzi o łączenie danych w ciągach znaków, a konkretniej ? kodów ASCII poprzedzanych znakiem #. Poniższy kod:

MessageBox(0, 'To jest ciąg znakowy... ' + #13 + #13 + '...druga linia', '', MB_OK);

Może być równie dobrze zastąpiony następującym:

MessageBox(0, 'To jest ciąg znakowy... '#13#13'...druga linia', '', MB_OK);

Oba zapisy mają takie same działanie ? tworzenie podwójnej linii w oknie informacyjnym.

Przekazywanie parametrów przez wartość

Przypomnijmy, iż parametry funkcji/procedury/metody mogą być przekazywane przez wartość (najczęściej wykorzystywana metoda), stałą oraz referencję. Przekazywanie parametrów przez stałą pozwala kompilatorowi na zapewnienie możliwie jak najlepszej optymalizacji kodu. Spójrzmy na dwie funckje poniżej:

function GetLength(S : String) : Integer;
begin
  Result := Length(S);
end;

function GetLength2(const S : String) : Integer;
begin
  Result := Length(S);
end;

Obie zwracają długość łańcucha, z tą różnicą iż parametr funkcj GetLength jest przekazywany przez wartość natomiast GetLength2 - przez stałą. Szybkość działania tych funkcji w pojedyńczym wywołaniu nie robi większej różnicy. Jeżeli skorzystamy z takiej funkcji, powiedzmy - 1 mln. razy wyraźnie widać różnicę prędkości. Pierwszej z nich, wykonanie tego zadania zajmuje ok. 100 milisekund natomiast drugiej - około 15 milisekund. W przypadku kompilatora .NET szykość działania tych dwóch funkcji jest zbliżona.

Przyjrzyjmy się kolejnemu przykładowi. Technika przekazywania parametrów przez stałą sprawdza się w momencie, gdy nie musimy modyfikować oryginalnej wartości łańcucha. Spójrzmy na poniższą procedurę oraz funkcję:

function Add(const S : String) : String;
begin
  Result := S + ' Ala ma kota ';
end;

procedure Add2(var S : String);
begin
  S := S + ' Ala ma kota ';
end;

Pierwsza z nich (funkcja) zwraca wartość łańcucha, powiększoną o tekst Ala ma kota. Procedura natomiast modyfikuje zawartość parametru (parametr przekazywany jest przez referencję).

50 tyś. wywołań pierwszej funkcji, w postaci:

for I := 0 to 50000 do
  Foo := Add(Foo);

zajmuje aplikacji mnóstwo czasu (w teście 114000 milisekund). Natomiast, tyle samo iteracji procedury Add2 trwa około 15 milisekund. Różnice są jak widać znaczne. Podsumowując: jeżeli musisz w swoim programi wielokrotnie manipulować danymi łańcucha, warto pomyśleć o przekazywaniu parametrów przez wartość.

Łączenie łańcuchów

Bardzo istostne zagadnienie, operacja często wykorzystywana w trakcie programowania, a mianowicie - łączenie łańcuchów. Skupmy się na łańuchach String i sposobów na łączenie łańcuchów. Najprostszy, najbardziej przejrzysty, to operator + (dodawanie), który jednocześnie jest najbardziej zalecanym sposobem łączenia łańcuchów. Innym, mniej zalecanym sposobem jest użycie funkcji Concat lub AppendStr, która jest procedurą przestarzałą i niezalecaną do użycia. 500 tyś. iteracji poniższych pętli dało mniej więcej podobne rezultaty czasowe (różnica kilku milisekund, czyli na granicy błędu):

for I := 0 to 500000 do
  Foo := Concat(Foo, ' Ala ma kota ');

for I := 0 to 500000 do
  Foo := Foo + ' Ala ma kota ';

for I := 0 to 500000 do
  AppendStr(Foo, ' Ala ma kota ');

Sytuacja wygląda inaczej na platformie .NET. Działanie operatora + jest niezwykle wolne (procedura AppendStr jest niedostępna). Łączenie łańcuchów powoduje de facto utworzenie nowej zmiennej, do której zostaną przypisane wartości dwóch dodawanych łańcuchów. W wypadku gdy musimy dokonać wielu operacji na łancuchach, korzysniejsze jest skorzystanie z klasy StringBuilder. Spójrz na poniższy listing:

program Foo;

{$APPTYPE CONSOLE}

uses
  Windows,
  SysUtils;

var
  Str : String;
  I : Integer;
  Start, Stop : Integer;
begin
  Str := '';
  Start := GetTickCount;

  for I := 0 to 20000 do
    Str := Str + 'Ala ma kota';

  Stop := GetTickCount;
 
  Writeln('Czas wykonywania: ' + IntToStr(Stop - Start));
  Readln;

end.

Program jest dość prosty, napisany tak, aby mógł zostać skompilowany zarówno w Delphi dla .NEt oraz w Delphi dla Win32. Jego zadaniem jest dodawanie w pętli (20,000 iteracji) napisu Ala ma kota. Delphi 7 poradzi sobie z takim programem w 16 milisekund natomiast Delphi dla .NET potrzebuje na to aż 40 milisekund! Aby przyspieszyć działanie aplikacji, możesz skorzystać z klasy StringBuilder, która znajduje się w przestrzeni System.Text. Użycie tej klasy spowoduje przyspieszenie działania aplikacji do 15 milisekund, czyli do wartości porównywalnej z Delphi dla Win32. Warto o tym wiedzieć, w sytuacjach, gdy Twoja aplikacja silnie korzysta z typu String:

program Foo;

{$APPTYPE CONSOLE}

uses
  Windows,
  System.Text,
  SysUtils;

var
  Str : StringBuilder;
  I : Integer;
  Start, Stop : Integer;
begin
  Str := StringBuilder.Create;

  Start := GetTickCount;

  for I := 0 to 20000 do
    Str.Append('Ala ma kota');

  Stop := GetTickCount;
  Writeln('Czas wykonywania: ' + IntToStr(Stop - Start));
  Readln;

  Str.Free;

end.

W klasie StringBuilder korzystamy z metody Append, która jest po prostu szybsza niż operator +. W przypadku pracy z większą ilością danych, zalecam więc użycie klasy StringBuilder. Wadą takiego rozwiązania jest to, iż taki program nie będzie kompatybilny ze starszymi wersjami Delphi (klasa StringBuilder jest charakterystyczna dla .NET).

Zmienne lokalne

Używaj zmiennych globalnych tylko wówczas jeśli jest to naprawdę konieczne! Dobrym zwyczajem jest dzielenie programu na procedury i funkcje, w których wewnątrz deklarowane są zmienne (tzw. zmienne lokalne). Zmienne globalne są umiesczane na stosie w momencie uruchamiania aplikacji - te znajdujące się wewnątrz procedur/funkcji czy też metod - dopiero w momencie wywolywania tejże. W momencie zakończenia działania procedur, są one zdejmowane ze stosu. Poniżej przedstawiono dwie wersje tego samego "algorytmu" (właściwie nie robi on nic konkretnego). W jednej wersji zastosowano zmienne globalne:

var
  Start, Stop : Integer;
  I, J : Integer;

procedure Global;
begin
  Start := GetTickCount;

  for I := 0 to MaxInt do
  begin
    J := J + 1;
  end;
  Stop := GetTickCount;

  Writeln('Czas wykonania kodu: ', Stop - Start);
end;

begin
  Global;
end.

Czas wykonania takiego kodu to 18234 milisekund. Mała modyfikacja takiego programu, tj. użycie zmiennych jako zmiennych lokalnych, skraca czas wykonania aplikacji aż do 4625 milisekund!

Deklarując zmienną kontrolną I, jako zmienną globalną, w trakcie kompilacji Delphi wyświetli komunikat: For loop control variable must be simple local variable

Procedury zagnieżdżone

Procedury lokalne, czyli procedury zagnieżdżone (procedury w procedurach) są wolniejsze niż tradycyjne wywołanie procedur/funkcji. Zaletą procedur zagnieżdżonych jest to, iż zmienne lokalne procedury zewnętrznej są również "widoczne" w procedurze zgnieżdżonej, lecz działanie takiego kodu jest wolniejsze. Przykładowe wkorzystanie procedury zagnieżdżonej:

procedure Bar1;
var
  LoopCounter : Integer;
  Start, Stop : Integer;

  procedure BarInside;
  var
    I, J : Integer;
  begin
    J := 0;

    for I := 0 to LoopCounter do
    begin
      J := J + 1;
    end;
  end;

begin
  LoopCounter := MaxInt;
  BarInside;
end;

Taki kod można rozbić na dwie mniejsze procedury:

procedure Foo(var Value : Integer);
var
  I, J : Integer;
begin
  for I := 0 to Value do
  begin
    J := J + 1;
  end;
end;

procedure Bar2;
var
  LoopCounter : Integer;
  Start, Stop : Integer;
begin
  LoopCounter := MaxInt;
  Foo(LoopCounter);
end;

Liczby całkowite

O wiele lepiej jest stosować typy 32bitowe w miejsce 16bitowych (Word, ShortInt). Procesor na chwile musi przełączyć się w tryb 16bitowy co oczywiście ma wpływ na prędkość działania aplikacji. Typy 8-bitowe nie są specjalnie spowalniające szczególnie gdy nie mieszamy tych typów z typami 32-bitowymi.

Mowiąc o typach 16-bitowych, należy wspomnieć o tym, aby unikać typów o ograniczonym zasięgu - np:

type
  TFoo = 1000..2000;

Ten typ danych zajmuje 16-bitów a jak wspomnieliśmy - taki typ danych jest wolniejszy.

W tym artykule wspomnieliśmy o korzyściaj wynikających z użycia zbiorów. Spójrz na poniższy przykład:

if (X > 0) and (X < 20) then { ... }

Mamy tutaj dwa warunki logiczne do sprawdzenie. O wiele lepszym byłoby zastosowanie w to miejsce zbiorów:

if X in [0..20] then { ... }

Mówiąc o liczbach całkowitych nie sposób wspomnieć o typach danych. Delphi oferuje nam wiele typów danych mogących operować na liczbach całkowitych (rzeczywistych również) dlatego należy rozsądnie dokonywać ich wyboru. Należy zastanowić się jak duży zakres będzie nam potrzebny i czy do danej operacji nie wystarczy użycie typu Integer? Ten typ danych jest najoptymalniejszy we wszelkich operacjach dokonywanych na liczbach całkowitych (mnożenie, dzielenie, dodawanie). Natomiast bardzo wolnym typem jest typ Int64 lub Comp (typ Comp uważany jest za przestarzały, zaleca się używanie typu Int64), lecz zapewnia on największą precyzję oraz zakres z pośród typów obecnych w Delphi.

Jeżeli dokonujesz operacji dodawania lub odejmowania, o wiele lepszym rozwiązaniem będzie zastosowanie procedur Inc oraz Dec w miejsce operatorów + i -.

Zobacz też:

.. [#] Więcej informacji o tablicach znajdziesz w tekście Tablice

Dynamiczne tworzenie form

Jak przekonał się każdy, kto próbował napisać (bądź napisał) jakąkolwiek większą aplikację, ciężko jest pozostać przy jednej formie. Jeśli już zdecydowaliśmy się na więcej form w naszym programie to aby mieć jakąkolwiek nad nimi kontrolę zmuszeni jesteśmy tworzyć je ręcznie. Większość programistów robi to w miejscu, w którym "potrzebują" danej formy. Np. mamy przycisk "Opcje", którego zadaniem jest wyświetlenie formy z opcjami. Kod wygląda tak:

procedure TForm1.btnOptionsClick(Sender: TObject);
var
  fOpcje: TfOpcje;
begin
  fOpcje := TfOpcje.Create(self);
  //tu ustawiamy formę 
  if fOpcje.ShowModal = mrOK then
  begin
    //tu np. zapisujemy zmienione opcje
  end;
  FreeAndNil(fOpcje);
end;

i jest poprawny. Ale teraz przyjmijmy taki scenariusz - Twoja aplikacja się rozrosła, dodałeś do niej MainMenu z pozycją "Opcje" oraz inną formę, na której tez jest przycisk "Opcje".
Jedno rozwiązanie to powielić jeszcze dwa razy powyższą procedurę. Drugie to z pozostałych dwóch wywoływać tą pierwszą.
Oba zadziałają i oba są poprawne ale prędzej czy później staną się kłopotliwe. Wadą pierwszego jest to, że mamy kod w trzech różnych miejscach - jeśli zmienimy w jakiś sposób formę "Opcje" musimy zmieniać kod w trzech miejscach i łatwo coś pominąć. Wadą drugiego jest to, że ten kod może się nam "zgubić" jeśli unit głównej formy się rozrośnie.
Istnieje trzecie rozwiązanie - w module formy, którą chcemy wywołać piszemy prostą funkcję, której ciało jest prawie takie samo jak ciało powyższej funkcji.

unit Unit2;

interface

uses
  ...

type
  TfOptions = class(TForm)
    ...
  private
    { Private declarations }
  public
    { Public declarations }
  end;

function ShowOptions(AOwner: TComponent): TModalResult;

implementation

function ShowOptions(AOwner: TComponent): TModalResult;
var
  fOptions: TfOptions;
begin
  fOptions := TfOptions.Create(AOwner);
  try
    with fOptions do
    begin
      //ustawienie formularza
      Result := ModalResult;
    end;
  finally
    FreeAndNil(fOptions);
  end;
end;

dodatkowo do funkcji tej możemy przekazać parametry. Załóżmy, że forma "Opcje" wyświetla nam takie dane jak
*Nazwa użytkownika
*Ścieżkę do pliku
*Kolor głównej formy

powyższą funkcję możemy trochę zmodyfikować

function ShowOptions(var AName, APath: string; var AColor: TColor; AOwner: TComponent): TModalResult;

implementation

function ShowOptions(var AName, APath: string; var AColor: TColor; AOwner: TComponent): TModalResult;
var
  fOptions: TfOptions;
begin
  fOptions := TfOptions.Create(AOwner);
  try
    with fOptions do
    begin
      edtName.Text := AName;
      edtPath.Text := APath;
      cpColor.Color := AColor;
      Result := ModalResult;
      if Result = mrOK then
      begin
        AName := edtName.Text;
        APath := edtPath.Text;
        AColor := cpColor.Color;
      end;
    end;
  finally
    FreeAndNil(fOptions);
  end;
end;

Teraz w miejscu, z którego "wywołujemy" dodatkową formę piszemy tylko

if ShowOptions(Name, Path, Color, Self) then
  //tu zapisanie opcji, które mamy w zmiennych Name, Path i Color.
  //jeśli użytkownik kliknął anuluj to wartość zmiennych się nie zmieniła

Zalety tego rozwiązanie

  • kod tworzący i ustawiający daną formę jest zawsze w tym samym miejscu - pod słowem implementation danej formy
  • tworzenie formy odbywa się w tylko jednym miejscu
  • do funkcji tworzącej formę możemy przekazać (i zwrócić) dowolną ilość parametrów

VCL

Dodawanie wielu elementów do list

Często początkujący programiści chcąc dodać wiele elementów do list TStrings używają takiego kodu:

var 
  i: Integer;
begin
  for i := 0 to 10000 do
    ListBox1.Items.Add('Liczba' + IntToStr(i));
end;

Kod ten wykonuje się dość wolno, ponieważ po każdym dodaniu elementu do listy jest on wyświetlany.

Przy dodawaniu elementów do list, czy w ogóle modyfikacji treści jakiegoś komponentu, powinno się korzystać z metod, które służą do wyłączania odświeżania graficznego komponentu.

var 
  i: Integer;
begin
  ListBox1.Items.BeginUpdate;
  for i := 0 to 10000 do
    ListBox1.Items.Add('Liczba' + IntToStr(i));
  ListBox1.Items.EndUpdate;
end;

Dzięki temu moc obliczeniowa nie jest marnowana na operacje, które i tak nie dają żadnego efektu.

Pierwszy kod wykonywał się ok. 3000ms, drugi natomiast: 820ms (na komputerze z procesorem 266MHz).

Jak widać, róznica jest znaczna i nie można zapominać o używaniu tych metod z komponentami typu: TMemo, TListBox, TComboBox, TTreeView itp.

20 komentarzy

Odnośnie interpretacji kodu operującego na liczbach 32 bitowych.
W trybie 32 bitowym żeby użyć rejestru 16 bitowego należy (to robi kompilator) przed kod mnemoniczny instrukcji dodać prefix, chyba to jest $66 jak dobrze teraz pamiętam. Dlatego taki kod działa wolniej, bo procesor ma do interpretacji dodatkowy bajt przed prawie każdą instrukcją 16 bitową.

Dawno w Delphi nie pisałem a jeszcze dawniej nie analizowałem kodu wynikowego, jaki produkuje, ale jeszcze w Delphi 6 kompilator wiele z podanych tutaj optymalizacji sam przeprowadzał (jak np. te ze zmiennymi boolowskimi). Czy bardziej czytelne? To kwestia tego jakie kto ma tło programistyczne. Jeżeli ktoś długo programuje i nie są mu obce języki typu C, to konstrukcja: if X then jest czytelniejsza, niż if X = True then. Jednak spytajcie początkującego, które wydaje mu się bardziej intuicyjne :)

Jednak jako materiał edukacyjny dla początkujacych bardzo mi się podoba. Lepiej nabierać dobrych nawyków, niż polegać na kompilatorze.

Sanjuro pisał, że warto wyliczanie wartości końcowej pętli FOR wynieść przed pętlę. Otóż nie warto, co wynika z zasad PASCALA! Wartość "końcowa" wyliczana jest tylko raz.
Możemy napisać: X:=cośtam; FOR X:= X-1 TO X+1 DO..., pętla ?obróci? się tylko trzy razy.

"W obu przypadkach zmienne muszą być oczywiście typu Boolean." nie powinno być opatrzone ikonką "zapamiętaj"

Sanjuro: Proponuje wiec dodac te cenne informacje do tekstu. Edytowanie nie boli! Zajrzyj do pomocy!

z 32 bitami w Delphi sie zgadza, kto nie wierzy niech sprawdzi. Dla wiekszosci danych roznych od 32bit proram dziala ciut wolniej. Tak wiec jesli nasza optymalizacja ma na celu przyspieszenie programu, kosztem pamieci to najlepiej korzystac z liczb calkowitych 32 bitowych.

Kolejna sprawa nie poruszona w artykule. Jesli korzystamy z petli to:

  1. dzialania powtarzajace sie w petli wiecej niz 1 raz warto obliczyc wczesniej i do dzialan podawac gotowa liczbe (przyklad: mamy tzw okno Hamminga:
    y(n)=0.53836-0.46164cos(2piNn);
    jak widac w cosinusie jest wartosc stala "2piN" tak wiec obliczamy ja przed petla zeby nie tracic cennych cykli na cztery mnozenia za kazdym razem wykonywania cosinusa, czyl;i wykonujemy cos takiego:

pi2N:=2piN;
for n:=0 to N-1 do
y[n]=0.53836-0.46164cos(pi2Nn);

co daje nam jedno mnozenie w cosinusie. Dodatkowo mozna przyspieszyc petle o kolejne kilka cykli o czym ponizej:
)

  1. jesli warunek wyjscia z petli jest dzialaniem o stalej wartoisci to takze dzialanie to wykonujemy przed petla, zeby nie wykonywac za kazdym razem tego dzialania (przyklad wykonywanie operacji na np. bitmapach mamy czesto petle

for i:=0 to BMP.Height-1 do
begin
P := BMP.ScanLine[i];
for j:= 0 to (BMP.Width-1)*4 do // bitmapa 32bitowa, gdyz niektore specyficzne obliczenia sa szybciej wykonywane
...; // tutaj operacje z drugiej petli
end;

tracimy cenne cykle na obliczanie wartosci potrzebnych do sprawdzenia warunku za kazdym razem przejscia petli. Czyli mozna przyspieszyc to w sposob:

BmpHm1 := BMP.Height-1;
BmpWm1M4 = (BMP.Width-1)*4; // tu mozna np dac shl 2, ale delphi dobrze sobie radzi z kompilacja takich mnozen.
for i:=0 to BmpHm1 do
begin
P := BMP.ScanLine[i];
for j:= 0 to BmpWm1M4 do
...; // tutaj operacje z drugiej petli
end;
)


32 czy 16?
Procesor nie przełącza się w jakiś tam inny tryb.
Obliczenia pośrednie wykonywane są na liczbach wygodnych dla procesora, obecnie obowiązuje (już odchodząca) moda na 32 bity. Kiedy pobrana zostanie wartość 16bitowa, musi zostać zamieniona na wartość 32bitową. To tylko jedna instrukcja assemblera, ale przecież może ich być bardzo wiele. Różnicę czasu wykonania da się odczuć!!!

Warto gdzie się tylko da, stosować liczby bez znaku.

Hmm, to chyba nie jest prawda, w trybie 32 bitowym tez jest dostep do 16 bitowych rejestrow, > poza tym wydaje mi sie, ze delphi i tak wszystie takie typy wyrownuje do 4b

Jak to nie ma ? A AX, BX, CX, DX ? Do 8 bitowych też jest dostęp: AL, AH, BL, BH, ...

Uruchom pod disassemblerem "dowolny" program i sobie zobacz...

Procesor na chwile musi przełączyć się w tryb 16bitowy co oczywiście ma wpływ na prędkość działania aplikacji.

Hmm, to chyba nie jest prawda, w trybie 32 bitowym tez jest dostep do 16 bitowych rejestrow, poza tym wydaje mi sie, ze delphi i tak wszystie takie typy wyrownuje do 4b

Pasowaloby jeszcze cos o obliczeniach.

W Delphi moze tak tego nie widac jak w C, ale chodzi mi o to, ze komputer jest jednostka binarna i najlepiej trawi wartosci bedace potega dwojki (2^n). Tak wiec jezeli np przetwarzamy bitmape, a nie zalezy nam na pamieci to warto ustawic 32bit kolor bitmapy i wtedy czesc operacji bedzie mnozeniem i dzieleniem bedacym wlasnie potega 2. Delphi ma dobry kompilator, ktory robi czesc optymalizacji matematycznych za nas, ale warto pamietac o operatorach dzialan na liczbach calkowitych, ktore przyspieszaja dzialania ("div", "shr", "shl" itp.). Zauwazylem, ze przynajmniej w Delphi 6, kompilator radzi sobie z zamiana mnozenia i dzielenia na przesuwanie bitowe tam gdzie mnoznikiem/dzielnikiem jest liczba bedaca potega dwojki.

Borland C++ Builder wymusza za nas myslenie o optymalizacji. Dlatego szegolnie wazne jest umiejetnosc uzywania operatorow przesuwania bitowego ("<<" oraz ">>").

Kolejna sprawa, o ktorej moga nie wiedziec poczatkujacy. Warto zobaczyc do opcji projektu. Sa tam ustawienia kompilatora. Standardowo kod uzywa instrukcji pod maszyny 386 (chyba nie musze mowic ze to niezbyt dobrze?) Warto przelaczyc sie na Pentium. Dodatkowo kompilator standardowo kompiluje w tzw. trybie "Full Debug" przydatne w fazie pisania programu, bezurzyteczne gdy mamy program gotowy dodatkowo tryb FullDebug ok 2x zwalnia prace programu. Z tego co pamietam to w "Delphi 6" trzeba recznie odchaczac wszystkie checkbox'y odpowiedzialne ze debugowanie (nie wiem jak jest w dalszych wersjach), w Builderze 5 wystarczy taki ladny przycisk nacisnac "Relase" w zakladce "Compiler" i juz mamy sprawe zalatwiona.

Czy powyzsze optyamlizacje cos daja?? Owszem dla przykladu podam programik bedacy gdzies na 4programmers BumpMaping (wersja dla C++ Buildera) na komputerze z P3 850Mhz na pokladzie:

  • przed zmianami w programie, kompilacja "Full Debug", instrukcje 386: ~10-13fps
  • po optymalizacjach matematycznych, kompilacja "Full Debug", instrukcje 386: ~30fps
  • po optymalizacjach matematycznych, kompilacji w trybie "Relase" z instrukcjami Pentium: ~50-55fps

Dodam jeszcze ze wyzsza szkola jazdy jest uzywanie matematyki staloprzecinkowej :) ktora umozliwia przyspieszenie dzialan na liczbach rzeczywistych.

Pawel200x.5: Dzięki za poprawienie, wydawało mi się, że mój sposób jest wystarczająco dobry. Ale... każdy się ciągle uczy (.

Migajek: albo bardziej ogólnie - dwukrotne (wielokrotne) obliczanie tej samej wartości.

Adam : czesto zdaza mi sie nawet w komercyjnych programach spotykac "List index out of bounds" :} ze juz nie wspomne o koniecznosci dwukrotengo przeszukania listy :P

Migajek: najczestszy blad? :]
Coldpeer: jezeli masz cos ciekawego, to dopisz.

a gdzie najczestszy blad? :>
np. if ListBox1.Items.IndexOf('ble') > -1 then ListBox1.Items.Delete(ListBox1.Items.IndexOf('ble'));
zamiast
var
int: integer;
begin
int:= ListBox1.Items.IndexOf('ble');
if int > -1 then ListBox1.Items.Delete(i);

W sumie, to to takie podstawy, ale jakiemuś początkującemu moze się przydać :)

Zachecam wszystkich do rozwijania tego tekstu :)

Adam: na razie nic nie mam... A tak w ogóle, to nie chodziło mi o to, że materiał podstawowy i że można mocno rozwinąć, tylko że bardziej dla początkujących programistów :) Ale mniejsza o to ;) I tak jest git :)

Moze byc #13#10#13#10 - jak kto woli ;) Chodzilo po prostu o wstawienie 2 linii.

#13#13? A nie przypadkiem #13#10?