6

I have a TStringGrid with 10 columns. Adding 500 rows to it takes around 2 seconds. Is this normal performance?

It seems a bit slow to me.

I am getting the data from a database query. If I loop through the query but don't write the results to the StringGrid, the process takes around 100ms, so it's not the database that's slowing things down.

Once the rows are added, the StringGrid performance is fine.

Here is the code I am using

Grid.RowCount := Query.RecordCount;
J := 0;

while not Query.EOF do
begin
    Grid.Cells[0,J]:=Query.FieldByName('Value1').AsString;
    Grid.Cells[1,J]:=Query.FieldByName('Value2').AsString;
    Grid.Cells[2,J]:=Query.FieldByName('Value3').AsString;
    // etc for other columns.
    Inc(J);
    Query.Next();
end;

The real code is actually a bit more complex (the table columns do not correspond exactly to the query columns) but that's the basic idea

awmross
  • 3,789
  • 3
  • 38
  • 51

11 Answers11

7

One other thing I have found to be very important when going through a lot of records is to use proper TField variables for each field. FieldByName iterates through the Fields collection every time so is not the most performant option. Before the loop define each field as in:

var
  f1, f2: TStringField;
  f3: TIntegerField;

begin
  // MyStringGrid.BeginUpdate; // Can't do this
  // Could try something like this instead:
  // MyStringGrid.Perform(WM_SETREDRAW, 0, 0);
  try
    while ... do
    begin
      rowvalues[0] := f1.AsString;
      rowvalues[1] := f2.AsString;
      rowvalues[2] := Format('%4.2d', f3.AsInteger);
      // etc 
    end;
  finally
    // MyStringGrid.EndUpdate; // Can't - see above
    // MyStringGrid.Perform(WM_SETREDRAW, 1, 0);
    // MyStringGrid.Invalidate;
  end;
end;

That along with BeginUpdate/Endupdate and calling Query.DisableControls if appropriate.

shunty
  • 3,699
  • 1
  • 22
  • 27
  • I specifically stated in my question that this was not a database issue. The time taken for database access is negligible (around 100ms) – awmross Oct 06 '11 at 22:33
  • Indeed you did. Nevertheless it can't hurt your apps performance to implement good practice methods for data access. Also "just looping through the query" is not a measure of whether it's a database issue or not - if the fields/values are not accessed then you won't get realistic performance results. As ever, performance improvements can often be achieved by applying more than just one "fix". – shunty Oct 07 '11 at 09:16
  • My bad. What a shame I didn't have my compiler with me all those years ago when I wrote that. Perhaps you might have given an alternative, such as WM_SETREDRAW as [here](http://stackoverflow.com/a/7099978/197962) – shunty Nov 03 '13 at 11:55
6

The solution was to add all values in a row at once, using the "Rows" property.

My code now looks like this:

Grid.RowCount := Query.RecordCount;
rowValues:=TStringList.Create;
J := 0;

while not Query.EOF do
begin
    rowValues[0]:=Query.FieldByName('Value1').AsString;
    rowValues[1]:=Query.FieldByName('Value2').AsString;
    rowValues[2]:=Query.FieldByName('Value3').AsString;
    // etc for other columns.
    Grid.Rows[J]:=rowValues;
    Inc(J);
    Query.Next();
end;

rowValues.Free; // for the OCD among us

This brought the time down from 2 seconds to about 50ms.

awmross
  • 3,789
  • 3
  • 38
  • 51
  • 4
    You should also lock the stringgrid during this operation. Grid.Cols[0].BeginUpdate, Try (Set RowCount and fill lines) Finally Grid.Cols[0].EndUpdate. It's a little weird but this trick locks the whole grid, not only the column. – LU RD Oct 06 '11 at 05:28
  • 2
    To be honest, I don't understand the speed difference. That would mean that `Grid.Cells` access is very slow, which I doubt...can anybody explain this? – jpfollenius Oct 06 '11 at 06:28
  • @Smasher As far as I can tell, it's just the number of updates. Using Cells requires 10 times as many "updates" to the grid; one for each cell. Whereas using Grid.Rows requires only one "update" per row. So if you have 10 columns in the grid, you have 10 times as many updates. – awmross Oct 06 '11 at 22:26
  • @LU RD I tried calling BeginUpdate and EndUpdate with my original code. It didn't make a noticeable difference. Not sure? – awmross Oct 06 '11 at 22:47
  • I thought that accessing rowValues[0] on a newly created TStringlist would lead to an index-out-of-range exception. – Warren P Oct 07 '11 at 00:49
  • @Warren yes you are right I didn't post every line of code... only the bits relevant to the question. – awmross Oct 07 '11 at 02:06
  • 1
    It's better to post code that runs, or else to put in ... (ellipsis) to indicate omissions. – Warren P Oct 07 '11 at 18:46
