javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen

9 Optimering


9.1 Optimering 140

9.2 Ting, man skal undgå (hvis man kan) 140

9.2.1 Oprette mange objekter 140

9.2.2 Oprette mange tråde 141

9.2.3 Kaste og fange mange undtagelser 141

9.2.4 Gå ind og ud af synkroniserede blokke/metoder 141

9.2.5 Bruge mange (evt. anonyme) klasser 141

9.3 Er C++ hurtigere end Java? 142

9.3.1 Et testprogram i Java 143

9.3.2 Testprogrammet i C++ 143

9.4 Videre læsning 144

9.1 Optimering

Et programs ydeevne begrænses næsten altid af højst to-tre faktorer (flaskehalse).

Inden du læser videre, så bemærk lige en generel regel omkring optimering af ydelse:

Under programmeringen er det i de fleste tilfælde spild af tid at tænke på optimering - det er bedre at koncentrere sig om funktionaliteten og, når programmet er skrevet næsten helt færdigt, identificere flaskehalsene og optimere disse dele af koden

Optimering af kørselstiden bør dog ske tidligere, hvis:

9.2 Ting, man skal undgå (hvis man kan)

Man skal så vidt muligt undgå at:

9.2.1 Oprette mange objekter

Det tager både tid at oprette dem, og det tager også tid for den virtuelle maskine at finde og frigive objekter, der ikke mere er i brug. Genbrug, objekterne hvis det er muligt.

Eksempel: Streng-manipulation

Skal du sætte mange strenge sammen, bør du bruge en StringBuffer, da du så undgår at oprette de mange midlertidige String-objekter, der ellers ville opstå.

Følgende program demonstrerer den enorme hastighedsforskel, en StringBuffer kan gøre.

// Demonstrerer hastighedsforskellen mellem String og StringBuffer 
// ved sammensætning af mange strenge
public class HastighedsforskelMellemStringOgStringBuffer
{
  public static void main (String[] arg)
  {
    long tid1 = System.currentTimeMillis();

    String s = "";
    for (int i=0; i<10000; i++) s = s + "x";   // her oprettes 10000 objekter

    long tid2 = System.currentTimeMillis();
    System.out.println("Antal sekunder med String: "+ (tid2-tid1)*0.001 );

    StringBuffer sb = new StringBuffer(10000); // reservér plads til 10000 tegn
    for (int i=0; i<10000; i++) sb.append("x");// her ændres i det samme objekt
    String s2 = sb.toString();

    long tid3 = System.currentTimeMillis();
    System.out.println("Antal sek med StringBuffer: "+ (tid3-tid2)*0.001 );
  }
}

Antal sekunder med String: 3.432
Antal sek med StringBuffer: 0.021

Her sparer man altså over faktor 150 i kørselstid (0.02 i stedet for 3.4 sekunder).

Besparelsen skyldes, at der oprettes færre objekter (1 StringBuffer i stedet for 10000 strenge), og oprettelse (og oprydning af) objekter er en forholdsvis tidskrævende operation.

9.2.2 Oprette mange tråde

Tråde tager beslag på en del systemresurser, så opret ikke alt for mange, og genbrug dem, hvis det er muligt (se afsnit 16.7.3, Genbrug af tråde).

9.2.3 Kaste og fange mange undtagelser

Undtagelser er netop undtagelser, og den virtuelle maskine udfører programmet hurtigst, hvis de ikke opstår.

9.2.4 Gå ind og ud af synkroniserede blokke/metoder

Hver gang en tråd går ind i en blok/metode mærket med synchronized, skal den virtuelle maskine tjekke, om der allerede er en tråd aktiv i denne blok. Den får altså noget ekstra arbejde, som tager tid.

9.2.5 Bruge mange (evt. anonyme) klasser

Ved opstart indlæses alle klasserne, som programmet består af eller har brug for, og det tager selvfølgelig tid.

Overraskende nok (for nogen) ligger indre klasser og anonyme klasser ikke inde i den ydre klasse på filsystemet, men skal indlæses separat.

Bruger man derfor mange indre klasser eller anonyme klasser, f.eks. i forbindelse med programmering af grafiske brugergrænseflader, bør man overveje, om man kunne slå nogen af dem sammen.

Hvis det drejer sig om hændelses-lyttere, kunne man overveje at lade den ydre klasse lytte efter alle de forskellige hændelser og droppe alle de anonyme klasser som et GUI-udviklingsværktøj typisk laver.

Eksempel

I afsnit 4.1.3 Bruge en javabønne er der et eksempel på hvordan. Først er der et eksempel med en anonym ActionListener-klasse (der er altså 2 klasser, der skal indlæses):

import java.awt.*;

public class BenytBoenneMedVaerktoej extends Frame
{
  ...

  public BenytBoenneMedVaerktoej()
  {
    // anonym indre klasse lytter på hændelser og kalder derpå videre til
    // metoden textFieldNavn_actionPerformed()
    textFieldNavn.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        textFieldNavn_actionPerformed(e);
      }
    });
    ...
  }

  void textFieldNavn_actionPerformed(ActionEvent e) {
    String navn = textFieldNavn.getText();
    System.out.println("Navnet er: "+navn);
  }
}

Efterfølgende blev den anonyme klasse sparet væk ved at lade vinduet selv implementere ActionListener (der er altså kun 1 klasse, der skal indlæses):

import java.awt.*;

public class BenytBoenneSkrevetSelv extends Frame implements ActionListener
{
  ...

