You may all know about String concatenation in Delphi, but do you know about the implicit String variables the compiler may create for you?
Along with the implicit variables come implicit exception frames, and a whole lot of hidden stack juggling, which can quickly become hidden complexity bottlenecks.
Looking Innocent
What’s more innocent-looking than a function like this one:
function Apples(nb : Integer) : String;
begin
Result := IntToStr(nb) + ' apple(s)';
end;
but we all know looks can be deceiving, and sometimes where there are apples, there can be a worm…
Well, here it is, look behind the code at the asm generated for that function:
Project65.dpr.31: begin 005D82B0 55 push ebp 005D82B1 8BEC mov ebp,esp 005D82B3 6A00 push $00 005D82B5 53 push ebx 005D82B6 56 push esi 005D82B7 8BF2 mov esi,edx 005D82B9 8BD8 mov ebx,eax 005D82BB 33C0 xor eax,eax 005D82BD 55 push ebp 005D82BE 68F8825D00 push $005d82f8 005D82C3 64FF30 push dword ptr fs:[eax] 005D82C6 648920 mov fs:[eax],esp Project65.dpr.32: Result := IntToStr(nb) + ' apple(s)'; 005D82C9 8D55FC lea edx,[ebp-$04] 005D82CC 8BC3 mov eax,ebx 005D82CE E8BD5FE4FF call IntToStr 005D82D3 8B55FC mov edx,[ebp-$04] 005D82D6 8BC6 mov eax,esi 005D82D8 B910835D00 mov ecx,$005d8310 005D82DD E85AF9E2FF call @UStrCat3 Project65.dpr.33: end; 005D82E2 33C0 xor eax,eax 005D82E4 5A pop edx 005D82E5 59 pop ecx 005D82E6 59 pop ecx 005D82E7 648910 mov fs:[eax],edx 005D82EA 68FF825D00 push $005d82ff 005D82EF 8D45FC lea eax,[ebp-$04] 005D82F2 E8E9EAE2FF call @UStrClr 005D82F7 C3 ret
That was scary eh? Does it matter in terms of performance? Yep, it is about twice slower than the alternative I will give below, and it’ll be even worse in a multi-threaded setting. In the profiler it will often come out as begin/end being the bottleneck points.
All that extra complexity because IntToStr is a function that returns a String, so the compiler needs a temporary variable to store that String. Ans a String is a reference-counted type, so the compiler needs an implicit exception frame (and yes, with ARC objects in NextGen you get the same kind of exception frames).
Hello Eric!
Does DWScript have functions that avoid UStrAsg, LStrAsg etc. which are toxic for multithreaded applications?
Yes, whenever they appear in the top of the profiling results, and when it’s reasonable complexity-wise.
Interesting, but this seems like premature optimization and reducing the readability of code just to make a string concatenation faster. How fast is the string concatenation now? It’s hard to imagine a single concatenation becoming a bottleneck.
With D7 there is almost no difference between the two methods.
But… this is faster.
function ApplesFormatStr(nb : Integer) : String;
begin
Result := Format(‘%d apple(s)’,[nb]);
end;
@sam: IntToStr was just picked as an example of a simple function returning a string. Under D7 you should see a difference as well (did you test with FastMM or the default D7 memory manager?)
@joseph: Of course you should check with a profiler first 😉
However for readability, this is not so clear-cut, in a more real-world example with longer strings, longer functions names, etc. you’re bound to need line breaks as well, so length-wise, it’ll be similar.
Yet the longer form with explicit assignment and step-by-step concatenation will be more easily debugable and maintainable (more break point locations, ability to inspect intermediate steps easily, more detailed feedback for crash call stacks, etc.).
IMHO it’s one of those case where compact code isn’t really more readable, but is definitely less maintainable and less efficient to boot.