Gravierende Laufzeitunterschiede

Für allgemeine Fragen zur Programmierung, welche nicht! direkt mit Lazarus zu tun haben.
Antworten
Adrian
Beiträge: 31
Registriert: Mo 12. Nov 2007, 12:41
OS, Lazarus, FPC: Winux (L 2.0.6 FPC 3.0.4)
CPU-Target: 64Bit

Gravierende Laufzeitunterschiede

Beitrag von Adrian »

Servus!

Kann mir jemand mal auf die Sprünge helfen.
Vor einigen Jahren schrieb ich ein kleines Programm zur Pi-Annäherung ungefähr so, wie es der nachfolgende Ausschnitt zeigt:

Code: Alles auswählen

 
var
  i : LongWord;
  x, n : Real;
  QueryLos, QueryHalt : Int64;
//irgendwas ...
  n:=1;
  x:=0;
  QueryLos := GetTickCount64;
  for i:=1 to 500000000 do
  begin
    x:=x+(1/n);
    if n<0 then n:=n-2;
    if n>0 then n:=n+2;
    n:=(-1)*n;
  end;
  QueryHalt := GetTickCount64;
  Dauer:= IntToStr(QueryHalt – QueryLos);
end;
 

Nur so interessehalber führte ich das Programm auf zwei recht verschiedenen Rechnern durch:
System 1: Netbook / Atom-Prozessor N450 / 1,66 GHz / 2 GB RAM
System 2: Desktop / Intel i5-2320 / 3 GHz / 8 GB RAM
Die Laufzeiten waren doch recht unterschiedlich, bei System 1 etwa 42 Sekunden, bei System 2 nur knapp 4 Sekunden. Und das ists, was ich nicht ganz nachvollziehen kann.
Da bei dem Programm nur ganz wenig Speicher gebraucht wird und auch kaum Festplattenzugriffe erfolgen, da sollte meiner Meinung nach nur die Taktfrequenz ausschlaggebend sein. Das Ergebnis widerspricht aber offensichtlich meiner Meinung.

Und nun suche ich jemanden, der mir erklären kann, wo mein Denkfehler ist, und warum so gravierende Laufzeitunterschiede auftreten.

Gruß,
Adrian

Mathias
Beiträge: 6146
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Gravierende Laufzeitunterschiede

Beitrag von Mathias »

Ein i5 arbeitet einiges schneller als ein Atom. Ein Atom kann man fast mit einem Rasberry Vergleichen.
Es tönt zwar komisch, wieso verbratet ein i6 einiges meher Strom als ein Atom ?
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

indianer-frank
Beiträge: 134
Registriert: So 30. Nov 2008, 21:53

Re: Gravierende Laufzeitunterschiede

Beitrag von indianer-frank »

Was soll (-1)*n bei einem Longword bedeuten? Und n kann selbst verständlich nicht kleiner als 0 sein. Sehe gerade n ist unüblicherweise auch real.

Und bist Du sicher, daß die Real-Arithmetik gleich implementiert ist? Beim i5 mit ziemlicher Sicherheit als Hardware double. Wie sieht das bei Atom aus. Wenn da Softfloat zum Einsatz kommt, wäre das ein weiterer Sargnagel.

Mathias
Beiträge: 6146
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Gravierende Laufzeitunterschiede

Beitrag von Mathias »

Wenn da Softfloat zum Einsatz kommt, wäre das ein weiterer Sargnagel.
Meinst du, sowas gibt es auf einem Intel noch, ab eine 486DX, wurde der Matheprozessor fest verbaut.
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

Benutzeravatar
theo
Beiträge: 10462
Registriert: Mo 11. Sep 2006, 19:01

Re: Gravierende Laufzeitunterschiede

Beitrag von theo »

Einfach mal die Benchmarks vergleichen:

https://www.cpubenchmark.net/compare/In ... 0/790vs618

Warf
Beiträge: 1907
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Gravierende Laufzeitunterschiede

Beitrag von Warf »

Branch prediction, Cache, erweiterter Befehlssatz, Fast memory access, DDR3 VS DDR2. Da kommt ne menge zusammen

Das ist als würdest du einen Opel Corsa mit einem BMW M5 vergleichen. Der Atom kann einfach viel weniger (als Mobile CPU soll er ja auch keine powermaschine sein, lieber weniger können dafür weniger Strom brauchen).

