Delphi Case with Strings

Zarko Gajic posted about this topic on his Delphi Tips recently showing how one could use a case statement with strings. His solution basically passes in an array on the stack and then iterates through it providing the index number of the matching string. I don't often want to do this, but the idea comes up occassionally enough that I thought I'd play with it a little.

The first thing that struck me with this is that passing things on the stack is bound to be slow. Any time you can avoid memory allocation in your routines, do it. The way Zarko wrote his StringToCaseSelect routine created a COPY of the information on the stack. In my testing, just changing the CaseList: array of string to CaseList:const array of string improved the performance of the code by almost 30% for his example. Mind, I'm not using hyper-precise counters; however, it definitely makes a difference.

Secondly, I was curious how the performance changed if I used the old stand-by: If then else. Using the same testing routine (and imprecise timers), If Then Else is 368% percent faster than the original, non-const version and 283% faster than the const version. That's a pretty hefty speed penalty to pay just for using a case with strings.

procedure TForm1.standardIF;
const strAb = 'About';
strBl = 'Borland';
strDl = 'Delphi';
var s:string;
if s=strAB then begin end
else if s=strBl then begin end
else if s=strDl then begin end

Finally, I started to wonder if there wasn't a faster way to do this. It is likely the iteration and memory operations that are consuming so much time in his example. Trying to reduce this lead me back to everyone's favorite string solution: the hash. CodeGear has a very simple hash routine they use in inifiles (TStringHash.HashOf). Taking that code, I tried using it as a simple

case HashOf('Delphi') of
HashOf('About') : begin end;
HashOf('Borland') : begin end;
HashOf('Delphi') : begin end;

but forgot that the items must be constants. Since Delphi doesn't have a precompiler, that leaves me with the ugly task of generating the hash values for each string and then using those constants in my case. That provides me with the following code:

procedure TForm1.delphiHash;
const cnAb = 24272; //HashOf('About');
cnBl = 389836; //HashOf('Borland');
cnDl = 92361; //HashOf('Delphi');
//from delphi hash inifiles
case HashOf('Delphi') of
cnAb : begin end;
cnBl : begin end;
cnDl : begin end;

That's quite a bit of work just to use a case with a string, but the performance actually does mean it might be worthwhile. In my tests, I found the case hash style ran 204% faster than the if then else, 577% faster than the const version of StringCaseSelect and 750% faster than the original StringCaseSelect.

Even though all of that is true, I have to admit that the performance difference on a modern CPU wasn't visible until I ran up to the 10,000,000 iterations or more. If you have a rarely-used routine, you could use which ever method suits you. If you have one that gets hammered a lot, the If Then Else style makes the most sense. I cannot really see the point where the payback for doing all the work of the precompiler makes a lot of sense to using the case hash style.

Of course, in my own development I rarely run into a case string need so perhaps you have a need that does make it worth while. If so, keep in mind that a hash does not guarantee uniqueness. It's unlikely you'll hit a duplicate, but not impossible. The less random the data, the more unlikely duplicates become.