Performance beim Zeichnen erhöhen?

  • Themenstarter Themenstarter Guest
  • Beginndatum Beginndatum
Status
Nicht offen für weitere Antworten.
G

Guest

Gast
Hallo,

ich habe das Problem, dass mein kleines, selbstgeschriebenes Programm meiner Meinung nach viel zuviel Ressourcen frisst (teilweise >80% CPU)
Ich denke es liegt an der List<Kreis> kreise, die ich benutze und dort immer remove(0) aufrufe.
Hat jemand ne Idee, wie man das Ganze performanter gestalten könnte?

mfg

Code:
package paintComponentTest;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class PaintComponentTest extends JFrame {

	private static final long serialVersionUID = 1L;
	private JPanel jContentPane = null;
	private int x = 20;
	private int y = 50;
	private int width = 50;
	private int height = 50;
	private static final int MAX = 750;
	private static final int DELAY = 5;
	private static final int MAXGROSSE = 250;
	private static final int MINGROSSE = 2;
	private static final int ABWEICHUNG_X = 3;
	private static final int ABWEICHUNG_Y = 3;
	private final List<Kreis> kreise = new ArrayList<Kreis>(MAX+1);
	private boolean repaintFinished = true;
	private boolean rechts = true;
	private boolean runter = true;
	private boolean groesser = true;
	private Color color = Color.BLACK;
	private final Random random = new Random();
	private double x_max = 0;
	private double y_max = 0;
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				new PaintComponentTest().setVisible(true);
			}
		});
	}

	/**
	 * This is the default constructor
	 */
	public PaintComponentTest() {
		super();
		initialize();
	}

	/**
	 * This method initializes this
	 * 
	 * @return void
	 */
	private void initialize() {
		this.setSize(500, 500);
		this.setContentPane(getJContentPane());
		this.setTitle("PoingPoing");
		this.setExtendedState(JFrame.MAXIMIZED_BOTH);
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		new Thread(new MyRunnable()).start();
	}
	
	private void geheRechts(int a)
	{
		x += a;
	}
	
	private void geheRunter(int a)
	{
		y+=a;
	}
	
	private void werdeGoesser(int a)
	{
		width+=a; 
		height+=a;
	}

	/**
	 * This method initializes jContentPane
	 * 
	 * @return javax.swing.JPanel
	 */

	
	@SuppressWarnings("serial")
	protected JPanel getJContentPane() {
		if (jContentPane == null) {
			jContentPane = new JPanel()
			{
				@Override
				protected void paintComponent(Graphics g) 
				{
					repaintFinished = false;
					if(kreise.size() >= MAX || kreise.size() == 0)
						super.paintComponent(g);
					
					Graphics2D g2 = (Graphics2D)g; 
//					g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON );
					
					for(Kreis k : kreise)
					{
						g2.setColor(k.getColor());
						g2.drawOval(k.getX(), k.getY(), k.getWidth(), k.getHeight());
						
					}
					
					repaintFinished = true;
				}
			};
			
			jContentPane.setLayout(new BorderLayout());
			
			jContentPane.addComponentListener(new ComponentAdapter(){

				@Override
				public void componentResized(ComponentEvent e) 
				{
					x_max = getJContentPane().getWidth();
					y_max = getJContentPane().getHeight();
				}
			});
		}
		return jContentPane;
	}
	
	class MyRunnable implements Runnable
	{

		@Override
		public void run() 
		{
			while(true)
			{
				try {
					Thread.sleep(DELAY);
					color = new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256));
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				if(repaintFinished)
				{
					if(rechts)
					{
						if(x >= (x_max - width))
						{
							rechts = false;
							geheRechts(-ABWEICHUNG_X);
						}
						else
						{
							rechts = true;
							geheRechts(ABWEICHUNG_X);
						}
					}
					else
					{
						if(x <= 0)
						{
							rechts = true;
							geheRechts(ABWEICHUNG_X);
						}
						else
						{
							rechts = false;
							geheRechts(-ABWEICHUNG_X);
						}
					}
					
					if(runter)
					{
						if(y >= (y_max - height))
						{
							runter = false;
							geheRunter(-ABWEICHUNG_Y);
						}
						else
						{
							runter = true;
							geheRunter(ABWEICHUNG_Y);
						}
					}
					else
					{
						if(y <= 0)
						{
							runter = true;
							geheRunter(ABWEICHUNG_Y);
						}
						else
						{
							runter = false;
							geheRunter(-ABWEICHUNG_Y);
						}
					}
					
					if(groesser)
					{
						if(width >= MAXGROSSE)
						{
							groesser = false;
							werdeGoesser(-1);
						}
						else
						{
							groesser = true;
							werdeGoesser(1);
						}
					}
					else
					{
						if(width <= MINGROSSE)
						{
							groesser = true;
							werdeGoesser(1);
						}
						else
						{
							groesser = false;
							werdeGoesser(-1);
						}
					}
					
					kreise.add(new Kreis(x,y,width, height, color));			
					
					if(kreise.size() > MAX)
						 kreise.remove(0);
					
					getJContentPane().repaint();
				}			
			}//while()
		}
	}
}
 
