views:

723

answers:

1

Im using GDI+ in C++ to draw a chart control. I want to know if there is any performance difference between the above 2 functions. I am not lazy to write code for DrawLines() but it is that doing so makes my code complex. So im weighin the chances of whether to make code execution faster at the expense of reducing readability and potentially increasing errors and bugs.

Any help wud be appreciated. Eraj.

+2  A: 

There shouldn't be a significant difference between the two for most drawing activities, but to be sure, I wrote up a test project to compare the difference between them (well, actually 3 of them).

For a very large number of lines (x25000) on my machine, DrawLines() (640ms) was about 50% faster over DrawLine() (420ms). To be honest here, I also misread the question the first time around and wrote my initial test in C#. Performance was about the same between the two, which is to be expected as .NET Graphics are based upon GDI+.

Just out of curiosity, I tried regular GDI, which I expect would be faster. Using the win32 PolyLine() (530ms) function was about 20% faster, with 45000 lines. This is 116% faster than using GDI+ DrawLines(). Even more stunning, perhaps, is that using win32 LineTo() instead of GDI+ DrawLine() results in times under 125ms. With an assumed time of 125ms and 45000 lines, this method is at least 800% faster. (Timer resolution and thread timing make it difficult to measure performance in this threshold without resorting to QueryPerformanceCounter and other timing methods of higher frequency.)

However, I should caution against making the assumption that this is a significant bottleneck in drawing code. Many of the performance improvements that can be made will have nothing to do with what objects have to be drawn. I would guess that your requirements will probably dictate that a few hundred items may need to be drawn in normal operation for your control. In that case, I would recommend you write your drawing code to be as straightforward and bug-free as you can, as debugging drawing issues can be an expensive use of time and potentially less beneficial as improving the rest of your control or your application.

Also, if you need to actively update thousands of items, you will see much higher performance gains by moving to a back-buffered solution. This should also make it easier to develop code to draw your control, aside from managing the off-screen buffer.

Here are my source code examples. Each of them handles mouse clicks to alternate between using bulk drawing versus itemized drawing.

GDI+, hosted in a barebones MFC SDI App

This assumes that someone has already declared GDI+ headers and written code to initialize/teardown GDI+.

In ChildView.h

// Attributes
public:
  bool m_bCompositeMode;

// Operations
public:
  void RedrawScene(Graphics &g, int lineCount, int width, int height);
  PointF *CreatePoints(int lineCount, int width, int height);
  void ReportTime(Graphics &g, int lineCount, DWORD tickSpan);

public:
  afx_msg void OnLButtonUp(UINT nFlags, CPoint point);

In ChildView.cpp, added to PreCreateWindow()

m_bCompositeMode = false;

Remainder of ChildView.cpp, including OnPaint() and Message Map changes.

BEGIN_MESSAGE_MAP(CChildView, CWnd)
  ON_WM_PAINT()
  ON_WM_LBUTTONUP()
END_MESSAGE_MAP()

void CChildView::OnPaint() 
{
  CPaintDC dc(this); // device context for painting
  RECT rcClient;
  ::GetClientRect(this->GetSafeHwnd(), &rcClient);

  Graphics g(dc.GetSafeHdc());
  g.Clear(Color(0, 0, 0));

  RedrawScene(g, 25000, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top);
}

void CChildView::RedrawScene(Graphics &g, int lineCount, int width, int height)
{
  DWORD tickStart = 0;
  DWORD tickEnd = 0;

    Pen p(Color(0, 0, 0x7F));
    PointF *pts = CreatePoints(lineCount, width, height);
    tickStart = GetTickCount();
    if (m_bCompositeMode)
    {
        g.DrawLines(&p, pts, lineCount);
    }
    else
    {
        int i = 0;
        int imax = lineCount - 1;
        for (i = 0; i < imax; i++)
        {
            g.DrawLine(&p, pts[i], pts[i + 1]);
        }
    }
    tickEnd = GetTickCount();
  delete[] pts;
    ReportTime(g, lineCount, tickEnd - tickStart);
}

void CChildView::ReportTime(Graphics &g, int lineCount, DWORD tickSpan)
{
  CString strDisp;
  if(m_bCompositeMode)
  {
    strDisp.Format(_T("Graphics::DrawLines(Pen *, PointF *, INT) x%d took %dms"), lineCount, tickSpan);
  }
  else
  {
    strDisp.Format(_T("Graphics::DrawLine(Pen *, PointF, PointF) x%d took %dms"), lineCount, tickSpan);
  }

    // Note: sloppy, but simple.
  Font font(L"Arial", 14.0f);
  PointF ptOrigin(0.0f, 0.0f);
  SolidBrush br(Color(255, 255, 255));
    Status s = g.DrawString(strDisp, -1, &font, ptOrigin, &br);
}

