[.net] GDI+ Horribly Slow. Using DX9 for UserControls good idea?

Started by
16 comments, last by BradSnobar 17 years, 7 months ago
here's some code:

// draws into a bitmap which exists during runtime of the program// this is called once, when the form loads, and each time the form gets resized   // m_Graphics points to the Bitmap m_BackBuffer           		private void repaint_buffer()		{						foreach(Cell c in m_Cells)			{												c.Draw(m_Graphics);			}									m_Grid.Draw(m_Graphics, 0,0 , m_NumCols-1,m_NumRows-1);									m_Graphics.Flush();		}		internal void	UpdateSurface()		{			m_Surface.DrawImage(m_BackBuffer, 0,0);			m_Surface.Flush();		}                // the OnPaint method of the control		private void FTable_Paint(object sender, System.Windows.Forms.PaintEventArgs e)		{			UpdateSurface();						}                // Draw Method of a Cell interior		internal void Draw(Graphics g)		{												Pen backgrpen, forepen;								if (bool_isSelected)				{											backgrpen = m_Pens[0];					forepen =  m_Pens[1];				}				else				{					backgrpen =  m_Pens[2];					forepen =  m_Pens[3];				}														g.FillRectangle(backgrpen.Brush,m_Fillcoords); 				g.DrawString(_Text,m_Font,forepen.Brush,m_Rect,m_StringFormat);				}//end Draw()        // draws the grid lines. since every line segment can have an own color,        // I have to draw the segments separately.	internal void	Draw(Graphics g, int column_start, int row_start, int column_end, int row_end)	{		for (int y=row_start;		y <= 1 + row_end;	++y)		for (int x=column_start;	x < column_end;		++x)		{			LineSeg s = m_HorizSegs[x,y];			g.DrawLine(s.pen, s.x1,s.y1 , s.x2,s.y2);			}		for (int y=row_start;		y <= row_end;			++y)		for (int x=column_start;	x < 1 + column_end;		++x)		{			LineSeg s = m_VertSegs[x,y];			g.DrawLine(s.pen, s.x1,s.y1 , s.x2,s.y2);			}	}


So, resizing is horribly slow, takes 1/4s or so,
and moving the window with one side across the screen border, you still notice some delay (jumpiness of movement), although there's done nothing more than drawing one shitty bitmap! (which has the size of the control)
Advertisement
I don't know much about user controls, but I've seen these tips. Use these settings so only your control handles painting:

Setstyle(Controlstyles.DoubleBuffer, true);
Setstyle(Controlstyles.Selectable, true);
Setstyle(Controlstyles.UserPaint, true);
Setstyle(Controlstyles.AllPaintingInWmPaint, true);


Also, you can only redraw cells in the dirty region that needs repainting. I forget how to do that offhand. Also I assume of course you are only drawing visible cells. If you do keep an offscreen bitmap/picture, it only needs to be as big as the control.

You'll get better help over in the Windows Forms forums:
http://www.windowsforms.net/Forums/ShowForum.aspx?tabIndex=1&tabId=41&ForumID=7
the settings dont change a thing.
thanks for the link, I'll look there later.

I downloaded DevPartner's free profiler,
and here are some numbers:


Quote:
MyControls
Number of Called Methods: 200
Percent of Time Spent in Image: 2,0
TestCtrl
Number of Called Methods: 32
Percent of Time Spent in Image: 0,5


--> The application itself doesnt use so much cycles
Most of the time is spent in GDI+ and kernel32.dll, 58.1% in the function Sleep()
WTF ??? Never called this function in the entire program, so, where does it get called? Somewhere in the GDI, when it can't yet return from a fuction, because it has to finish its drawing or somethin like that??

Quote:
System and Other DLLs

kernel32.dll
Number of Called Methods: 150
Percent of Time Spent in Image: 59,3

GdiPlus.dll
Number of Called Methods: 68
Percent of Time Spent in Image: 19,6

System.Drawing
Number of Called Methods: 316
Percent of Time Spent in Image: 6,2

mscorlib
Number of Called Methods: 943
Percent of Time Spent in Image: 4,3

System.Windows.Forms
Number of Called Methods: 1087
Percent of Time Spent in Image: 3,4

ntdll.dll
Number of Called Methods: 10
Percent of Time Spent in Image: 2,6

user32.dll
Number of Called Methods: 101
Percent of Time Spent in Image: 0,9

I would suggest two things. First, replace the foreach statement with a for loop, it's supposedly as much as 40% slower. Second, throw all your painting into a thread, it might help to increase the APPARENT speed of your application.

[Formerly "capn_midnight". See some of my projects. Find me on twitter tumblr G+ Github.]