Hi,
Du erzeugst jedes Mal ein neues JPanel mit Layout und allem drum und dran. Das ist tatsächlich extrem aufwändig. Definieren Dir eine von JPanel abgeleitete Klasse, in der Du paintComponent(Graphics g) überschreibst, wie Du es ja hier gemacht hast. Du kannst dann jeweils über ein repaint ein Neuzeichnen veranlassen und das wird dann auch direkt dargestellt. Es ist nicht nötig, ständig neue Panels zu erzeugen.

Ciao,
Wolfgang
 
Wolfgang Lenhard hat gesagt.:
Hi,
Du erzeugst jedes Mal ein neues JPanel mit Layout und allem drum und dran. Das ist tatsächlich extrem aufwändig. Definieren Dir eine von JPanel abgeleitete Klasse, in der Du paintComponent(Graphics g) überschreibst, wie Du es ja hier gemacht hast. Du kannst dann jeweils über ein repaint ein Neuzeichnen veranlassen und das wird dann auch direkt dargestellt. Es ist nicht nötig, ständig neue Panels zu erzeugen.

Ciao,
Wolfgang

Das ist eindeutig falsch!
Schau dir den Code nochmal an 😉

Da steht :
Code:
if (jContentPane == null) ....
Ist das Panel einmal initialisiert, wird es nicht nochmal erzeugt.
Visual-Editor-Style 😉

Zum Überprüfen einfach einSystem.out einfügen:

Code:
		if (jContentPane == null) 
		{
			System.out.println("initialisiere panel!"); //wird nur einmal angezeigt
 
bei einer ArrayList mit 750 Elementen kann remove(0) durchaus etwas Arbeit verursachen, aber dass man das gleich sichtbar merkt?
ne LinkedList dürfte schneller sein bei remove() im Millisekundentakt,
dann wäre allerdings generell zu überdenken, ob man nicht bei wenigen Kreisen bleibt und nur dessen Inhalt ändert oder so
 
Oh, die 2. Klasse hab ich ja vergessen:

Code:
import java.awt.Color;

public class Kreis 
{
	private final int x;
	private final int y;
	private final int width;
	private final int height;
	private final Color color;
	
	public Kreis(int x, int y, int width, int height, Color color) 
	{
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
		this.color = color;
	}

	/**
	 * @return the x
	 */
	public int getX() {
		return x;
	}

	/**
	 * @return the y
	 */
	public int getY() {
		return y;
	}

	/**
	 * @return the width
	 */
	public int getWidth() {
		return width;
	}

	/**
	 * @return the height
	 */
	public int getHeight() {
		return height;
	}

	/**
	 * @return the color
	 */
	public Color getColor() {
		return color;
	}
	
	@Override
	public String toString() 
	{
		return 
		"x: " + x + 
		"\ny: " + y +
		"\nwidth: " + width +
		"\nheight: " + height;
	}
	
}
 
Hm. SlaterB's Vorschlag solltest du schonmal in Erwägung ziehen: remove(0) auf einer ArrayList dauert relativ lange, im Vergleich zur gleichen Operation auf einer LinkedList. Aber ... bei mir (sitz' hier grad' an so einer alten Kiste) ist es schon bei 200 Kreisen ziemlich lahm... Es könnte zwar schon helfen, den "repaint"-Aufruf zu ändern in
getJContentPane().repaint(rect.x, rect.y, rect.width, rect.height);
(wobei "rect" ein Rectangle ist, das den NEUEN Kreis und ggf. den REMOVEten Kreis einschließt), aber treten bei mir (vmtl. wegen http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4151279 ) noch Artefakte auf.... 🙁
 
Wenn ich eine LinkedList nehme bringt mir das irgendwie keinen merklichen Unterschied.

Ich habe mal bissl rumprobiert:
1) Ich dachte zwar nicht, dass es hilft, ABER wenn ich die Anzahl der repaint-Aufrufe "begrenze", dann wird das bei mir etwas besser.
Hab einfach nen Zähler initialisiert und da wo der repaint steht dann
Code:
					if(++durchgang > MAX)
						durchgang = 1;
					
					if(durchgang % 5 == 0)
						getJContentPane().repaint();

