Een microseconden teller component in Delphi


Inleiding

Dit artikel beschrijft een microseconden teller in de programmeertaal Delphi (versie 7).
De nauwkerigheid is in nanoseconden.
Hiervoor wordt de interne processor klok gebruikt, een 64 bits teller die elke clockcycle ophoogt.
Van de programmacode is een Class gemaakt die als component geïnstalleerd kan worden.
De bedoeling van dit project is het nauwkeurig meten van de tijdsduur.

Werkwijze

Een CPU (chip) heeft een bepaalde clock frequentie, bijvoorbeeld 3GHz (Giga Hertz).
Dat betekent dat per seconde er 3 miljard "tikken" plaatsvinden.
Eerst moet worden uitgezocht wat de clockfrequentie is, anders kan geen tijdsduur worden berekend.

Er is ook een andere clock, die milliseconden telt.
De functie getTickCount levert die clock.
Maar daar is wat vreemds mee aan de hand.
We voeren het volgende programma uit, dat opvolgende verhogingen van de clock in een TMemo component zet:
procedure TForm1.Button1Click(Sender: TObject);
var n : byte;
    t1,t2 : dword;
    s : string;
begin
 memo1.Clear;
 n := 0;
 t1 := GetTickCount;
 repeat
  t2 := getTickcount;
  if t2 <> t1 then
   begin
    inc(n);
    s := inttostr(t2 - t1);
    t1 := t2;
    memo1.lines.add(s);
   end
   else application.ProcessMessages;
 until n = 25;
end;

end.
We zien dan dit:
Er worden milliseconden geteld, maar de teller wordt maar eens per 15 of 16 milliseconden bijgewerkt.

Berekenen van de CPU kloksnelheid

Met de GetTickCount waarde kunnen we nauwkeurig 500 milliseconden tellen.
In deze afgepaste tijd meten we ook het aantal "tikken" van de 64 bits CPU klok.
Die laatste waarde leggen we vast in een floating point double variabele.

Lezen van de CPU clock gaat zo:
function TTimer64.getCPUtime : Int64;
asm
 RDTSC;  //dw $310F
end;
Opmerking: TTimer64 is de naam van de Class. Later meer details.

Opmerking: de volgende code werkt om de een of andere reden niet:
function TTimer64.getCPUtime : Int64;
begin
 asm
  RDTSC;  //dw $310F
 end;
end; 
De RDTSC (of hexadecimaal $310F) schrijft de laagste 32 bits in register EAX
en de hoogste 32 bits in register EDX.
Dat zijn ook de registers waar enkele functie parameters van het type Int64 in staan.

Nu het meten van de CPU kloksnelheid:
constructor TTimer64.create(Aowner : TComponent);
var t1,t2 : dword;
begin
 inherited create(AOwner);
 t2 := getTickCount;
 repeat
  t1 := getTickCount;
 until t2 <> t1;           //wait for millisecs counter to change
 Fstart := CPUtime;
 repeat
  t2 := getTickCount;
 until t2 - t1 >= 500;
 Fend := CPUtime;
 FCPUclock := (Fend - Fstart)/(t2 - t1) * 1e-3; //megahertz clock
end;
Eerst wordt gewacht tot de milliseconden teller is verhoogd.
CPUtime is een property die staat voor de functie getCPUTime.
Fstart krijgt de waarde van de CPU clock.
Nu wordt 500 milliseconden gewacht.
Dan krijgt Fend de waarde van de CPU clock.
FCPUclock is de (double) waarde die we zochten.

Meten van tijd

Nu de CPU kloksnelheid bekend is kunnen we nauwkeurig executietijden berekenen.
Voor het te meten proces zetten we een de CPU klok in een startwaarde.
Na het proces zetten we de CPU klok in een eindwaarde (alletwee type Int64).
Tijdsduur = (eindwaarde - beginwaarde) / klokfrequentie.
Immers: 1/klokfrequentie is de tijd tussen twee "CPU "tikken" en
eindwaarde - beginwaarde is het aantal tikken.