PointF* CChildView::CreatePoints(int lineCount, int width, int height)
{
  if(lineCount <= 0)
  {
    PointF *ptEmpty = new PointF[2];
    ptEmpty[0].X = 0;
    ptEmpty[0].Y = 0;
    ptEmpty[1].X = 0;
    ptEmpty[1].Y = 0;
    return ptEmpty;
  }

  PointF *pts = new PointF[lineCount + 1];
  int i = 1;
  while(i < lineCount)
  {
    pts[i].X = (float)(rand() % width);
    pts[i].Y = (float)(rand() % height);
    i++;
  }
  return pts;
}

void CChildView::OnLButtonUp(UINT nFlags, CPoint point)
{
  m_bCompositeMode = !m_bCompositeMode;
  this->Invalidate();

  CWnd::OnLButtonUp(nFlags, point);
}

C#.NET, hosted in a basebones WinForms App, with default class Form1

Set a default size for the form, equal to the size of the MFC version if you are comparing the two. A size-change handler could be added as well.

public Form1()
{
    InitializeComponent();
    bCompositeMode = false;
}

bool bCompositeMode;

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.Clear(Color.Black);
    RedrawScene(e.Graphics, 25000, this.ClientRectangle.Width, this.ClientRectangle.Height);
}

private void RedrawScene(Graphics g, int lineCount, int width, int height)
{
    DateTime dtStart = DateTime.MinValue;
    DateTime dtEnd = DateTime.MinValue;
    using (Pen p = new Pen(Color.Navy))
    {
        Point[] pts = CreatePoints(lineCount, width, height);
        dtStart = DateTime.Now;
        if (bCompositeMode)
        {
            g.DrawLines(p, pts);
        }
        else
        {
            int i = 0;
            int imax = pts.Length - 1;
            for (i = 0; i < imax; i++)
            {
                g.DrawLine(p, pts[i], pts[i + 1]);
            }
        }
        dtEnd = DateTime.Now;
    }
    ReportTime(g, lineCount, dtEnd - dtStart);
}

private void ReportTime(Graphics g, int lineCount, TimeSpan ts)
{
    string strDisp = null;
    if (bCompositeMode)
    {
        strDisp = string.Format("DrawLines(Pen, Point[]) x{0} took {1}ms", lineCount, ts.Milliseconds);
    }
    else
    {
        strDisp = string.Format("DrawLine(Pen, Point, Point) x{0} took {1}ms", lineCount, ts.Milliseconds);
    }

    // Note: sloppy, but simple.
    using (Font font = new Font(FontFamily.GenericSansSerif, 14.0f, FontStyle.Regular))
    {
        g.DrawString(strDisp, font, Brushes.White, 0.0f, 0.0f);
    }
}

private Point[] CreatePoints(int count, int width, int height)
{
    Random rnd = new Random();
    if (count <= 0) { return new Point[] { new Point(0,0), new Point(0,0)}; }
    Point[] pts = new Point[count + 1];
    pts[0] = new Point(0, 0);
    int i = 1;
    while (i <= count)
    {
        pts[i] = new Point(rnd.Next(width), rnd.Next(height));
        i++;
    }
    return pts;
}

private void Form1_Click(object sender, EventArgs e)
{
    bCompositeMode = !bCompositeMode;
    Invalidate();
}

Regular GDI, hosted in a barebones MFC SDI App

In ChildView.h

// Attributes
public:
  bool m_bCompositeMode;

// Operations
public:
  void RedrawScene(HDC hdc, int lineCount, int width, int height);
  POINT *CreatePoints(int lineCount, int width, int height);
  void ReportTime(HDC hdc, int lineCount, DWORD tickSpan);

public:
  afx_msg void OnLButtonUp(UINT nFlags, CPoint point);

In ChildView.cpp

Update PreCreateWindow() just as in the GDI+ sample.

BEGIN_MESSAGE_MAP(CChildView, CWnd)
  ON_WM_PAINT()
  ON_WM_LBUTTONUP()
END_MESSAGE_MAP()

void CChildView::OnPaint() 
{
  CPaintDC dc(this); // device context for painting
  HDC hdc = dc.GetSafeHdc();

  HBRUSH brClear = (HBRUSH)::GetStockObject(BLACK_BRUSH);
  RECT rcClient;
  ::GetClientRect(this->m_hWnd, &rcClient);
  ::FillRect(hdc, &rcClient, brClear);
  ::DeleteObject(brClear);

  RedrawScene(hdc, 45000, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top);
}