2) was mich irgendwie stutzig macht:
Lasse ich die Farben weg, läuft es irgendwie viel geschmeidiger 😀
Liegt das an der Farbe an sich, oder am Erzeugen des neuen Color-Objekts?
Oder vielelicht gar am random.nextInt()?

3) Kann man da irgendwie was am rendering machen?
(Kenn ich mich net aus)
 
Achja, nur so ganz nebenbei: Der DELAY ist mit 5 ms vermutlich zu klein: 200 mal pro Sekunde bewegen und Neuzeichnen schluckt er einfach nicht - also, vermutlich würde ein DELAY von 15 und dafür eine größere Schrittweise genauso aussehen, aber nur 1/3 der Rechenzeit benötigen...
 
Bringt zwar Performance-mäßig nichts, aber die Klasse Kreis kann man einfacher haben:

Code:
class Kreis extends Rectangle{

	private final Color	color;

	public Kreis(int x, int y, int width, int height, Color color) {
    super(x,y,width,height);
		this.color = color;
	}

	public Color getColor() {
		return color;
	}

	@Override
	public String toString() {
		return "x: " + x + "\ny: " + y + "\nwidth: " + width + "\nheight: "
				+ height;
	}

}

Man muß dann nur drawOval etwas anpassen:

Code:
 g2.drawOval((int)k.getX(), (int)k.getY(), (int)k.getWidth(),(int)k.getHeight());
 
Quaxli hat gesagt.:
Bringt zwar Performance-mäßig nichts, aber die Klasse Kreis kann man einfacher haben:

🙂 So hatte ich sie mir auch gebastelt, bevor sie gepostet wurde: Damit kann man dann den Bereich, der neu gezeichnet werden muss, relativ leicht angeben...
 
Ich habe nochmal an dem Problem rumgewerkelt und ein ähnliches Programm geschrieben - mit Fullscreen und active Rendering. Es ist kein Beispiel für schönes programmieren, eher so hingefrickelt, aber es läuft (bei mir) relativ zügig.



Code:
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import javax.swing.*;

public class Animation extends JFrame implements Runnable, KeyListener{
	
	private static final long serialVersionUID = 1L;
	BufferStrategy strategy;
	boolean active = true;
	ArrayList<Circle> circles;

	double deltax = 3;
	double deltay = 3;
	int size = 50;
	int deltasize = 1;
	
	public static void main(String[] args){
		new Animation();
	}
	