  public BenytBoenneSkrevetSelv() {
    // klassen selv lytter på hændelser
    textFieldNavn.addActionListener(this);
    ...
  }

  void actionPerformed(ActionEvent e) {
    String navn = textFieldNavn.getText();
    System.out.println("Navnet er: "+navn);
  }
}

9.3 Er C++ hurtigere end Java?

Det er en udbredt opfattelse, at C++ er hurtigere end Java, og at C++ derfor er mere velegnet til programmer, der skal foretage mange beregninger.

Det er imidlertid en myte, der slet ikke gælder for de nyere udgave af den virtuelle maskine (JDK 1.2 og derefter).

Et javaprogram laver beregninger nogenlunde lige så hurtigt (eller hurtigere) som et fuldt optimeret C++-program

Myten stammer fra 1990-erne, før de virtuelle maskiner fik implementeret JIT (Just In Time)-oversættelse. JIT virker ved at oversætte bytekoden til maskinkode, efterhånden som den skal udføres, sådan at der bruges noget tid på JIT-oversættelse første gang, noget bytekode udføres, derefter går det lige så hurtigt som maskinkode.

Almindelige C/C++-programmer (og Fortran- og Pascal-programmer for den sags skyld) bliver oversat til maskinkode én gang for alle, hvorefter de kan køres igen og igen.

Javaprogrammer bliver oversat til maskinkode af den virtuelle maskine under kørslen, hvilket naturligvis tager mere tid første gang, men i modsætning til C/C++-programmer kan JIT-oversætteren vælge at lave maskinkoden afhængig af, hvordan kørslen 'normalt' foregår.

9.3.1 Et testprogram i Java

Dette lille program finder alle primtal mellem 50000 og 100000. Der bruges ingen objekter:

public class Primtal
{
  public static void main(String[] args) {
    int antalPrimtal = 0;

    int tal;
    int faktor;

    for (tal = 50000; tal<100000; tal++)
    {
      faktor = 2;

      while (tal % faktor > 0) faktor++;

      if (faktor == tal)
      {
        System.out.print(tal + " er et primtal.\n");
        antalPrimtal = antalPrimtal + 1;
      }
    }
    System.out.println("Antal primtal i alt: " + antalPrimtal);
  }
}

Under Linux kan man undersøge, hvor lang tid en kommando tager med 'time'. For at tage tiden på ovenstående program skriver man:

time java Primtal

... hvorefter programmet kører:

50021 er et primtal.
50023 er et primtal.
...
99961 er et primtal.
99971 er et primtal.
99989 er et primtal.
99991 er et primtal.
Antal primtal i alt: 4459
32.16user 0.19system 0:36.13elapsed 89%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (5674major+2738minor)pagefaults 0swaps

Sidst udskrives kørselstiden. Dette program tager 32.16 sekunders CPU (plus 0.19 sekunders CPU-tid til systemkald).

9.3.2 Testprogrammet i C++

Lad os nu skrive det tilsvarende program i C++:

#include <iostream.h>

int main() {
  int antalPrimtal = 0;

  int tal;
  int faktor;

  for (tal = 50000; tal<100000; tal++)
  {
    faktor = 2;

    while (tal % faktor > 0) faktor++;

    if (faktor == tal)
    {
      cout<<tal<<" er et primtal.\n";
      antalPrimtal = antalPrimtal + 1;
    }
  }

  cout<<"Antal primtal i alt: "<<antalPrimtal<<"\n";
}

Lad sige, at kildeteksten ligger i filen prim.c++. Så oversætter man og kører programmet med:

g++ -O3 prim.c++
time a.out

Parameteren -O3 fortæller oversætteren, at den skal forsøge at optimere programmet så meget som overhovedet muligt (mildere indstillinger er -O og -O2). Oversætteren generer filen a.out med det eksekverbare program, som udføres og times samtidig:

time a.out

Uddata fra programmet er:

...
99929 er et primtal.
99961 er et primtal.
99971 er et primtal.
99989 er et primtal.
99991 er et primtal.
Antal primtal i alt: 4459
32.71user 0.08system 0:35.96elapsed 91%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (200major+27minor)pagefaults 0swaps

Selv med den mest ekstreme optimering af C++-programmet, klarer det sig altså lidt dårligere end Javaprogrammet (dog ikke ret meget - men uden optimering tager C++-programmet 37 sekunder at køre).

Forklaringen på, at Java i dette tilfælde er hurtigere end C++, skal nok findes i JIT'ens behandling af while-løkken og if-sætningen:

      while (tal % faktor > 0) faktor++;
      if (faktor == tal)

Eftersom C++-oversætteren under oversættelsen og optimeringen ikke kan vide, om betingelserne som regel er opfyldt eller ej, må den prøve at gætte - og den gætter sandsynligvis ofte forkert.

JIT'en kan derimod observere kørslen nogle gange og konstatere, at while-betingelsen oftest er opfyldt, mens if-sætningen meget sjældent er det, og generere maskinkode, der er optimeret derefter.

9.4 Videre læsning

javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen
http://javabog.dk/ - af Jacob Nordfalk.
Licens og kopiering under Åben Dokumentlicens (ÅDL) hvor intet andet er nævnt (71% af værket).

Ønsker du at se de sidste 29% af dette værk (362838 tegn) skal du købe bogen. Så får du pæne figurer og layout, stikordsregister og en trykt bog med i købet.