void CChildView::RedrawScene(HDC hdc, int lineCount, int width, int height)
{
  DWORD tickStart = 0;
  DWORD tickEnd = 0;

  HPEN p = ::CreatePen(PS_SOLID, 1, RGB(0, 0, 0x7F));
  POINT *pts = CreatePoints(lineCount, width, height);
  HGDIOBJ prevPen = SelectObject(hdc, p);
  tickStart = GetTickCount();
  if(m_bCompositeMode)
  {
    ::Polyline(hdc, pts, lineCount);
  }
  else
  {
    ::MoveToEx(hdc, pts[0].x, pts[0].y, &(pts[0]));
    int i = 0;
    int imax = lineCount;
    for(i = 1; i < imax; i++)
    {
      ::LineTo(hdc, pts[i].x, pts[i].y);
    }
  }
  tickEnd = GetTickCount();
  ::SelectObject(hdc, prevPen);
  delete pts;
  ::DeleteObject(p);

  ReportTime(hdc, lineCount, tickEnd - tickStart);
}

POINT *CChildView::CreatePoints(int lineCount, int width, int height)
{
  if(lineCount <= 0)
  {
    POINT *ptEmpty = new POINT[2];
    memset(&ptEmpty, 0, sizeof(POINT) * 2);
    return ptEmpty;
  }

  POINT *pts = new POINT[lineCount + 1];
  int i = 1;
  while(i < lineCount)
  {
    pts[i].x = rand() % width;
    pts[i].y = rand() % height;
    i++;
  }
  return pts;
}

void CChildView::ReportTime(HDC hdc, int lineCount, DWORD tickSpan)
{
  CString strDisp;
  if(m_bCompositeMode)
  {
    strDisp.Format(_T("PolyLine(HDC, POINT *, int) x%d took %dms"), lineCount, tickSpan);
  }
  else
  {
    strDisp.Format(_T("LineTo(HDC, HPEN, int, int) x%d took %dms"), lineCount, tickSpan);
  }

  HFONT font = (HFONT)::GetStockObject(SYSTEM_FONT);
  HFONT fontPrev = (HFONT)::SelectObject(hdc, font);

  RECT rcClient;
  ::GetClientRect(this->m_hWnd, &rcClient);
  ::ExtTextOut(hdc, 0, 0, ETO_CLIPPED, &rcClient, strDisp.GetString(), strDisp.GetLength(), NULL);
  ::SelectObject(hdc, fontPrev);
  ::DeleteObject(font);
}

void CChildView::OnLButtonUp(UINT nFlags, CPoint point)
{
  m_bCompositeMode = !m_bCompositeMode;
  this->Invalidate();

  CWnd::OnLButtonUp(nFlags, point);
}
meklarian
hello meklarian, your reply is quite useful. I was expecting ppl to submit their ideas based on their experience. Bt u actually wrote code to test the output. Thanx a lot. Your findings are quite helpful to make an understanding of what I should do. As for an indication of the drawing iterations I do, my chart ctrl should support at least 10000 points per series so your timings should be valid for me as well.
OverTheEdge
No problem, glad to help. I should note that one could probably squeeze more performance out of the samples here, but the relative performance should remain the same. Also, if you are drawing into a non-display HDC/Graphics context (bitmap or other off-screen buffer), the timings may change significantly. Let me know if you have any trouble with these sample snippets.
meklarian
why does it affect GDI+ rendering performance when drawn to an offscreen buffer ? I do this to avoid flicker (draw onto a mem bitmap and then bitblt that). At the moment Im trying to squeeze the best performance that I can get out of GDI+. I have even tried multithreading my drawing function but that dint help. I can't help but feel that Im doing smthing wrong so any tips on increasing GDI+ performance would be highly appreciated.
OverTheEdge
Rendering into an offscreen buffer can improve performance if it is too expensive to do partial redraws of content. However, you'll incur the penalty of consuming more memory and having to perform extra work on window/region changes. You mention that you have 10,000 points per active series. Does the display require animation or does the underlying data change frequently? If neither, you should start by reducing overhead when only part of the view is invalidated. If there are rapid changes in display required, try looking at culling the number of items rendered.
meklarian
Consider this- if you have a 800x600 viewport, the minimum number of single-width lines per update to cover all of it is only 600. If you are drawing 10,000 items into an area of this size- there will be significant overlap, and you may be able to eliminate wasted time spent drawing over such areas. A few ways to do this are to merge peaks/valleys if the display area is lower in resolution than the ranges of your source data, implement layering for separate series and non-data features, and even moving non-data features out of the buffering scheme can help too.
meklarian