	public Animation(){
		super("Kreise");
		setExtendedState(JFrame.MAXIMIZED_BOTH);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setIgnoreRepaint(true);
    setBackground(Color.BLACK);
    addKeyListener(this);
    
		DisplayMode mode = new DisplayMode(800, 600, 16, 75);


		GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
		GraphicsDevice device = environment.getDefaultScreenDevice();
		device.setFullScreenWindow(this);
		device.setDisplayMode(mode);
    
    circles = new ArrayList<Circle>();
    circles.add(new Circle(200,200,size,size));
    
    setVisible(true);
    
    createBufferStrategy(2);
    strategy = getBufferStrategy();
        
    Thread t = new Thread(this);
    t.start();

	}
	
  public Circle getNewCircle(Circle parent){
 	 
 	 double dx = parent.getX()+deltax;
 	 double dy = parent.getY()+deltay;
 	 
 	 size+= deltasize;
 	 
 	 if(size>=100){
 		 size = 100;
 		 deltasize *= -1;
 	 }
 	 
 	 if(size<=5){
 		 size = 5;
 		 deltasize *= -1;
 	 }

 	 Circle circle = new Circle(dx,dy,size,size);
 	 
 	 if(dy+size>getHeight()){
 		deltay *= -1;
 		circle.y = parent.getY()+deltay;
 	 }
 	 
 	 if(dx+size>getWidth()){
 		 deltax *= -1;
 		 circle.x = parent.getX()+deltax;
 	 }
 	 
 	 if(dx<0){
 		 deltax *= -1;
 		 circle.x = parent.getX()+deltay;
 	 }
 	 
 	 if (dy<0){
 		 deltay *= -1;
 		 circle.y = parent.getY()+deltax;
 	 }
	 
	 circle.width = size;
	 circle.height = size;
	 
 	 
 	 return circle;
  }
	
	
	
	public void run() {
	
		long last = System.nanoTime();
		long delta = System.nanoTime();
		Circle clast = null;
		
	  while(active){
	  	
	  	delta = System.nanoTime() - last;
	  	last  = System.nanoTime();
	  	
	  	Graphics2D g = (Graphics2D) strategy.getDrawGraphics();
	  	//g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON );
	  	
	  	g.clearRect(0, 0, getWidth(),getHeight());

	  	for(Circle c: circles){
	  		c.draw(g);
	  		clast = c;
	  	}
	  	
	  	circles.add(getNewCircle(clast));
	  	
	    if(circles.size()>750){
	    	circles.remove(0);
	    }


	  	g.setColor(Color.RED);
	  	g.drawString("circles: " + Integer.toString(circles.size()), 50, 50);
	  	g.drawString("frames: " + Long.toString((long) (1e9/delta)), 50, 70);
	  	
	  	g.dispose();
	  	strategy.show();
	  	Toolkit.getDefaultToolkit().sync();
	  	
	  	
	  	try {
				Thread.sleep(15);
			} catch (InterruptedException e) {}

	  	
	  }
		
		setVisible(false);
		dispose();
		
	}

	public void keyPressed(KeyEvent e) {

		
	}

	public void keyReleased(KeyEvent e) {
    active = false;

		
	}

	public void keyTyped(KeyEvent e) {

		
	}

}

Und der Kreis:

Code:
package src;

import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.util.Random;

public class Circle extends Rectangle2D.Double{

	private static final long serialVersionUID = 1L;
	private final Color   color;
	final int MAXSIZE = 250;
	final int MINSIZE = 2;
	
	public boolean increase = true;
	
   public Circle(double x, double y, double width, double height) {
     super(x,y,width,height);
     Random random = new Random(); 
     color = new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)); 
   }

   public Color getColor() {
      return color;
   }

   public void draw(Graphics g){
  	 g.setColor(color);
  	 g.drawOval((int)x, (int)y, (int)width, (int)height);
   }
   
  
   
}
 
Anonymous hat gesagt.:
ich habe das Problem, dass mein kleines, selbstgeschriebenes Programm meiner Meinung nach viel zuviel Ressourcen frisst (teilweise >80% CPU)
Ich denke es liegt an der List<Kreis> kreise, die ich benutze und dort immer remove(0) aufrufe.
Hat jemand ne Idee, wie man das Ganze performanter gestalten könnte?
beim Optimieren gibt es mehrere gute Strategien, auch wenn man dafür mit dem guten Design brechen muss.

