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
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:
Det forventes at forårsage større strukturelle ændringer i programmet. Optimeringer der forventes at indvirke på programmets struktur, bør ske i design-fasen, inden programmeringen påbegyndes.
Det forventes at give kortere udviklingstid (fordi de løbende afprøvninger af programmet kan udføres hurtigere).
Man skal så vidt muligt undgå at:
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.
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.
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).
Undtagelser er netop undtagelser, og den virtuelle maskine udfører programmet hurtigst, hvis de ikke opstår.
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.
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.
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); } }
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.
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).
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.
På hjemmesiden http://www.javaperformancetuning.com/ af Jack Shirazi findes tusindvis af tip til, hvordan man kan optimere sit program.
Samme forfatter har
udgivet bogen "Java Performance Tuning":
på
http://www.oreilly.com/catalog/javapt/.
I næste kapitel er beskrevet, hvordan man kalder et eksternt program eller lænker noget maskinkode fra et andet programmeringssprog ind i ens javaprogram.