3

FieldByName used in a loop is very slow since it is calculated each time. You should do it out of the loop and then just use results inside of a loop.

avra
  • 3,690
  • 19
  • 19
3

First optimization is to replace very slow Query.FieldByName('Value1') calls by a local TQuery.

var
  F1, F2, F3: TField;

Grid.RowCount := Query.RecordCount;
J := 0;
F1 := Query.FieldByName('Value1');
F2 := Query.FieldByName('Value2');
F3 := Query.FieldByName('Value3');
while not Query.EOF do
begin
    Grid.Cells[0,J]:=F1.AsString;
    Grid.Cells[1,J]:=F2.AsString;
    Grid.Cells[2,J]:=F3.AsString;
    // etc for other columns.
    Inc(J);
    Query.Next();
end;

If this is not enough, use the grid in virtual mode, i.e. retrieve all content in a TStringList or any in-memory structure, then use the OnGetText or OnDrawCell methods.

Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
  • 2
    @ Arnaud, Are you sure that F1 etc are of type TQuery? I would have thought they should be of type TField. – iamjoosy Oct 06 '11 at 16:12
  • 1
    @iamjoosy Of course you are right, they are TField kind. Fixed. Thanks for the report. If only SO did have an inline parser and Delphi compiler to check for our code. ;) – Arnaud Bouchez Oct 06 '11 at 19:24
3

TStringGrid works OK for a small number of records, but don't try it for more than 10.000 records.

We had severe performance problems with TAdvStringGrid from TMS (which is based on Delphi TStringGrid) when loading/sorting/grouping large grid sets, but also when inserting one row at the top of the grid (expanding a grid group node). Also memory usage was high. And yes, I used the beginupdate/endupdate already. Also other tricks. But after diving into the structure of TStringGrid I concluded it could never be fast for many records.

As a general tip (for large grids): use the OnGetText (and OnSetText) event. This event is used for filling the grid on demand (only the cells that are displayed). Store the data in your own data objects. This made our grid very fast (1.000.000 record is no problem anymore, loads within seconds!)

André
  • 8,920
  • 1
  • 24
  • 24
2

I believe it's slow because it has to repaint itself everytime you add a row. Since you are taking the values from a query i think it would be better for you to use a TDBGrid instead.

Best regards.

Cesar
  • 500
  • 5
  • 12
2

If you know how many rows you're about to add, store the current rowcount in a temporary variable, set the grid's rowcount to accommodate the current rowcount plus the rows you're about to add, then assign the new values to the rows (using the former rowcount you stored) rather than adding them. This will reduce a lot of background processing.

Argalatyr
  • 4,639
  • 3
  • 36
  • 62
2

Try testing with AQTime or similar tool (profilers).
Without any code is difficult, but I thinks thar the poor performance is due to FieldByName, not StringGrid.

FieldByName make a liear search:

  for I := 0 to FList.Count - 1 do
  begin
    Result := FList.Items[I];
  ...

If your Dataset have many columns (fields) the performance will still be lower.

Regards.

  • FieldByName really adds up. We've solved several performance problems this way, when there are dozens of fields referenced in a big loop. – Chris Thornton Oct 06 '11 at 12:42
2

I was going to say "why not just use beginupdate/endupdate?" but now I see that the regular string grid doesn't support it.
While googling that, I found a way to simulate beginupdate/endupdate:

http://www.experts-exchange.com/Programming/Languages/Pascal/Delphi/Q_21832072.html

See the answer by ZhaawZ, where he uses a pair of WM_SETREDRAW messages to disable/enable the repainting. If this works, use in conjunction with the "eliminate use of FieldbyName" trick, and it should take no time to draw.

Chris Thornton
  • 15,620
  • 5
  • 37
  • 62
1

Set Grid.RowCount = 2 before the loop then when the loop is finished set the rowcount to the correct value.

That avoids lots of calls to the OnPaint event.

Sirko
  • 72,589
  • 19
  • 149
  • 183
Lenny
  • 11
  • 1
0

In my case it turned out that the Debug build was slow and the Release build was fast - a Heisenbug.

More specifically, FastMM4 FullDebugMode triggered the slowness.

micha137
  • 1,195
  • 8
  • 22