Was bei dir den Riesen unterschied macht keine Ahnung, aber wenn die Variablen nicht in Registern liegen kann allein der Unterschied im Memory Access dafür verantwortlich sein

Horst_h
Beiträge: 72
Registriert: Mi 20. Mär 2013, 08:57

Re: Gravierende Laufzeitunterschiede

Beitrag von Horst_h »

Hallo,

wie der CPu Bench zeigt, liegt die single-thread performance bei 1:8 also nicht so weit weg von 1:10 hier.
Man kann aber das Programm ein wenig umstellen, um ohne IF's und n als double auszukommen.
Vielleicht hilft das ja ein wenig.

Code: Alles auswählen

program p;
uses
  sysutils;
 
var
  x : double;
  i : NativeInt;
  n : NativeUint;
  QueryLos, QueryHalt : Int64;
begin
//irgendwas ...
  n:=1;
  x:=0;
  QueryLos := GetTickCount64;
  for i:=1 to 250000000 do
  begin
    x:=x+(1/n);
    n:=n+2;
    x:=x-(1/n);
    n:=n+2;
  end;
  QueryHalt := GetTickCount64;
  writeln(QueryHalt-QueryLos);
  writeln(x*4.0-pi);
end.
 

Das ergibt auf einem Ryzen 5 1600

Code: Alles auswählen

1922
-2.00053541549345681450E-0009

Die urspüngliche Version der Schleife brauchte bei mir knapp 4 Sekunden.

Gruß Horst

sstvmaster
Beiträge: 575
Registriert: Sa 22. Okt 2016, 23:12
OS, Lazarus, FPC: W10, L 2.2.6
CPU-Target: 32+64bit
Wohnort: Dresden

Re: Gravierende Laufzeitunterschiede

Beitrag von sstvmaster »

@Horst

Intel Core2Duo E8500 @ 3.16GHz

Code: Alles auswählen

7238
-2.00038753378657674364E-0009
LG Maik

Windows 10,
- Lazarus 2.2.6 (stable) + fpc 3.2.2 (stable)
- Lazarus 2.2.7 (fixes) + fpc 3.3.1 (main/trunk)

Timm Thaler
Beiträge: 1224
Registriert: So 20. Mär 2016, 22:14
OS, Lazarus, FPC: Win7-64bit Laz1.9.0 FPC3.1.1 für Win, RPi, AVR embedded
CPU-Target: Raspberry Pi 3

Re: Gravierende Laufzeitunterschiede

Beitrag von Timm Thaler »

i5-4570 @3.20GHz

Optimierung -O1, mit Debugger

Code: Alles auswählen

3916
-2.0005352929786113E-009


Optimierung -O3, ohne Debugger

Code: Alles auswählen

3775
-2.0005352929786113E-009

Adrian
Beiträge: 31
Registriert: Mo 12. Nov 2007, 12:41
OS, Lazarus, FPC: Winux (L 2.0.6 FPC 3.0.4)
CPU-Target: 64Bit

Re: Gravierende Laufzeitunterschiede

Beitrag von Adrian »

Hallo Horst!

Dein Programm verbessert die Laufzeit natürlich, aber es ging mir eigentlich nicht um die Optimierung, sondern um die Verarbeitungsgeschwindigkeit der einzelnen CPUs.
Da während meines Studiums noch der 8085 durchgenommen wurde (damals, als es noch den guten Westen und den bösen Osten gab, als die Mauer noch stand und das Geld noch was wert war), kenne ich mich mit den modernen Prozessorarchitekturen nicht gut aus. Aber das würde ich ganz gern ändern. Wer kann mir den ein gutes Buch nennen (darf ruhig auch in Englisch sein), in dem ich so etwas nachschlagen kann? Also beispielsweise Beschreibungen, welche Befehle wieviele Taktzyklen brauchen. Gerade Studenten der Elektrotechnik müssten das doch wissen.

Gruß,
Adrian

wp_xyz
Beiträge: 4864
Registriert: Fr 8. Apr 2011, 09:01

Re: Gravierende Laufzeitunterschiede

Beitrag von wp_xyz »

Horst_h hat geschrieben:Das ergibt auf einem Ryzen 5 1600

Code: Alles auswählen

1922
-2.00053541549345681450E-0009