De timer Component

unit timercomponent;

interface
uses windows,classes,sysutils;

type TTimer64 = class(TComponent)
      private
       FCPUclock : double;
       Fstart    : Int64;
       Fend      : Int64;
      protected
       function getCPUclockString : string;
       function getCPUtime : Int64;
       function getCPUTimeString  : string;
       function getElapsedTime : double;
       function getElapsedTimeString : string;
      public
       constructor create(Aowner:TComponent);  override;
       destructor destroy; override;
       property CPUclock : double read FCPUclock;
       property CPUclockString : string read getCPUclockString;
       property CPUTimeString  : string read getCPUTimeString;
       procedure start;
       procedure stop;
       property ElapsedTime : double read getElapsedTime;
       property ElapsedTimeString : string read getElapsedTimeString;
       property CPUtime : Int64 read getCPUtime;
       function CPUclockToTime(I64 : Int64) : double;
      end;

procedure Register;

implementation

procedure Register;
begin
 RegisterComponents('system',[TTimer64]);
end;

function TTimer64.getCPUtime : Int64;
asm
 RDTSC;  //dw $310F
end;

constructor TTimer64.create(Aowner : TComponent);
var t1,t2 : dword;
begin
 inherited create(AOwner);
 t2 := getTickCount;
 repeat
  t1 := getTickCount;
 until t2 <> t1;           //wait for millisecs counter to change
 Fstart := CPUtime;
 repeat
  t2 := getTickCount;
 until t2 - t1 >= 500;
 Fend := CPUtime;
 FCPUclock := (Fend - Fstart)/(t2 - t1) * 1e-3; //microhertz clock
end;

destructor TTimer64.destroy;
begin
 inherited destroy;
end;

procedure TTimer64.Start;
begin
 FStart := CPUtime;
end;

procedure TTimer64.Stop;
begin
 Fend := CPUtime;
end;

function TTimer64.getElapsedTime : double;
begin
 result := (Fend - Fstart) / FCPUclock;
end;

function TTimer64.CPUclockToTime(i64 : Int64) : double;
begin
 result := i64 / FCPUclock;
end;

function TTimer64.getElapsedTimeString : string;
const Fconvert = '#####0.0###';
var t : double;
begin
 t := getElapsedTime;
 result := formatfloat(Fconvert,t);
end;

function TTimer64.getCPUclockString : string;
const Fconvert = '###0.0###';
begin
 result := formatfloat(FConvert,FCPUclock);
end;

function TTimer64.getCPUTimeString : string;
var i64 : int64;
begin
 i64 := CPUTime;
 result := inttostr(i64);
end;

end.
De procedures start en stop zetten de CPU klok in Fstart en Fstop.
Property ElapsedTime berekent de verlopen tijd.
De waarde kan ook meteen als string worden gegeven met ElapsedTimeString.

De class is afgeleid van TComponent.
Bij installatie als component wordt op het form wel een bitmap image (.dcr) gezet,
maar dit verdwijnt in RunTime.

Toepassing

Plaats een timer op het main form met naam CPUt.
Om de kloksnelheid in label1 te zetten:
 label1.caption := CPUt.CPUclockstring;
Om een programma te pauzeren gebruiken we de opdracht: application.processmessages
Die geeft het commando even terug aan Windows om uitstaande events te verwerken,
bijvoorbeeld ontstaan omdat we op een knop klikten of een toets indrukten.
We vragen ons af hoeveel tijd Windows nodig heeft.

Plaats een timer op het main form met een TButton en een TLabel component.
Voeg deze code toe aan het ButtonClick event:
procedure TForm1.Button1Click(Sender: TObject);
begin
 with CPUt do
  begin
   start;
   application.ProcessMessages;
   stop;
   label1.Caption := elapsedtimestring;
  end;
end;
Ruim 85 microseconden.