/ Forside / Teknologi / Udvikling / C/C++ / Nyhedsindlæg
Login
Glemt dit kodeord?
Brugernavn

Kodeord


Reklame
Top 10 brugere
C/C++
#NavnPoint
BertelBra.. 2425
pmbruun 695
Master_of.. 501
jdjespers.. 500
kyllekylle 500
Bech_bb 500
scootergr.. 300
gibson 300
molokyle 287
10  strarup 270
Kan denne funktion gøres hurtigere?
Fra : Per Abrahamsen


Dato : 09-05-03 17:55

Den funktion der bliver brugt mest tid i i mit program er

double
PLF::Implementation:erator () (const double pos) const
{
assert (x.size () > 0);
assert (x.size () == y.size ());

int min = 0;
int max = x.size () - 1;

if (pos <= x[min])
return y[min];
else if (pos >= x[max])
return y[max];

while (true)
{
if (max - min == 1)
   return y[min]
    + (y[max] - y[min]) / (x[max] - x[min]) * (pos - x[min]);

const int guess = (max + min) / 2;

if (x[guess] < pos)
   min = guess;
else if (x[guess] > pos)
   max = guess;
else
   return y[guess];
assert (max > min);
}
}

Den implementerer en stykvis lineær funktion, hvor x og y indeholder
en række punkter, og der interpoleres lineært mellem punkterne.

Den bliver brugt rigtig mange forskellige steder i programmet, nogen
gange med få punkter, andre gange med mange.

 
 
Bertel Lund Hansen (09-05-2003)
Kommentar
Fra : Bertel Lund Hansen


Dato : 09-05-03 18:10

Per Abrahamsen skrev:

>double
>PLF::Implementation:erator () (const double pos) const

Kan du ikke bruge en reference til pos i stedet?

> assert (x.size () > 0);
> assert (x.size () == y.size ());

Er assert ikke en dyr måde at programmere på?

--
Bertel
http://bertel.lundhansen.dk/   FIDUSO: http://fiduso.dk/

Jakob Nielsen (10-05-2003)
Kommentar
Fra : Jakob Nielsen


Dato : 10-05-03 09:04

> Er assert ikke en dyr måde at programmere på?

assert kører kun i debug mode. I release giver den ingen kode. Den er derfor
100% gratis i release, hvor det betyder noget.



Per Abrahamsen (11-05-2003)
Kommentar
Fra : Per Abrahamsen


Dato : 11-05-03 14:37

Bertel Lund Hansen <nospamfor@lundhansen.dk> writes:

> Per Abrahamsen skrev:
>
>>double
>>PLF::Implementation:erator () (const double pos) const
>
> Kan du ikke bruge en reference til pos i stedet?

Jeg tror det vil gøre den langsommere. Det vil spare 4 byte på
stakken ved kald, til gengæld vil hvert eneste reference til pos kræve
en ekstra indirektion.

>> assert (x.size () > 0);
>> assert (x.size () == y.size ());
>
> Er assert ikke en dyr måde at programmere på?

Det er dyrt _ikke_ at bruge asserts. Fejl opdages senere.

Som Jakob siger, hvis man compiler med _NDEBUG bliver assert defineret
som

#define assert(x) /* ingenting */

så det er bare en kommentar.

Jeg compiler nu aldrig med _NDEBUG, heller ikke i released kode, det
er ufatteligt meget bedre at få fejlrapport fra en bruger i stil med
"programmer siger

file.c:101: assertion 'x > 0' failed

end "Programmet siger 'ulovlig undtagelse' eller værre "programmet
giver forkerte svar".


Bertel Lund Hansen (11-05-2003)
Kommentar
Fra : Bertel Lund Hansen


Dato : 11-05-03 15:05

Per Abrahamsen skrev:

>> Kan du ikke bruge en reference til pos i stedet?

>Jeg tror det vil gøre den langsommere. Det vil spare 4 byte på
>stakken ved kald, til gengæld vil hvert eneste reference til pos kræve
>en ekstra indirektion.

Jeg troede egentlig at adressen på pos blev indsat direkte. Kan
det ikke lade sig gøre?

--
Bertel
http://bertel.lundhansen.dk/   FIDUSO: http://fiduso.dk/

Anders J. Munch (11-05-2003)
Kommentar
Fra : Anders J. Munch


Dato : 11-05-03 17:20

"Bertel Lund Hansen" skrev:
> Per Abrahamsen skrev:
>
> >> Kan du ikke bruge en reference til pos i stedet?
>
> >Jeg tror det vil gøre den langsommere. Det vil spare 4 byte på
> >stakken ved kald, til gengæld vil hvert eneste reference til pos kræve
> >en ekstra indirektion.
>
> Jeg troede egentlig at adressen på pos blev indsat direkte. Kan
> det ikke lade sig gøre?

Principielt jo. Men dels vil det gøre funktionen ikke-reentrant, og
dels kan kode-modifikation være en meget dyr operation på en moderne
pipelinet cpu.

- Anders




Bertel Lund Hansen (11-05-2003)
Kommentar
Fra : Bertel Lund Hansen


Dato : 11-05-03 17:27

Anders J. Munch skrev:

>> Jeg troede egentlig at adressen på pos blev indsat direkte. Kan
>> det ikke lade sig gøre?

>Principielt jo. Men dels vil det gøre funktionen ikke-reentrant

Det er da også rigtigt.

--
Bertel
http://bertel.lundhansen.dk/   FIDUSO: http://fiduso.dk/