Die urspüngliche Version der Schleife brauchte bei mir knapp 4 Sekunden.

Seltsam, das kann ich überhaupt nicht bestätigen. Auf meinem (etwas betagten) i5 ist Adrian's Code fast doppelt so schnell als Horst_h's. Erst wenn ich in Horst's Code die NativeUInt gegen stinknormale Integer austausche, wird dieser Code genauso schnell wie bei Adrian. Die Floating-Point-Rechnung macht sich nicht bemerkbar. Hätte ich nicht erwartet. Also Vorsicht bei vermeintlich naheliegenden Optimierungen!

Code: Alles auswählen

program p;
uses
  sysutils;
 
const
  IMAX = 250000000;
var
  x: double;
  i : NativeInt;
  n : NativeUInt;
  ni: Integer;
  nd: Double;
  QueryLos, QueryHalt : Int64;
 
begin
  WriteLn('Adrian / double');
  nd:=1;
  x := 0;
  QueryLos := GetTickCount64;
  for i:=1 to IMAX*2 do
  begin
    x := x + (1/nd);
    if nd<0 then nd:=nd-2;
    if nd>0 then nd:=nd+2;
    nd := -nd;
  end;
  QueryHalt := GetTickCount64;
  writeln(QueryHalt-QueryLos);
  writeln(x*4.0-pi);
  WriteLn;
 
  WriteLn('Horst_h / NativeUInt');
  n:=1;
  x:=0;
  QueryLos := GetTickCount64;
  for i:=1 to IMAX do
  begin
    x:=x+(1/n);
    inc(n, 2);
    x:=x-(1/n);
    inc(n, 2);
  end;
  QueryHalt := GetTickCount64;
  writeln(QueryHalt-QueryLos);
  writeln(x*4.0-pi);
  WriteLn;
 
  WriteLn('Horst_h / integer');
  ni:=1;
  x:=0;
  QueryLos := GetTickCount64;
  for i:=1 to IMAX do
  begin
    x:=x+(1/ni);
    inc(ni, 2);
    x:=x-(1/ni);
    inc(ni, 2);
  end;
  QueryHalt := GetTickCount64;
  writeln(QueryHalt-QueryLos);
  writeln(x*4.0-pi);
  WriteLn;
 
  WriteLn('Horst_h / double');
  nd:=1;
  x:=0;
  QueryLos := GetTickCount64;
  for i:=1 to IMAX do
  begin
    x:=x+(1.0/nd);
    nd:=nd+2;
    x:=x-(1.0/nd);
    nd:=nd+2;
  end;
  QueryHalt := GetTickCount64;
  writeln(QueryHalt-QueryLos);
  writeln(x*4.0-pi);
  WriteLn;
 
  WriteLn('ENTER druecken zum Beenden...');
  ReadLn;
end.   


Mit Standard-Optimierung -O1 (Unterschiede zu ohne Optimierung, -O2, -O3 und zu -O4 sind < 1%):
Adrian / double
3375
-2.00038753378657674364E-0009

Horst_h / NativeUInt
7406
-2.00038753378657674364E-0009

Horst_h / integer
3375
-2.00038753378657674364E-0009

Horst_h / double
3360
-2.00038753378657674364E-0009

ENTER druecken zum Beenden...

Horst_h
Beiträge: 72
Registriert: Mi 20. Mär 2013, 08:57

Re: Gravierende Laufzeitunterschiede

Beitrag von Horst_h »

Hallo,

besonders bei 32-Bit bekommt man die geringe Zahl an Registern zu spüren.Das auslagern in kleine Funktionen bringt erheblich was und es sollte auch vergleichbarer werden, egal wo man die Funktion im Quelltext benutzt.

Code: Alles auswählen

program p;
uses
  sysutils;
 
const
  IMAX = 250000000;
var
  x: double;
  QueryLos, QueryHalt : Int64;
 
function GetPiAdrian:double;
var
  x,n : double;
  i : NativeInt;
begin
  n:=1;
  x := 0;
  for i:=2*Imax-1 downto 0 do
  begin
    x := x + (1/n);
   // leicht geändert und einen Vergleich gespart, denn n= 0 kommt nicht vor
    if n>0 then
      n:= -(n+2)
    else
      n:= -(n-2);
  end;
  GetPiAdrian := x;
end;
 