hehe, altho the loop overhead doesnt matter here,

just for the interest, I read several articles about the differences in performance between foreach, for loop using .Length every iteration, and storing length temporarily.

A lot of people who are considered experts said it won't really matter,
or showed that the generated IL listing would be sometimes a few instructions longer with for, and blah, others saying the JIT can't optimize if you use temporary...


I did a quick test (edit: corrected copy&paste symptom ;) ), looping through a 100MB int array, running the code with all options a few times to make sure the JIT optimization had taken place.

here's the code
using System;namespace ConsoleApplication1{	class Class1	{		[STAThread]		static void Main(string[] args)		{			if (args.Length < 1)			{				display_options();			 return;			}					int[] arr = new int[25 * 1024*1024];	// 4*25 MB = 100MB			int tickct_start, tickct_end, ticks_diff;							switch (args[0])			{				case "-fe":	{tickct_start=System.Environment.TickCount; test__foreach(arr);}		break;				case "-fl":	{tickct_start=System.Environment.TickCount; test__for_length(arr);}	break;				case "-ft":	{tickct_start=System.Environment.TickCount; test__for_temp(arr);}	break;								default:	display_options();				return;			}						tickct_end = System.Environment.TickCount;			ticks_diff = tickct_end - tickct_start;						Console.WriteLine();			Console.WriteLine("time elapsed: " + ticks_diff);			Console.WriteLine();		}				static void	display_options()		{					Console.WriteLine();			Console.WriteLine("Options:");			Console.WriteLine("-fe : foreach");			Console.WriteLine("-fl : for with .length");			Console.WriteLine("-ft : for with temporary len");			Console.WriteLine();		}						static void	test__foreach(int[] arr)		{						foreach (int i in arr);				}				static void	test__for_length(int[] arr)		{			int dummy;						for (int i=0;	i<arr.Length;	++i)				dummy = arr;		}		static void	test__for_temp(int[] arr)		{			int dummy;						for (int i=0,len=arr.Length;	i<len;	++i)				dummy = arr;		}			}}



results on an AMD Athlon 1GHz, 512MB RAM,
Windows XP,
after running each option several times for "warmup":
(I ran the exe from the cmd command prompt, outside devenv)

(time in "ticks")

debug:
--------
foreach: 490..510
for .length: 461..471
for temp: 390..401

release:
--------
foreach: 390..411
for .length: 380..520
for temp: 420..431


While for seems a little quicker in debug build, with temp it seems a little slower using release build, so the optimization thing that was mentioned may be the reason.
This is compiled with VS 2003, don't know if it would be different with 2005.

[Edited by - UnshavenBastard on September 25, 2006 7:57:28 AM]
Did you get anywhere with this ?

I've got the same problem drawing a custom grid of cells with styling.
I've even tried using the BufferedGraphics objects, but rendering a screen full of cells with text and formatting takes nearly a second a pop.

Did you get anywhere with using directX ?

Matt
I find the trick with GDI+ is cache often, draw less-often, and chop things up into controls where you can. Oh and don't forget to dipose of your drawing objects when you are done. Also perception is king; if an app hangs when drawing it *appears* to the user to take forever, whereas handling drawing on a seperate thread, drawing a "loading" image, then drawing the result on the UI thread *appears* to take less time - when infact it would probabbly take longer ;)
I implemented an excel clone the other day with gdi+, and have not had a great deal of difficulty with redrawing cells.

Here are some tips to help you out.

Setstyle to optimized double buffer.

Don't react to events to do the redrawing of cells if this is what you are doing.
Handle that redrawing without it being fired from an event.
Basically avoid handling much of anything in events, but keep them only as hooks for the end user to gain access to your code.

If you are looping through your list of items then break after the visible items are done being viewed.
Start looping at the first visible item.

Use clipping when appropriate. (For example: Instead of wrapping text, or using a loop to find where to cut off text in a cell.)

As a side note I use a dirived List<> class to allow events for add/remove/whatever of items.

Pipe all of your invalidate calls through a single method that can output a message during debugging. Use the System.Diagnostics.Debug.WriteLine routine to show your message to the output window, this will help you find where you are unnecessarily calling invalidate() on your control.


Surround everything that you can (escpecially loops) in if statements to cut down on unnecessary processing. Check null, empty, bounds, etc.

Avoid using excess memory in your control. This will add pressure to the garbage collector later.

Avoid weak references.
Hook and unhook events with += and -= when appropriate

Loops or hittests are probably the culprit, but finally, use a profiler if you still can't find the bottleneck.

This topic is closed to new replies.

Advertisement