Mogens Hansen (11-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 11-05-03 17:35


"Per Abrahamsen" <abraham@dina.kvl.dk> wrote

[8<8<8<]
> Jeg tror det vil gøre den langsommere. Det vil spare 4 byte på
> stakken ved kald, til gengæld vil hvert eneste reference til pos kræve
> en ekstra indirektion.

Hvis compileren er god, tror jeg ikke at der er være nævneværdig forskel.
Hvis compileren ikke er tilstrækkelig god, ville jeg også forvente det blev
langsommere.

Med Intel C++ V6.0 (til IA32) bliver "pos" kun refereret een gang pr. kald
til funktionen, hvilket kun udgør en meget lille del af den samlede tid.
Det sker i starten af funktionen, når værdien bliver lagt en i et processor
register (på FPU stakken) og bliver der under hele funktionens kørsel.

Det vil sige at ved brug af reference opnår man:
* Ingen performance forbedring, men måske en degradering
* En afvigelse for den idiomatiske måde at overføre build-in typer til
funktioner


Venlig hilsen

Mogens Hansen





Troels Thomsen (12-05-2003)
Kommentar
Fra : Troels Thomsen


Dato : 12-05-03 13:45


> Jeg compiler nu aldrig med _NDEBUG, heller ikke i released kode, det
> er ufatteligt meget bedre at få fejlrapport fra en bruger i stil med
>

God pointe, men man mangler tit kald stakken for at finde ud af i hvad der
gik galt.
"Assertion hDc > 0 failed" siger ikke meget.

GCC har garanteret en switch der gør at den skriver kald stakken ud hvis
noget går galt ?





Carsten Agger (25-05-2003)
Kommentar
Fra : Carsten Agger


Dato : 25-05-03 08:25


"Per Abrahamsen" <abraham@dina.kvl.dk> skrev i en meddelelse
news:rjhe8138q4.fsf@zuse.dina.kvl.dk...
> Bertel Lund Hansen <nospamfor@lundhansen.dk> writes:
>
> > Er assert ikke en dyr måde at programmere på?
>
> Det er dyrt _ikke_ at bruge asserts. Fejl opdages senere.
>
(... )
> Jeg compiler nu aldrig med _NDEBUG, heller ikke i released kode, det
> er ufatteligt meget bedre at få fejlrapport fra en bruger i stil med
> "programmer siger
>
> file.c:101: assertion 'x > 0' failed
>
> end "Programmet siger 'ulovlig undtagelse' eller værre "programmet
> giver forkerte svar".

Undskyld jeg siger det, men hvis dit program er så performance-kritisk,
at man er nødt til at spare på simple operationer som addition o.l. i
en funktion som den, du sendte, ville du da (afhængig af compileren)
kunne spare en hel del ved at slå debug-information fra i release mode
og i stedet lave en hensigtsmæssig fejlhåndtering/logning?

vh
Carsten Agger
(der nu ikke, må jeg tilstå, har nogen særlig erfaring med realtidssystemer,
som jeg antager det må dreje sig om - min egen erfaring ligger hovedsaglig
indenfor størrre administrative systemer).

--
FAKLENS NYHEDSTJENESTE:
http://www.faklen.dk/nyheder




Per Abrahamsen (11-05-2003)
Kommentar
Fra : Per Abrahamsen


Dato : 11-05-03 22:16

"Mogens Hansen" <mogens_h@dk-online.dk> writes:

> Med Intel C++ V6.0 (til IA32) bliver "pos" kun refereret een gang pr. kald
> til funktionen, hvilket kun udgør en meget lille del af den samlede tid.
> Det sker i starten af funktionen, når værdien bliver lagt en i et processor
> register (på FPU stakken) og bliver der under hele funktionens kørsel.

Det er kun fordi funktionen er så simpel, og ikke selv kalder en
eneste "ekstern" funktion. Ellers ville compileren blive nødt til at
antage at den eksterne funktion kunne have ændre på pos.

Per Abrahamsen (12-05-2003)
Kommentar
Fra : Per Abrahamsen


Dato : 12-05-03 19:22

"Troels Thomsen" <troels.thomsen@mailteledk> writes:

>> Jeg compiler nu aldrig med _NDEBUG, heller ikke i released kode, det
>> er ufatteligt meget bedre at få fejlrapport fra en bruger i stil med
>>
>
> God pointe, men man mangler tit kald stakken for at finde ud af i hvad der
> gik galt.
> "Assertion hDc > 0 failed" siger ikke meget.

Man har også tit brug for værdier for lokale og globale variable.

> GCC har garanteret en switch der gør at den skriver kald stakken ud hvis
> noget går galt ?

Hvis man bruger Unix ja, det hedder et "core dump".

Per Abrahamsen (25-05-2003)
Kommentar
Fra : Per Abrahamsen


Dato : 25-05-03 18:02

"Carsten Agger" <agger@post8.tele.dk> writes:

> Undskyld jeg siger det, men hvis dit program er så performance-kritisk,
> at man er nødt til at spare på simple operationer som addition o.l. i
> en funktion som den, du sendte, ville du da (afhængig af compileren)
> kunne spare en hel del ved at slå debug-information fra i release mode
> og i stedet lave en hensigtsmæssig fejlhåndtering/logning?

Det er lidt uklart hvad du mener.

GCC genererer identisk kode hvad enten man slår debug information fra
eller til. De ekstra informationer ligger i et segment af filen der
ikke bliver loaded ved kørsel, men som en debugger kan læse. Jeg har
det altid slået til for eget brug, men stripper det fra inden jeg
sender det ud til kunderne, fordi de foretrække at skulle hente 8 Mb
(uden debug information) fremfor 32 Mb (med debug information).

Assertions er ortogonale til compiler genereret debug information, og
de koster altid noget når de er slået til. Men hvis man undlader at
placere dem sine aller-inderste løkker, er prisen ubetydeligt. Men
fordi mange alligevel er bange for den pris, er der en _NDEBUG flag
man kan sætte så de helt forsvinder.

Visual Studio har som default to modes, et med debug-information og
assertions, og et uden. Men compileren ser det som to forskellige
flag, og det er trivielt at gå ind og sætte hvilke flag de to modes
skal sætte, så man kan sagtens have assertions on i debug mode.

Det generelle råd er at folk skal brge assertions alle steder de mener
at vide noget om hvordan verden ser ud, compile med og uden _NDEBUG,
og se om forskellen er målelig. Det er den normalt ikke, og hvis den
er kan det normalt klares ved at fjerne enkelte assertion fra indre
løkker.

Det bedste er selvfølgelig at undgå dem ved at checke input, men det
aktuelle program består af koblede ligninger der beskriver fysiske,
kemiske og biologiske processer, nogen af dem med numeriske løsninger
der ikke altid er lige stabile, og ikke alle processor er lige godt
modeleret, og mange har et begænset gyldighedsområde. Så jeg checker
ved input at hver parameter giver fysisk mening (ingen negative masser
eller, ingen nuller i empiriske parametre der bliver brugt i division,
og ligninende, jeg checker for nogen processor at parametrene passer
sammen internt i modellen, og enkelte gange checker jeg at der ikke
bliver sat inkompatible modeller sammen. Men for masser af ting er
den eneste måde at checke for om parametrene er gyldige, at køre
simulationen. Og så have masser af asserts for hver model, så jeg
ikke pludselig begynder at regne på data leveret fra en anden process
der er ugyldige for den model.

For de steder hvor der i praksis kommer problemer i bestemte
situationer, giver jeg en klar fejlmeddelese om hvad der gik galt.
For alle andre kalder jeg min egen assert, der skriver beskeden i en
log fil, og lukker pænt ned. Så må jeg i samarbejde med brugeren
finde frem til hvad der førte til den "umulige" situation, og hvad der
skal gøres ved det.

> vh
> Carsten Agger
> (der nu ikke, må jeg tilstå, har nogen særlig erfaring med realtidssystemer,
> som jeg antager det må dreje sig om - min egen erfaring ligger hovedsaglig
> indenfor størrre administrative systemer).

Det er ikke et realtidssystem, det er en numerisk simulation. Vi har
lige kørt en række simuleringer af 1200 års varighed (simuleret tid),
hvis systemet var real-time ville vi være nødt til at satse på store
fremskridt i medicinsk teknologi for nogensinde at få resultaterne at
se.

Soeren Sandmann (09-05-2003)
Kommentar
Fra : Soeren Sandmann


Dato : 09-05-03 18:57

Per Abrahamsen <abraham@dina.kvl.dk> writes:

> Den implementerer en stykvis lineær funktion, hvor x og y indeholder
> en række punkter, og der interpoleres lineært mellem punkterne.

Hvad med at opbevare punkterne i et std::map<double, double>, og så
bruge ::upper_bound() til at finde de to punkter der skal interpoleres
mellem?

Det vil give dig garanteret O(log n) tid, hvorimod den binære søgning
i rutinen du postede har en kompleksitet som afhænger af fordelingen
af punkternes x-værdier.

Soeren Sandmann (09-05-2003)
Kommentar
Fra : Soeren Sandmann


Dato : 09-05-03 19:11

Soeren Sandmann <sandmann@daimi.au.dk> writes:

> Det vil give dig garanteret O(log n) tid, hvorimod den binære søgning
> i rutinen du postede har en kompleksitet som afhænger af fordelingen
> af punkternes x-værdier.

Det er ikke rigtigt. Din rutine er også O(log n), for den slipper af
med halvdelen af kandidatpunkterne for hver iteration. Så det er ikke
nødvendigvis bedre at opbevare punkterne i et std::map<>.

Thomas Krog (10-05-2003)
Kommentar
Fra : Thomas Krog


Dato : 10-05-03 00:22

> Den implementerer en stykvis lineær funktion, hvor x og y indeholder
> en række punkter, og der interpoleres lineært mellem punkterne.

Hvis værdierne i array x ligger nogenlunde jævnt fordelt kunne du vælge at
nøjes med et enkelt array yn:
for(pos=first; pos < last; pos+=delta)
yn[(pos-first)/delta] = PLF::Implementation()(pos);

Det koster ekstra ram tilgengæld kan den kritiske funktion køre i konstant
tid:

double
PLF::Implementation:erator () (const double pos) const{
return yn[(pos-first)/delta]; // uden lineær interpolation
}

Desuden kræver metoden at punkterne ikke bliver ændret alt for meget (men så
længe det blot er et par punkter der ændres koster det ikke meget at
opdatere yn).

Hvis værdierne i array x ligger i klumper ville jeg nok forsøge at udnytte
pipelinen noget bedre. Der opstår cache-miss fordi funktionen springer rundt
i arrayet og de mange if-sætninger gør det også svært for pipelinen.
Problemet kunne løses ved fx. at interpolere 10 punkter samtidig.
Hvorvidt det er mest smart at gøre det med en enkelt tråd eller flere skal
jeg ikke gøre mig klog på.



Thomas Krog (10-05-2003)
Kommentar
Fra : Thomas Krog


Dato : 10-05-03 00:33

> Hvis værdierne i array x ligger nogenlunde jævnt fordelt kunne du vælge at
> nøjes med et enkelt array yn:
> for(pos=first; pos < last; pos+=delta)
> yn[(pos-first)/delta] = PLF::Implementation()(pos);

I ovenstående special-tilfælde kan kaldet af PLF::Implementation:erator()
også udføres i konstant tid hvis man husker hvilket index man er kommet til
i array x



Mogens Hansen (10-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 10-05-03 11:09

Jeg tror kun at det kan gøres lidt hurtigere, mod at koden bliver lidt
grimmere.
Jeg kan ikke se at algoritmen kan forbedres, medmindre værdierne har nogle
særlige egenskaber der kan udnyttes.

Kan man skære ned i antallet af gange den bliver kaldt, ved at ændre
algoritme på et højere niveau ?
Det er sikkert der den største mulighed for optimering ligger.

Jeg har målt på et fast dataset, bestående af 1000 tilfældige tal på en IA32
maskine.
Målingerne er naturligvis meget afhængige af compiler og CPU arkitektur.
Programmet er oversat med Microsoft Visual C++ V6.0, og målt med en sampling
profiler på en lang kørsel.

Min måle reference er meget tæt på hvad du postede (de små ændringer skyldes
ikke performance, men personlig preference i kode stilen):
<code>
double interpolate_1(const double pos)
{
int min = 0;
int max = x.size()-1;

if(pos <= x[min])
return y[min];

if(pos >= x[max])
return y[max];

while(1 != (max - min)) {
const int guess = (max+min)/2;

if(x[guess] < pos)
min = guess;
else if(x[guess] > pos)
max = guess;
else
return y[guess];
}

return y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);
}
</code>

Sammenligningen
while(1 != (max - min)) {
tager knap 10 % af tiden.
Ved at udnytte at inde i løkken tildeles "max" ca. halvt så ofte som "min"
kan man gøre betigelsen i løkken simplere:
while(max_minus_one != min) {
// ...
else if(x[guess] > pos)
max_minus_one = (max = guess) -1;
// ...

Det giver noget der ligner 5-7 % forbedret hastighed.

I et forsøg på at udnytte CPU'ens flere pipelines
if(x[guess] < pos)
min = guess;
else {
max_minus_one = (max = guess) -1;
if(x[guess] == pos)
return y[guess];
}
udnyttes at der som regel gælder at "x[guess] != pos".
Ved at optimere for det mest almindelige tilfælde når ikke "x[guess] <
pos"og tildele "max" uden yderligere test og derefter teste på om "x[guess]
== pos" kan tildelingen og testen måske foregå parallelt på CPU niveau.

Det giver en performance forbedring på godt 1%.

Jeg prøvede at cache "x[guess]" for at spare beregningen af adressen, men
det giver en performance degradering, fordi "x[guess]" kun bliver læst een
gang fra hukommelsen og lagt på FPU stakken, hvor den så bliver brugt 2
gange.

Et forbedret bud er således:
<code>
double interpolate_4(const double pos)
{
int min = 0;
int max = x.size()-1;

if(pos <= x[min])
return y[min];

if(pos >= x[max])
return y[max];

int max_minus_one = max - 1;

while(max_minus_one != min) {
const int guess = (max+min)/2;

if(x[guess] < pos)
min = guess;
else {
max_minus_one = (max = guess) -1;

if(x[guess] == pos)
return y[guess];
}
}

return y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);
}
</code>

Er "x" og "y" data-medlemmer i en klasse ?
Hvis de er, kan det så svare sig at cache en reference til "x", for at spare
adresse beregning ?

Når man kigger på den generede assembler kode fra
if(x[guess] < pos)
//..
else if(x[guess] > pos)
// ..
bliver der lavet 2 floating point compare, hvor man egentlig kunne nøjes med
1 og derefter lave 2 betingede jump (f.eks. jae og jbe).

Linien
else if(x[guess] > pos)
står for ca 15 % af eksekveringstiden.
Mit bud er at ca. 10 % af eksekveringtiden her ville kunne spare, hvis man
kunne nøjes med 1 compare.
Det kan jeg dog ikke set er muligt i C++ - medmindre compileren kan gøre
det, og det kan mine ikke.

Derfor er et seriøst bud, hvis funktionen virkelig er kritisk og selv små
forbedringer er nødvendige, at skrive den i assembler.
Samlet set ville jeg forvente en 10-20 % forbedring, mod at det ikke er
portabelt.
Hvis det er på en IA32 maskine kan Intel VTune profileren være en utrolig
god hjælp til at hjælpe med optimere for at holde gang i pipelines etc.


Da koden laver binær søgning, vil manuel loop-unrolling kune minimere
antallet af gennemløb af løkken sættes meget ned, og koden gøres mere
lineær.

<code>
double interpolate_5(const double pos)
{
int min = 0;
int max = x.size()-1;

if(pos <= x[min])
return y[min];

if(pos >= x[max])
return y[max];

while(true) {
int guess = (max+min)/2;
switch(max-min) {
case 32:
case 31:
case 30:
case 29:
case 28:
case 27:
case 26:
case 25:
case 24:
case 23:
case 22:
case 21:
case 20:
case 19:
case 18:
case 17:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
// fall through

case 16:
case 15:
case 14:
case 13:
case 12:
case 11:
case 10:
case 9:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
// fall through

case 8:
case 7:
case 6:
case 5:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
// fall through

case 4:
case 3:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
// fall through

case 2:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
// fall through

case 1:
return y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);

default:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;

(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;

(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;

(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;

(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
}
}
}
</code>
Det kører på på ca. 80 % af den oprindelige kodes køre tid.
Det sidste niveau af loop-unrolling ser ikke ud til at give væsentlige
forbedringer for mine testdata.
Koden klarer sig med max n gennemløb for max 32^n datapunkter.
Bemærk iøvrigt at koden ikke er tilstrækkeligt godt testet.
Koden bør testes _meget_ grundigt inden det sættes i produktion.

Så måske med manuel loop-unrolling i assembler kan man komme ned på 60-70%
af den oprindelige kodes køre tid

Intel C++ V6.0 ser iøvrigt ud til at kunne køre funktionen på ca. 97 % af
den tid MSVC 6.0 bruger begge med almindelig release style, og en lille
smule hurtigere når der bruges "profile guided optimization".

Venlig hilsen

Mogens Hansen











Ulrik Magnusson (10-05-2003)
Kommentar
Fra : Ulrik Magnusson


Dato : 10-05-03 11:15



Mogens Hansen wrote:

> Jeg prøvede at cache "x[guess]" for at spare beregningen af adressen, men
> det giver en performance degradering, fordi "x[guess]" kun bliver læst een
> gang fra hukommelsen og lagt på FPU stakken, hvor den så bliver brugt 2
> gange.

Det var altså et tip man kunne bruge til noget! Amatører som mig ville ellers
foreslå netop det (og undrede sig over dets fravær).

Ulrik Magnusson


Mogens Hansen (10-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 10-05-03 11:49


"Ulrik Magnusson" <ulrikm@yahoo.com> wrote

[8<8<8<]
> Det var altså et tip man kunne bruge til noget! Amatører som mig ville
ellers
> foreslå netop det (og undrede sig over dets fravær).

Det var også det første jeg prøvede

1. regel for performance optimering:
Mål før og efter ændringen, for at være sikker på hvorvidt det er en
optimering.

2 regel for performance optimering:
De flestes intuition om hvor tiden bruges er utrolig dårlig.
Stol kun på målinger

Det gælder så afgjort for mig.

Det er iøvrigt ikke helt trivielt at lave et testprogram, der giver en
stabil performance måling.
Målingerne blev lavet i et program, hvor forskellige varianter af funktionen
så man på een måling kunne sammenligne forskellige bud på optimeringer.
Hver performance måling skulle mindst køre 10 minutter for at give et
stabilt resultat, fordi jeg brugte en sampling profiler.

Venlig hilsen

Mogens Hansen



Simon Strandgaard (10-05-2003)
Kommentar
Fra : Simon Strandgaard


Dato : 10-05-03 12:23

On Sat, 10 May 2003 13:49:12 +0200, Mogens Hansen wrote:
>
> 2 regel for performance optimering:
> De flestes intuition om hvor tiden bruges er utrolig dårlig.
> Stol kun på målinger
>
> Det gælder så afgjort for mig.

Enig, jeg har tilsvarende haft rutiner som skulle
optimeres til døde. Til dette lavede jeg først et test-framework og derpå
gik jeg så igang med at optimere.

Det Test-framework jeg lavede, testede både ydelse og om uddata stemte
overens med det forventede uddata.

intuitionen snyder ofte

--
Simon Strandgaard



Mogens Hansen (10-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 10-05-03 21:10


"Mogens Hansen" <mogens_h@dk-online.dk> wrote

[8<8<8<]
> Når man kigger på den generede assembler kode fra
> if(x[guess] < pos)
> //..
> else if(x[guess] > pos)
> // ..
> bliver der lavet 2 floating point compare, hvor man egentlig kunne nøjes
med
> 1 og derefter lave 2 betingede jump (f.eks. jae og jbe).

Idet jeg antager, ud fra typen af "pos", at typen for "x" er
"vector<double>" (og ikke "vector<int>"), og at værdien af "pos" er
tilfældig i forhold til værdierne, der er lagret i "x", vil det være
sjældent at betingelsen "x[guess] == pos" er opfyldt.
Derfor kan denne test helt undlades, og lade interpolationsberegningen klare
ærterne til sidst i alle tilfælde.

Det fil sige at løkken bliver til:

while(1 != (max - min)) {
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
}

og det kan kombineres med manuel loop-unrolling, så man får
<code>
double interpolate_5(const double pos)
{
int min = 0;
int max = x.size()-1;

if(pos <= x[min])
return y[min];

if(pos >= x[max])
return y[max];

while(true) {
int guess;
switch(max-min) {
case 32:
case 31:
case 30:
case 29:
case 28:
case 27:
case 26:
case 25:
case 24:
case 23:
case 22:
case 21:
case 20:
case 19:
case 18:
case 17:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 16:
case 15:
case 14:
case 13:
case 12:
case 11:
case 10:
case 9:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 8:
case 7:
case 6:
case 5:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 4:
case 3:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 2:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 1:
return y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);

default:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
}
}
}
</code>

Under testforhold, hvor "pos" er tilfældig i forhold til værdierne i "x"
kører denne version på 67 % af den oprindelig udgaves kørselstid, når det
oversættes med Intel C++ V6.0.
Under kørselsforhold, hvor såvel "pos" værdierne i "x" egentlig er et heltal
(konverteret til double), og der i ca. 10% af tilfældende findes "pos" i
"x", kører denne version på 67 % af den oprindelige udgaves kørselstid, når
det oversættes med Intel C++ V6.0.

Med Microsoft Visual C++ V6.0 kører det på 89 % af den oprindelig udgaves
kørselstid.

Når jeg kigger på den generede assembler kode fra Intel C++, kan jeg ikke se
at det kan gøre meget bedre, så det er droppet at skrive det i hånden som
assembler.


Venlig hilsen

Mogens Hansen



Mogens Hansen (11-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 11-05-03 07:04


"Mogens Hansen" <mogens_h@dk-online.dk> wrote

[8<8<8<]
> Når jeg kigger på den generede assembler kode fra Intel C++, kan jeg ikke
se
> at det kan gøre meget bedre

Og dog ...

Beregningen
guess = (max+min)/2
er på assembler niveau lidt mere kompliceret end nødvendigt, pga. håndtering
af fortegn.

Men "min", "max" og "guess" er aldrig negative, så ved at gøre den
"unsigned", gå man fra 65% til 55% af den oprindelige køretid.
Ved at markere dem som "register", falder kørselstiden lidt yderligere til
55%.
Oversat med Intel C++ V6.0 med profile guided optimization og maksimal
optimering.

Venlig hilsen

Mogens Hansen



Per Abrahamsen (11-05-2003)
Kommentar
Fra : Per Abrahamsen


Dato : 11-05-03 22:19

Jeg dropper

else if(x[guess] > pos)

delen, bruger unsigned int, og prekalkulererer slope som Anders
J. Munch foreslår. Tak for rådene.

Men jeg tror jeg vil overlade det til compileren at lave loop
unrolling. Prisen i læsbarhed er for høj.

Mogens Hansen (13-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 13-05-03 21:45


"Per Abrahamsen" <abraham@dina.kvl.dk> wrote
> Jeg dropper
>
> else if(x[guess] > pos)
>
> delen, bruger unsigned int, og prekalkulererer slope som Anders
> J. Munch foreslår.

Ok.
Jeg går ud fra din kode ligner noget i retningen af:
<code>
double interpolate_4(const double pos)
{
register unsigned min = 0;

if(pos <= x[min])
return y[min];

register unsigned max = x.size()-1;

if(pos >= x[max])
return y[max];

while(1 != (max - min)) {
register unsigned guess;
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
}

return y[min] + slope[min]*(pos-x[min]);
}
</code>

Har du nogen tal for hvilken betydning det har for performance for den
samlede applikation ?

> Tak for rådene.

Du er velkommen.

> Men jeg tror jeg vil overlade det til compileren at lave loop
> unrolling. Prisen i læsbarhed er for høj.

1. Viden om hvor meget tid de enkelte linier i funktionen er interessant
2. Læsbarheden kan forbedres væsentligt, i forhold til hvad jeg tidligere
viste
3. Med loop unrolling får man, ifølge mine målinger, altid bedre performance
end med prekalkuleret slope
4. Loop unrolling og prekalkuleret slope kan kombineres
5. Gevinsten ved loop unrolling er fast, mens gevinsten (eller overhead) ved
prekalkuleret slope afhænger af hvor mange gange funktionen kaldes på et
givent dataset
6. Loop unrolling ændrer ikke på interfacet til funktionen. Prekalkuleret
slope kræver af man på et tidspunkt siger at der ikke kommer flere punkter
og kan så foretager prekalkuleringen.
7. Jeg har ikke set at nogen af compilerne faktisk laver loop unrolling på
dette eksempel

Ad 1.

Følgende viser hvor stor en procent-del af funktionens samlede tid, der
bruges i de enkelte linier.
<tid fordelt på linier>
1%: double interpolate_3(const double pos)
2%: {
register unsigned min = 0;

2%: if(pos <= x[min])
0%: return y[min];

1%: register unsigned max = x.size()-1;

1%: if(pos >= x[max])
0%: return y[max];

9%: while(1 != (max - min)) {
register unsigned guess;
51%: (x[guess = (max+min)/2] < pos ? min : max ) = guess;
}

33%: return y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);
}
</tid fordelt på linier>

Det ses at beregningen af resultatet tager ca. halv så meget tid som løkken.

Ad 2.
Naturligvis giver manuel loop unrolling mindre læsevenlig kode - men det
gælder vel for enhver performance optimering, i forhold til den mest oplagte
funktion.
En mere læsbar funktion ved loop unrolling er
<code>
double interpolate_5(const double pos)
{
register unsigned min = 0;

if(pos <= x[min])
return y[min];

register unsigned max = x.size()-1;

if(pos >= x[max])
return y[max];

while(true) {
register unsigned guess;
switch(max - min) {
case 1:
return y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);

default:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 16: case 15: case 14: case 13: case 12: case 11:case 10: case 9:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 8: case 7: case 6: case 5:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 4: case 3:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 2:
(x[guess = (max+min)/2] < pos ? min : max ) = guess;
// fall through

case 0: ;
}
}
}
</code>

Ad 3, 4, 5.

Ved at sætte nogle formler op for de 4 kombinationer af prekalkulering og
loop unrolling, kan må få et mere præcist indtryk af gevindsten af
optimeringerne.

Hvis vi har følgende parametre:
*calc
tiden bruges på at beregne resultatet med formlen
y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);

* slope_calc
tiden der bruges på at beregne resultatet med formlen
y[min] + slope[min]*(pos-x[min]);
men hvor tiden til at fylde "slope" ikke er medregnet

*loop
tiden der bruges i starten af funktionen og i den normale løkke

*unroll_loop
tiden der bruges brug i starten af funktionen og i den unrollede løkke

* call_count
er antallet af gange som beregningen bliver kaldt

* x_size
er antallet af punkter der skal interpoleres mellem

* pre_calc
tiden bruges på at fylde "slope".
Når call_count == x_size antages det at der er break-even for prekaleret
slope.
Det vil sige at pre_calc = calc-slope_calc

Så vil den samlede tid i hver af de 4 varianter være givet ved

perf_1 = loop + calc
perf_2 = loop + slope_calc + x_size/call_count*pre_calc
perf_3 = unroll_loop + calc
perf_4 = unroll_loop + slope_calc + x_size/call_count*pre_calc

når prisen for "pre_calc" fordeles ud over "call_count".

Idet "perf_1" sættes til 100, fås ca. følgende værdier for Intel C++ V6.0
loop = 67
unroll_loop = 42
calc = 33
slope_calc = 13
pre_calc = 20
hvilket giver
perf_1 = 100
perf_2 = 80 + x_size/call_count*20
perf_3 = 75
perf_4 = 55 + x_size/call_count*20

Tilsvarende fås ca. følgende værdier for Microsoft Visual C++ V6.0
loop = 70
unroll_loop = 51
calc = 30
slope_calc = 12
pre_calc = 18
hvilket giver
perf_1 = 100
perf_2 = 82 + x_size/call_count*18
perf_3 = 81
perf_4 = 63 + x_size/call_count*18

Venlig hilsen

Mogens Hansen



Thomas Krog Christen~ (13-05-2003)
Kommentar
Fra : Thomas Krog Christen~


Dato : 13-05-03 23:50

> * pre_calc
> tiden bruges på at fylde "slope".
> Når call_count == x_size antages det at der er break-even for prekaleret
> slope.
> Det vil sige at pre_calc = calc-slope_calc

Jeg vil dog mene den antagelse er lidt pessimistisk (jeg vil tro pre_calc <
calc-slope_calc). De mange if-sætninger og cache-miss i løkken giver pipelinen
dårlige vilkår til at lave noget samtidig med divisionen.

Derimod vil en "forudberegnings-løkke" ala
for(...) slope[i]=...;
give pipelinen perfekte vilkår (ganske få cache-miss og afhængigheder mellem
beregningerne)

Desuden vil jeg tro at slope arrayet er noget mere effektivt hvis det bliver
slået sammen med array y (dvs. et array af tuppler). Mon ikke det fjerner et
cache-miss pr kald af funktionen interpolate? (forudsat punkterne fylder meget
i forhold til cachens størrelse - eller at funktionen kaldes få gange)


Mogens Hansen (14-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 14-05-03 13:48


"Thomas Krog Christensen" <c973526@student.dtu.dk> wrote

> > * pre_calc
> > tiden bruges på at fylde "slope".
> > Når call_count == x_size antages det at der er break-even for prekaleret
> > slope.
> > Det vil sige at pre_calc = calc-slope_calc
>
> Jeg vil dog mene den antagelse er lidt pessimistisk (jeg vil tro pre_calc
<
> calc-slope_calc). De mange if-sætninger og cache-miss i løkken giver
pipelinen
> dårlige vilkår til at lave noget samtidig med divisionen.
>
> Derimod vil en "forudberegnings-løkke" ala
> for(...) slope[i]=...;
> give pipelinen perfekte vilkår (ganske få cache-miss og afhængigheder
mellem
> beregningerne)

Det kan der sikkert være noget om.

I udtrykket
slope[i] = (y[i+1]-y[i])/(x[i+1]-x[i]);
vil y[i] og x[i] formodentligt være ligge i cache fra foregående gennemløb,
så tilgangen vil være næsten gratis.
Til gengæld skal resultatet lagres i slope[i], som giver en ekstra
hukommeles tilgang.

Desuden skal det siges at det dataset jeg har lavet målingerne på, har 1000
elementer.
Hvert element er af typen "double", som konkret fylder 8 bytes.
Altså er der med der uden prekalkulering ca. 16 kbyte data og med
prekalkulering ca. 24 kbyte data.
Jeg kan ikke huske konkret hvor meget L1 og L2 cache mine CPU'er har, men
det lyder som en datamængde der kan ligge i L1 og helt sikkert i L2.
Performance målingen laver mindst 50.000.000 interpolationer, hvor der slåes
op i de samme 16-24 kbyte.
Det er naturligvis interessant at se hvordan det opfører sig når man krydser
størrelsen på L1 og L2 - men det bliver nok en anden gang.

Man kunne eventuelt gøre prekalkuleringen hurtigere med manuel loop
unrolling


En måling hvor call_count == x_size viser med Visual C++ V6.0 at
pre_calc = 14
hvor jeg havde estimeret det til 18, og med Intel C++ V6.0 at
pre_calc = 18
hvor jeg havde estimeret det til 20.
Så du har ret i at
pre_calc < calc-slope_calc

> Desuden vil jeg tro at slope arrayet er noget mere effektivt hvis det
bliver
> slået sammen med array y (dvs. et array af tuppler). Mon ikke det fjerner
et
> cache-miss pr kald af funktionen interpolate? (forudsat punkterne fylder
meget
> i forhold til cachens størrelse - eller at funktionen kaldes få gange)

Et fair forsøg på at lave en måling viser stor set ingen forskel - vi
snakker promiller.
I den situation hvor der ikke bruges loop unrolling er det hurtigst uden
tuppler, og med loop unrolling er det hurtist med tuppler, men i ingen af
tilfældende noget som ikke kan være måleusikkerhed.

Venlig hilsen

Mogens Hansen



Thomas Krog Christen~ (14-05-2003)
Kommentar
Fra : Thomas Krog Christen~


Dato : 14-05-03 16:30

>
>
>I udtrykket
> slope[i] = (y[i+1]-y[i])/(x[i+1]-x[i]);
>vil y[i] og x[i] formodentligt være ligge i cache fra foregående gennemløb,
>så tilgangen vil være næsten gratis.
>Til gengæld skal resultatet lagres i slope[i], som giver en ekstra
>hukommeles tilgang.
>
>
Jeg troede egentlig at trafikken mellem ram og cache foregik i blokke på
fx en kb. Dvs. hvis man vil læse y[0] så bliver de første fx. 128
elementer fra y kopieret fra ram til cache. Tilsvarende hvis man vil
skrive til slope[0] så bliver de første 128 elementer fra slope kopieret
fra ram til cache. På den måde koster det kun to cache-miss (der skal
også bruges en til at kopiere tilbage til ram) at skrive til de første
128 værdier fra slope. Er det forkert forstået?

>Man kunne eventuelt gøre prekalkuleringen hurtigere med manuel loop
>unrolling
>
Jeg havde egentlig håbet at oversætteren selv kunne gøre det når nu
loopet er så simpelt. Altså omskrive det til noget ala:
for(...;i+=5){
i1=i+1;
i2=i+2;
....
dy0 = y[i1]-y[i];
dx0 = x[i1]-x[i];
dy1 = y[i2]-y[i1];
dx1 = x[i2]-x[i1];
dy2 = y[i3]-y[i2];
dx2 = x[i3]-x[i2];
....
slope[i] =dy0/dx0;
slope[i1] = dy1/dx1;
slope[i2] = dy2/dx2;
....
}

>
>
>En måling hvor call_count == x_size viser med Visual C++ V6.0 at
> pre_calc = 14
>hvor jeg havde estimeret det til 18, og med Intel C++ V6.0 at
> pre_calc = 18
>hvor jeg havde estimeret det til 20.
>Så du har ret i at
> pre_calc < calc-slope_calc
>
Jeg troede der var noget større forskel - men det er måske fordi
oversætterne er ret dårlige til at unrolle.

>Et fair forsøg på at lave en måling viser stor set ingen forskel - vi
>snakker promiller.
>I den situation hvor der ikke bruges loop unrolling er det hurtigst uden
>tuppler, og med loop unrolling er det hurtist med tuppler, men i ingen af
>tilfældende noget som ikke kan være måleusikkerhed.
>
>
>
Tror dog forskellen ville være større hvis der var flere punkter - eller
i et program hvor cachen også bliver brugt til andre ting end at køre
interpolate.


Mogens Hansen (15-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 15-05-03 15:04


"Thomas Krog Christensen" <c973526@student.dtu.dk> wrote

[8<8<8<]
> Jeg troede egentlig at trafikken mellem ram og cache foregik i blokke på
> fx en kb. Dvs. hvis man vil læse y[0] så bliver de første fx. 128
> elementer fra y kopieret fra ram til cache. Tilsvarende hvis man vil
> skrive til slope[0] så bliver de første 128 elementer fra slope kopieret
> fra ram til cache. På den måde koster det kun to cache-miss (der skal
> også bruges en til at kopiere tilbage til ram) at skrive til de første
> 128 værdier fra slope. Er det forkert forstået?

Se
ftp://download.intel.com/design/Pentium4/manuals/24896608.pdf

Venlig hilsen

Mogens Hansen



Anders J. Munch (14-05-2003)
Kommentar
Fra : Anders J. Munch


Dato : 14-05-03 16:43

"Mogens Hansen" <mogens_h@dk-online.dk> skrev:
> ... måleusikkerhed.

Mogens, nu har du lavet alle de her målinger, men fik du nævnt hvilken
type cpu du har målt på? Jeg forestiller mig at konklusionerne kan
variere en hel del alt efter om man bruger en P4 eller en cpu med en
mere traditionel arkitektur.

Det at du har målt på pseudotilfældige tal vil formentlig være hård
kost for P4s branch predictor.

mvh. Anders



Mogens Hansen (14-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 14-05-03 20:14


"Anders J. Munch" <andersjm@dancontrol.dk> wrote

> Mogens, nu har du lavet alle de her målinger, men fik du nævnt hvilken
> type cpu du har målt på?

Nej, det har du ret i.
Alle målingerne er lavet på en gammel dual 333 MHz Celeron (overclocket til
413 MHz), som kører Windows 2000 Professional.

> Jeg forestiller mig at konklusionerne kan
> variere en hel del alt efter om man bruger en P4 eller en cpu med en
> mere traditionel arkitektur.

Jeg vil prøve at lave målingerne på
* 700 MHz Pentium III, Windows XP Professional
* Athlon XP 2200+, Windows XP Professional
* dual 2GHz Pentium IV Xeon, Windows XP Professional

Det tager noget tid at lave målingerne.

Jeg vil holde alle andre parametre end CPU konstante og kun prøve med Intel
C++ V6.0 compileren.
Ud fra målingerne vil jeg bestemme de samme parametre som tidligere nævnt:
* calc
* slope_calc
* loop
* unroll_loop
* pre_calc

> Det at du har målt på pseudotilfældige tal vil formentlig være hård
> kost for P4s branch predictor.

Det vil være interessant at undersøge.

Venlig hilsen

Mogens Hansen



Mogens Hansen (15-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 15-05-03 15:06

Her er målingerne på forskellige CPU typer:

(perf_0 er stort set den oprindelige funktion)

Dual Celeron (Pentium II), 413 MHz (PC100 RAM):
loop = 67
unroll_loop = 42
calc = 33
slope_calc = 14
pre_calc = 15

perf_0 = 129
perf_1 = 100
perf_2 = 81 + x_size/call_count*15
perf_3 = 75
perf_4 = 56 + x_size/call_count*15

Pentium III, 700 MHz (??? RAM)
loop = 67
unroll_loop = 42
calc = 33
slope_calc = 14
pre_calc = 15

perf_0 = 129
perf_1 = 100
perf_2 = 81 + x_size/call_count*
perf_3 = 75
perf_4 = 56 + x_size/call_count*

Athlon XP 2200+ (Dual channel DDR400 RAM)
loop = 78
unroll_loop = 73
calc = 22
slope_calc = 9
pre_calc = 9

perf_0 = 145
perf_1 = 100
perf_2 = 87 + x_size/call_count*9
perf_3 = 95
perf_4 = 82 + x_size/call_count*9

Dual Pentium IV Xeon, 2GHz (Rambus ??? RAM)
loop = 63
unroll_loop = 51
calc = 37
slope_calc = 13
pre_calc = 24

perf_0 = 154
perf_1 = 100
perf_2 = 76 + x_size/call_count*24
perf_3 = 88
perf_4 = 64 + x_size/call_count*24

For de ældre CPU'er (Pentium II og Pentium III) kan man se:
* De har mere glæde af loop unrolling end prekalkulering

For de nyere CPU'er (Athon XP og Pentium Xeon) kan man se:
* De har mere glæde af prekalkulering end loop unrolling

Venlig hilsen

Mogens Hansen




Per Abrahamsen (15-05-2003)
Kommentar
Fra : Per Abrahamsen


Dato : 15-05-03 19:00

"Mogens Hansen" <mogens_h@dk-online.dk> writes:

> Har du nogen tal for hvilken betydning det har for performance for den
> samlede applikation ?

Mindre end måleusikkerheden. Det er den funktion der bruger mest tid,
men det er stadig kun et par procent af den samlede køretid.

> 5. Gevinsten ved loop unrolling er fast, mens gevinsten (eller overhead) ved
> prekalkuleret slope afhænger af hvor mange gange funktionen kaldes på et
> givent dataset

De væsentlige PLF'er bliver kun oprettet en gang i løbet af en
simulering, så det er et konstant overhead, og performance er ret
uinteressant for korte simuleringer.

> 6. Loop unrolling ændrer ikke på interfacet til funktionen. Prekalkuleret
> slope kræver af man på et tidspunkt siger at der ikke kommer flere punkter
> og kan så foretager prekalkuleringen.

Jeg udregner det hver gang jeg tilføjer et punkt.

> 7. Jeg har ikke set at nogen af compilerne faktisk laver loop unrolling på
> dette eksempel

Jeg prøvede GCC 2.95.3 -O2 med og uden -funroll-all-loops, og koden
for den funktion var identitisk i de to tilfælde.


Mogens Hansen (15-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 15-05-03 19:19


"Per Abrahamsen" <abraham@dina.kvl.dk> wrote

[8<8<8<]
> Mindre end måleusikkerheden. Det er den funktion der bruger mest tid,
> men det er stadig kun et par procent af den samlede køretid.

Så hjælper det ikke alverden at optimere den funktion.

Venlig hilsen

Mogens Hansen



Per Abrahamsen (17-05-2003)
Kommentar
Fra : Per Abrahamsen


Dato : 17-05-03 12:14

"Mogens Hansen" <mogens_h@dk-online.dk> writes:

> "Per Abrahamsen" <abraham@dina.kvl.dk> wrote
>
> [8<8<8<]
>> Mindre end måleusikkerheden. Det er den funktion der bruger mest tid,
>> men det er stadig kun et par procent af den samlede køretid.
>
> Så hjælper det ikke alverden at optimere den funktion.

Næh, men profilen er flad nu. Så står den på hårdt arbejde og mange
små forbedringer for at få mere performance ud af systemet. Og
eftersom det var sådan en lille isoleret funktion der lå i toppen af
profilen var det oplagt lige at tage et kig på den.

Mogens Hansen (10-05-2003)
Kommentar
Fra : Mogens Hansen


Dato : 10-05-03 11:09

Jeg tror kun at det kan gøres lidt hurtigere, mod at koden bliver lidt
grimmere.
Jeg kan ikke se at algoritmen kan forbedres, medmindre værdierne har nogle
særlige egenskaber der kan udnyttes.

Kan man skære ned i antallet af gange den bliver kaldt, ved at ændre
algoritme på et højere niveau ?
Det er sikkert der den største mulighed for optimering ligger.

Jeg har målt på et fast dataset, bestående af 1000 tilfældige tal på en IA32
maskine.
Målingerne er naturligvis meget afhængige af compiler og CPU arkitektur.
Programmet er oversat med Microsoft Visual C++ V6.0, og målt med en sampling
profiler på en lang kørsel.

Min måle reference er meget tæt på hvad du postede (de små ændringer skyldes
ikke performance, men personlig preference i kode stilen):
<code>
double interpolate_1(const double pos)
{
int min = 0;
int max = x.size()-1;

if(pos <= x[min])
return y[min];

if(pos >= x[max])
return y[max];

while(1 != (max - min)) {
const int guess = (max+min)/2;

if(x[guess] < pos)
min = guess;
else if(x[guess] > pos)
max = guess;
else
return y[guess];
}

return y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);
}
</code>

Sammenligningen
while(1 != (max - min)) {
tager knap 10 % af tiden.
Ved at udnytte at inde i løkken tildeles "max" ca. halvt så ofte som "min"
kan man gøre betigelsen i løkken simplere:
while(max_minus_one != min) {
// ...
else if(x[guess] > pos)
max_minus_one = (max = guess) -1;
// ...

Det giver noget der ligner 5-7 % forbedret hastighed.

I et forsøg på at udnytte CPU'ens flere pipelines
if(x[guess] < pos)
min = guess;
else {
max_minus_one = (max = guess) -1;
if(x[guess] == pos)
return y[guess];
}
udnyttes at der som regel gælder at "x[guess] != pos".
Ved at optimere for det mest almindelige tilfælde når ikke "x[guess] <
pos"og tildele "max" uden yderligere test og derefter teste på om "x[guess]
== pos" kan tildelingen og testen måske foregå parallelt på CPU niveau.

Det giver en performance forbedring på godt 1%.

Jeg prøvede at cache "x[guess]" for at spare beregningen af adressen, men
det giver en performance degradering, fordi "x[guess]" kun bliver læst een
gang fra hukommelsen og lagt på FPU stakken, hvor den så bliver brugt 2
gange.

Et forbedret bud er således:
<code>
double interpolate_4(const double pos)
{
int min = 0;
int max = x.size()-1;

if(pos <= x[min])
return y[min];

if(pos >= x[max])
return y[max];

int max_minus_one = max - 1;

while(max_minus_one != min) {
const int guess = (max+min)/2;

if(x[guess] < pos)
min = guess;
else {
max_minus_one = (max = guess) -1;

if(x[guess] == pos)
return y[guess];
}
}

return y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);
}
</code>

Er "x" og "y" data-medlemmer i en klasse ?
Hvis de er, kan det så svare sig at cache en reference til "x", for at spare
adresse beregning ?

Når man kigger på den generede assembler kode fra
if(x[guess] < pos)
//..
else if(x[guess] > pos)
// ..
bliver der lavet 2 floating point compare, hvor man egentlig kunne nøjes med
1 og derefter lave 2 betingede jump (f.eks. jae og jbe).

Linien
else if(x[guess] > pos)
står for ca 15 % af eksekveringstiden.
Mit bud er at ca. 10 % af eksekveringtiden her ville kunne spare, hvis man
kunne nøjes med 1 compare.
Det kan jeg dog ikke set er muligt i C++ - medmindre compileren kan gøre
det, og det kan mine ikke.

Derfor er et seriøst bud, hvis funktionen virkelig er kritisk og selv små
forbedringer er nødvendige, at skrive den i assembler.
Samlet set ville jeg forvente en 10-20 % forbedring, mod at det ikke er
portabelt.
Hvis det er på en IA32 maskine kan Intel VTune profileren være en utrolig
god hjælp til at hjælpe med optimere for at holde gang i pipelines etc.


Da koden laver binær søgning, vil manuel loop-unrolling kune minimere
antallet af gennemløb af løkken sættes meget ned, og koden gøres mere
lineær.

<code>
double interpolate_5(const double pos)
{
int min = 0;
int max = x.size()-1;

if(pos <= x[min])
return y[min];

if(pos >= x[max])
return y[max];

while(true) {
int guess = (max+min)/2;
switch(max-min) {
case 32:
case 31:
case 30:
case 29:
case 28:
case 27:
case 26:
case 25:
case 24:
case 23:
case 22:
case 21:
case 20:
case 19:
case 18:
case 17:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
// fall through

case 16:
case 15:
case 14:
case 13:
case 12:
case 11:
case 10:
case 9:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
// fall through

case 8:
case 7:
case 6:
case 5:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
// fall through

case 4:
case 3:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
// fall through

case 2:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
// fall through

case 1:
return y[min] + (y[max]-y[min])/(x[max]-x[min])*(pos-x[min]);

default:
(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;

(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;

(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;

(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;

(x[guess] < pos ? min : max ) = guess;
if(x[guess] == pos)
return y[guess];
guess = (max+min)/2;
}
}
}
</code>
Det kører på på ca. 80 % af den oprindelige kodes køre tid.
Det sidste niveau af loop-unrolling ser ikke ud til at give væsentlige
forbedringer for mine testdata.
Koden klarer sig med max n gennemløb for max 32^n datapunkter.
Bemærk iøvrigt at koden ikke er tilstrækkeligt godt testet.
Koden bør testes _meget_ grundigt inden det sættes i produktion.

Så måske med manuel loop-unrolling i assembler kan man komme ned på 60-70%
af den oprindelige kodes køre tid

Intel C++ V6.0 ser iøvrigt ud til at kunne køre funktionen på ca. 97 % af
den tid MSVC 6.0 bruger begge med almindelig release style, og en lille
smule hurtigere når der bruges "profile guided optimization".

Venlig hilsen

Mogens Hansen











Anders J. Munch (11-05-2003)
Kommentar
Fra : Anders J. Munch


Dato : 11-05-03 13:17

"Per Abrahamsen" <abraham@dina.kvl.dk> skrev:
> Den funktion der bliver brugt mest tid i i mit program er
>
[...]

> if (max - min == 1)

En slutbetingelse på max==min sparer en instruktion pr. gennemløb, men
giver til gengæld et ekstra gennemløb. YMMV.

> return y[min]
> + (y[max] - y[min]) / (x[max] - x[min]) * (pos - x[min]);

Forudberegning af hældningen synes at ligge lige for.
return y[min] + haeldning[min] * (pos - x[min]);

Har du prøvet at samle x og y i ét punkt-array? Jeg ved ikke om det er
hurtigere, jeg vil gætte på at det ikke er det, men det kan dog
prøves.

For små size() kan komplet udfoldning af løkken med et template være
en mulighed; løkkebetingelsen forsvinder og alle array indices bliver
konstante. Den større kode spiller til gengæld ikke så godt sammen med
cachen, så om det bliver hurtigere eller langsommere er svært at
forudse.

mvh. Anders

--
Anders Munch | Stadig forvirret men på et højere niveau
Capon - a software build framework http://www.dancontrol.net/share/capon/




Søg
Reklame
Statistik
Spørgsmål : 177558
Tips : 31968
Nyheder : 719565
Indlæg : 6408924
Brugere : 218888

Månedens bedste
Årets bedste
Sidste års bedste