function GetPiNativeInt:double;
var
  x : double;
  n : NativeInt;
  i : NativeInt;
begin
  n:=1;
  x := 0;
  for i:=1 to IMAX do
  begin
    x:=x+(1/n);
    inc(n, 2);
    x:=x-(1/n);
    inc(n, 2);
  end;
  GetPiNativeInt := x;
end;
 
function GetPiLongInt:double;
var
  x : double;
  n : LongInt;
  i : NativeInt;
begin
  n:=1;
  x := 0;
  for i:=1 to IMAX do
  begin
    x:=x+(1/n);
    inc(n, 2);
    x:=x-(1/n);
    inc(n, 2);
  end;
  GetPiLongInt := x;
end;
 
function GetPiDouble:double;
var
  x,n : double;
  i : NativeInt;
begin
  n:=1;
  x := 0;
  for i:=1 to IMAX do
  begin
    x:=x+(1.0/n);
    n:=n+2;
    x:=x-(1.0/n);
    n:=n+2;
  end;
  GetPiDouble:= x;
end;
 
begin
  WriteLn('Adrian / double');
  QueryLos := GetTickCount64;
  x := GetPiAdrian;
  QueryHalt := GetTickCount64;
  writeln(QueryHalt-QueryLos); writeln(x*4.0-pi);WriteLn;
 
  WriteLn('Horst_h / NativeUInt');
  QueryLos := GetTickCount64;
  x := GetPiNativeInt;
  QueryHalt := GetTickCount64;
  writeln(QueryHalt-QueryLos);writeln(x*4.0-pi);WriteLn;
 
  WriteLn('Horst_h / integer');
  QueryLos := GetTickCount64;
  x := GetPiLongInt;
  QueryHalt := GetTickCount64;
  writeln(QueryHalt-QueryLos);writeln(x*4.0-pi);WriteLn;
 
  WriteLn('Horst_h / double');
  QueryLos := GetTickCount64;
  x:= GetPiDouble;
  QueryHalt := GetTickCount64;
  writeln(QueryHalt-QueryLos);writeln(x*4.0-pi);WriteLn;
 
  WriteLn('ENTER druecken zum Beenden...');
  ReadLn;
end.

Linux64
Ryzen 5 1600 @ 3.2 Ghz ( Turbo? aus )

32-Bit Kompilat fpc -O1 -al -Xs "%f" // Benutzt die FPU und schreibt sichert ständig die Daten auf den Stack.

Code: Alles auswählen

 Adrian / double
2727
-2.00038753378657674364E-0009
 
Horst_h / NativeUInt
2318
-2.00038753378657674364E-0009
 
Horst_h / integer
2318
-2.00038753378657674364E-0009
 
Horst_h / double
2407
-2.00038753378657674364E-0009
 
ENTER druecken zum Beenden...

64-Bit Kompilat
/usr/lib/fpc/3.0.4/ppcx64 -O1 -al -Xs "%f"

Code: Alles auswählen

 
Adrian / double
1916
-2.00053541549345681450E-0009
 
Horst_h / NativeUInt
1662
-2.00053541549345681450E-0009
 
Horst_h / integer
1659
-2.00053541549345681450E-0009
 
Horst_h / double
1707
-2.00053541549345681450E-0009
 
ENTER druecken zum Beenden...

Jetzt die Optimierung hochgeschraubt, die bei 32-Bit fast nichts brachte.
/usr/lib/fpc/3.0.4/ppcx64 -O3 -al -Xs "%f"

Code: Alles auswählen

 
Adrian / double
657
-2.00053541549345681450E-0009
 
Horst_h / NativeUInt
615
-2.00053541549345681450E-0009
 
Horst_h / integer
616
-2.00053541549345681450E-0009
 
Horst_h / double
612
-2.00053541549345681450E-0009
 
ENTER druecken zum Beenden...


Man sieht, dass die Berechnung mit der FPU ( 32-Bit ) leicht andere Zahlen als mit SSE ( 64-Bit ) liefert.

Gruß Horst
EDIT:
es 500e6 Berechnungen.
Takte pro Berechnugen = Zeit/AnzahlBerechnungen * CPU-Takte

612 ms/500e6*3,2e6 Takte/ms = 3,917 Takte/Berechnung selbst bei Turbo mit 3,6 Ghz sind es nur 4,4 Takte.

Antworten