Strategie 1: "new" ist schlecht
alles im kritischen Teil was mit "new" zu tun hat, muss überarbeitet werden, dass es ohne new auskommt. In deinem Programm hast Du eine Liste mit der festen Größe MAX+1. Die Objekte dafür erzeugst Du dynamisch. Das gesamte System sieht nach einem Ringpuffer aus. Hier kann man erst mal vorab alle Objekte schon mal erzeugen und in der run() Methode nur verändern, was ein new weniger ist. Doch da dein "Kreis" Datenmodel nur aus 5 einfachen Elementen besteht, würde ich sagen, alle Listen und Objekte weg und nur mit Feldern arbeiten. Dazu zwei Variablen, die den Anfang und das Ende des Ringpuffers repräsentieren:

Code:
int [][] kreis_rahmen = new int[MAX+1][4];
Color [] kreis_farbe = new Color[MAX+1];
int kreis_queue_first = 0;
int kreis_queue_last = 0;

Strategie 2: Funktionsaufrufe sind schlecht
in deinem Programm benutzt Du Funktionen wie "geheRechts". Diese sind sehr kurz und sollten besser nicht als Funktionen, sondern als Code in der run() stehen.

Strategie 3: Neuzeichnen, repaint() ist in diesem Fall schlecht
auch wenn es quer durch das Internet heißt, man sollte bei Java immer repaint aufrufen und niemals sich direkt das Grafikobjekt holen, ist es in diesem Fall angebracht sich einmal mit getJContentPane().getGraphics() eine Graphics2D am Anfang der run() zu holen. Das benutzt man dann solange MyRunnable läuft. Man muss sich dann zwar selber um das Neuzeichnen des Hintergründes kümmern, aber das ist kein großes Problem. Damit das Betriebssystem einem nicht in die Quere kommt, muss man zusätzlich paint() lahmlegen:

Code:
 public void paint(Graphics g)
  {
  if(run_is_running)
    {
    // mache gar nichts
    }
  else
    {
    super.paint(g);
    }
  }

Jedenfalls muss der kritische Teil des Programms als Endergebnis so wenig unnötigen Code haben, wie möglich. Gäbe es ein g.setColor(int r, int g, int b), könnte man auch new Color() einsparen.

Slawa
 
Strategie 1: "new" ist schlecht

