Extracting common expressions
This next tip will sound obvious, but it will nicely introduce us to the next topic. Plus, it is a real problem frequently found in production code.
The ExtractCommonExpression demo creates a list box with a mere 1,000 entries, all in the form Author–Title. A click on the Complicated expression button runs a short code which reverts the order of Author and Title in the list box so that it shows entries in the form Title–Author:
procedure TfrmCommonExpression.Button1Click(Sender: TObject);
var
i: Integer;
sw: TStopwatch;
begin
ListBox1.Items.BeginUpdate;
try
sw := TStopwatch.StartNew;
for i := 0 to ListBox1.Count - 1 do
ListBox1.Items[i] :=
Copy(ListBox1.Items[i], Pos('-', ListBox1.Items[i]) + 1,
Length(ListBox1.Items[i]))
+ '-'
+ Copy(ListBox1.Items[i], 1, Pos('-', ListBox1.Items[i]) - 1);
sw.Stop;
Button1.Caption := IntToStr(sw.ElapsedMilliseconds);
finally ListBox1.Items.EndUpdate; end;
end;
The code goes over the list and for each entry finds the '-' character, extracts the first and second part of the entry and combines them back together, reversed. It does that, however, in a terrible copy-and-paste way. The code refers to ListBox1.Items[i] five times while calculating the result. It also calls Pos('-', ListBox1.Items[i]) twice.
In a language with a really good compiler, you could expect that both subexpressions mentioned in the previous paragraph would be calculated only once. Not with Delphi's compiler, though. It has some optimization built in, but Delphi's optimization is far from the level required for such tricks to work. That leaves a burden of optimization on us, the programmers.
The second button in this demo executes the code shown next. This implementation of the same algorithm is not only more readable, but accesses ListBox1.Items[i] only once. It also calculates the position of '-' inside the string only once:
procedure TfrmCommonExpression.Button2Click(Sender: TObject);
var
i: Integer;
s: string;
p: Integer;
sw: TStopwatch;
begin
ListBox1.Items.BeginUpdate;
try
sw := TStopwatch.StartNew;
for i := 0 to ListBox1.Count - 1 do begin
s := ListBox1.Items[i];
p := Pos('-', s);
ListBox1.Items[i] := Copy(s, p + 1, Length(s)) + '-' +
Copy(s, 1, p - 1);
end;
sw.Stop;
Button2.Caption := IntToStr(sw.ElapsedMilliseconds);
finally ListBox1.Items.EndUpdate; end;
end;
Comparing both approaches shows a definite improvement in the second case. The first method uses around 40 ms and the second one around 30 ms, which is 25% faster. The code only times the inner for loop, not the updating of the list box itself, which takes the same time in both cases.
I can recap all this with a simple statement—Calculate every subexpression only once. Good advice but, still, don't exaggerate. Simple expressions, such as i+1 or 2*i are so cheap (in computing time) that extracting them in a subexpression won't speed up the code.