Ja, das kann ein Problem sein - bei höchst zeitkritischen Berechnungen, irgendwo in innersten Schleifen - aber welche Krämpfe man sich antun sollte, um "new"s zu vermeiden, sagt einem bestenfalls der Profiler - wenn immer neue Kreise mit zufälligen(!) Farben erzeugt werden sollen, muss man das ja dynamisch machen. Falls du darauf hinaus wolltest: Von Object Pooling ist generell abzuraten (siehe auch http://java.sun.com/docs/hotspot/HotSpotFAQ.html#gc_pooling ). Dass das effizienter mit einer verketteten Liste gemacht werden könnte, wurde schon gesagt.


Strategie 2: Funktionsaufrufe sind schlecht
in deinem Programm benutzt Du Funktionen wie "geheRechts". Diese sind sehr kurz und sollten besser nicht als Funktionen, sondern als Code in der run() stehen.[/i]

Hm. Mehr als NEIN braucht man dazu eigentlich nicht zu sagen. Vielleicht noch ein Verweis auf http://www.javaspecialists.eu/archive/Issue158.html oder auch nur das entsprechende Zitat von dort: The amazing performance of inlining code should affect how we code Java. We should stay clear of long methods and rather break them up more. Lots of small methods usually result in less duplicate copy and paste. Since the inlining is done for you by the Server HotSpot Compiler, there is no cost in calling the method. (und "no" heißt in diesem Fall wirklich "no"...)


Strategie 3: Neuzeichnen, repaint() ist in diesem Fall schlecht
auch wenn es quer durch das Internet heißt, man sollte bei Java immer repaint aufrufen und niemals sich direkt das Grafikobjekt holen, ist es in diesem Fall angebracht sich einmal mit getJContentPane().getGraphics() eine Graphics2D am Anfang der run() zu holen.


:autsch: Wenn das in Richtung "active rendering" gehen sollte, dann verweise bitte auf Seiten, wo beschrieben ist, wie man das richtig macht. Ansonsten: Hol' dir mal ein Graphics-Objekt, und spiel' dann während das Programm läuft ein bißchen mit dem Fenster rum (verschieben, maxim/minimieren, Größe ändern....).


EDIT: Noch so als Nachtrag: Hast du mal (nur zum Spaß) all deine "Optimierungen" eingebaut, und mal geschaut, wie viel schneller es dadurch wird ...? :wink:
 
Tolle Lösung :toll:
Bei mir wurden nur die neuen Kreise teilweise nicht vollständig gezeichnet, bis ich den Clipping-Bereich für die neuen Kreise etwas vergrößert hatte:

Code:
    getJContentPane().repaint(x-10, y-10, width + 20, height + 20);

Ein paar Pixel mehr sollten nicht weh tun 😉
 
Danke,
gefällt mir gut, nun läuft es sogar mit 2500 Kreisen sehr flüssig.
Oder mit 5 Threads a 250 Kreisen 🙂

mfg
 
@André Uhres: Das war die Optimierung, die ich vorgeschlagen hatte (repaint-Bereich beschränken) - aber DA sollte es jeetzt ziemlich egal sein, ob man einen Array oder eine LinkedList verwendet...
 
Marco13 hat gesagt.:
EDIT: Noch so als Nachtrag: Hast du mal (nur zum Spaß) all deine "Optimierungen" eingebaut, und mal geschaut, wie viel schneller es dadurch wird ...? :wink:
nein, habe ich nicht und hatte es auch nicht vor. Am Anfang des Threads hat der Fragesteller nach Ideen gefragt, wie man es performanter gestalten kann. Ich habe ein paar Strategien genannt, die ich selber schon in anderen Projekten verwendet und gute Erfahrungen damit gesammelt habe. Wenn der Fragesteller noch weitere Fragen oder Klärungsbedarf gehabt hätte, hätte ich ihm gerne weitergeholfen.

Noch ein paar Sachen zu deinen Einwänden. Die Hinweise zum Object Pooling, die Du verlinkt hast, beziehen sich auf den Garbage Collector. Das trifft in diesem Fall nicht zu, weil es hier um eine zeitkritische Ausführung geht und nicht darum den GC das Leben zu erleichtern, wenn er mal aufräumt. In der zeitkritischen Ausführung sind new's so weit es geht zu vermeiden, den sie kosten viel Ausführungszeit. Eine gute Möglichkeit dabei ist Object Pooling, doch in diesem Fall reicht ein Ringpuffer über Felder mit fester Größe aus.

Was die Inline-Funktionen angeht, natürlich optimiert eine HotSpot JVM wo es geht. Aber es ist eben die VM. Es ist eine Sache das der VM zu überlassen, aber eine andere unabhängig davon zu optimieren. In Systemen, wo man regelmäßig Tests ausführt und z.B. die Ausführungszeit misst, könnten die Ergebnisse durch die Optimierung einer oder mehreren VM unterschiedliche ausfallen. Deshalb ist es manchmal sinnvoll, selber so viel es geht zu optimieren. Weiterhin kann es sein, dass das Programm später nicht auf einer HotSpot JVM ausgeführt wird (z.B wenn es dann auf einem mobilen Gerät läuft) oder die kritischen Teile z.B. nach C portiert werden.

Ich habe von Strategien gesprochen und zwar unabhängig von einer Sprache. Diese kann man ausprobieren, muss man aber nicht unbedingt nehmen. Es hängt davon ab, was das Endergebnis sein soll. Das gut optimierter Code danach hässlich aussieht, das habe ich am Anfang meines letzten Posts schon erwähnt.

Slawa
 
Status
Nicht offen für weitere Antworten.

Zurück
Oben