The series of tests conducted here are based on the "String Processing with VFP" article published in the Spring 2000 issue of CoDe Magazine.

All tests are based on the "War and Peace" text, which is about 3.3MB in size. Note that the text version is not necessarily 100% identical to the original used in the article, but this should not have an influence on the results. We also added a few extra tests for more complete results.

The benchmarks described in this article are based on C#. In addition, we provide benchmark code written in VB.NET, to allow the reader to easily experiment with this on his/her own. The included benchmark code uses the VFP Toolkit written by Kamal Patel. This library was chosen because it closely duplicates many functions found in Visual FoxPro. The C# benchmark code presented in this paper uses code similar to the code found in Kamal's Toolkit, We believe that the combination of the two approaches provides a nice overview of multiple available options.

All performance times are given in milliseconds and are taken from the VFP and C# code shown in the tables.

Checking for an existing sub-string

This test checks for the existence of the text "the end" in the entire story.

LanguageTimeCodeVFP78ms? "Find 'the end':"?? "the end" $ cTextVS.Net15msConsole.WriteLine("Find 'the end': " +(sText.IndexOf("the end") > 0).ToString());

Note: VFP's performance varies somewhat here. I got results between 150ms and the measured 78ms. Also, VFP is ALWAYS slower (around 128ms at least) the first time the test runs. .NET does not show any performance differences at all after running this several times (unless the two calls to the IndexOf() method are immediate, in that case the result is 0ms).

Checking for the position of a string

In this test, we ask for the exact position of a string within the text.

LanguageTimeCodeVFP78ms? "Position of 'the end':"?? AT("the end",cText)VS.Net15msConsole.WriteLine("Position of 'the end': "+ sText.IndexOf("the end").ToString());

Note: The VS.NET code here is virtually the same, therefore it makes sense that there isn't any difference in performance. In VFP, the differences to the first example are slim. Performance is very similar, although 78ms is the absolute best I got, and there was a wide range for fluctuations as I ran the test repeatedly.

Checking for the position of a string in a case-insensitive way

This test is very similar to the previous test, except this time I do the search case-insensitive.

LanguageTimeCodeVFP79ms? "Position of 'THE END':"?? AC("THE END",cText)VS.Net62msConsole.WriteLine("Position of 'THE END': "+ sText.ToUpper().IndexOf("THE END").ToString());

Note: The approach taken in .NET is very inefficient, since the search is not done case-insensitive. Instead, the string is converted to its upper-case representation and then searched.

Retrieving 20 Chars beginning at the location of "the end"

This test "kinda" was in the original article, although not really measured performance wise. I did it anyway:

LanguageTimeCodeVFP93msLOCAL lnAtlnAt = AT("the end",cText)? "20 chars after 'the end': " + SUBSTR(cText,lnAt,20)VS.Net15msint iAt = sText.IndexOf("the end");Console.WriteLine("20 chars after 'the end': "+sText.Substring(iAt,20) );

Note: I ran this very test a number of times. Twice I saw the VS test take 31ms, but over 50 times, I saw 15ms. Once again, VFPs performance fluctuated widely, up to 200ms.

Checking for the number of time a certain string occurs in the text (5 different strings total)

This test looks for the number of occurrences of 5 different strings in the entire text, and measures the complete time it takes for that operation.

LanguageTimeCodeVFP469msLOCAL ILOCAL aStrings(5)aStrings(1) = "Russia"aStrings(2) = "Anna"aStrings(3) = "Czar"aStrings(4) = "windows"aStrings(5) = "Pentium"FOR i = 1 TO 5 ? OCCURS(aStrings(i),cText)ENDFORVS.Net156msstring [] aStrings = {"Russia","Anna","Czar", "windows","Pentium"};for (int i = 0;i<5;i++){ int j = 0; int iFound = 0; j = sText.IndexOf(aStrings[i],j); while (j>0) { iFound++; j = sText.IndexOf(aStrings[i],j+1); } Console.WriteLine(iFound.ToString());}

Note that to my knowledge, .NET does not have a native "Occurs" method as VFP does. Therefore, I had to write a simple loop (the while-construct) to get the same functionality. This could easily be put into a function or method, but to keep the sample clear, I just put it into a loop here. (Note: Kamal Patel's VFP Toolkit for .NET has such as method).

Finding the 101st, 201st,...701st instance of the word "Russia"

This test finds the 101st, 201st, 301st, 401st, 601st, and 701st instance of the word "Russia" in the ext, and reports its occurrence.

LanguageTimeCodeVFP250msLOCAL ILOCAL nOccFOR i = 1 TO 7 nOcc = i*100+1 ? AT("Russia",cText,nOcc)ENDFORVS.Net124msfor (int i = 1; i<8; i++){ int iOcc = i*100+1; int j = 0; int iFound = 0; j = sText.IndexOf("Russia",j); while (j>0) { iFound++; if (iFound >= iOcc) break; j = sText.IndexOf("Russia",j+1); } Console.WriteLine(j.ToString());}VS.Net (better)46msint iOcc2 = 101;int j2 = 0; int iFound2 = 0;j2 = sText.IndexOf("Russia",j2);while (j2>0){ iFound2++; if (iFound2 >= iOcc2) { Console.WriteLine(j2.ToString()); iOcc2 += 100; } if (iOcc2 > 701) break; j2 = sText.IndexOf("Russia",j2+1);}

Note: Once again, there is no native method in .NET to do this, so I had to write another loop. Just like in the previous test, the code could be greatly simplified by moving the code in the loop to a separate method.

Note: The first .NET implementation can be well compared to VFP, but it really isn't a sensible implementation for .NET. If I were to look for instances of text the way this example sets it up, there really is no reason to go through previously searched text. In other words, if I was to look for occurrence #201 of "Russia", I can safely ignore the first 101 occurrences, since I have already explored those. Therefore, the second version of the .NET code is much more realistic than the first one. However, VFP does not have the option to do that using the At() function. Therefore, I also provide the first .NET implementation for comparison.

Traversing text line-by-line

In this example, we read the entire text line-by-line.

LanguageTimeCodeVFP375msLOCAL ILOCAL ARRAY laLines(1)LOCAL cLineALINES(laLines,cText)FOR i = 1 TO ALEN(laLines) cLine = laLines(i)ENDFORVFP (safe)Minutes!LOCAL ILOCAL cLine_MLINE = 0FOR i = 1 TO MEMLINES(cText) cLine = MLINE(cText,1,_MLINE)ENDFORVS.Net93msstring [] aLines = sText.Split("\n".ToCharArray());string sLine;for (int i=0;i>aLines.Length;i++) sLine = aLines[i];

Note: As Steve suggests in his article, I am using the ALines() function to retrieve an array of lines, which is the fastest way to do this in VFP. However, the version of War and Peace that I am using for these tests has more than 65000 lines. In VFP arrays can not grow that large. For that reason, I was forced to cut of the epilogs of the book to reduce the number of lines. The two main tests here therefore use a slightly truncated version of the text. In .NET, this limitation does not exist. Obviously, this makes ALines() a somewhat tricky method to use and not safe for large amounts of text. For this reason, I also created a second test that uses MLine() which will work in all instances.

Replacing String Occurrences

In this test, we replace all occurrences of the string "Anna" with the string "the McBride twins".

LanguageTimeCodeVFP281mscText = StrTran(cText,"Anna","the McBride twins")VS.Net62mssText = sText.Replace("Anna","the McBride twins");

This replaces 293 instances of the searched string.

Replacing Characters with strings

In this test, we will replace the character "s" with "ch"

LanguageTimeCodeVFP63mscText = StrTran(cText,"s","ch")VS.Net62mssText = sText.Replace("s","ch");

Note: In VS.Net, this is exactly the same as it is in the previous example. This time however, we have a very close call.

Replacing Characters with Characters

In this test, we will replace the character "s" with the character "z"

LanguageTimeCodeVFP62mscText = CHRTRAN(cText,"s","z")VS.Net31mssText = sText.Replace('s','z');

Note: In VFP this is practically the same as the previous test. In the C# version, the code looks very similar. Note however, that the single-quote indicates a "char" data type, rather than a "string", which calls a different overload of the Replace() method.

Adding text to the existing string

We add the text "<b>Drink Milk!</b>" to the War and Peace text.

LanguageTimeCodeVFP0mscText = cText + "<b>Drink Milk!</b>"VS.Net31mssText += "<b>Drink Milk!</b>";

Wow! Fox is too fast to measure on this one. Amazing! Note that the .NET result is not surprising, as this is not especially optimized in .NET as it is in VFP. (Note, the use of a StringBuilder object would produce different results here. See below for more information on string builders.)

Note however, that the size of the added string doesn't make much of a difference in .NET, while it has a huge impact in VFP. Consider the following test:

LanguageTimeCodeVFP357016ms (almost 6 minutes)cText = cText + cTextVS.Net62mssText += sText;

Note that on an example like this, using a StringBuilder object in .NET results in the same exact performance.

Concatenating Strings

In this example, we build a string of 1,000,000 "Strings are amazingly fast " strings, resulting in 27MB of text.

LanguageTimeCodeVFP>20 Minutes!cText = ""LOCAL iFOR i = 1 TO 1000000 cText = cText + "Strings are amazingly fast "ENDFORVS.Net453msStringBuilder sb = new StringBuilder();for (int i=0;i<1000000;i++) sb.Append("Strings are amazingly fast ");string sText = sb.ToString();

Note: I wasn't able to complete this test in VFP. I terminated my instance of VFP after 20 minutes. I will test this when I have more time...

Conclusion

Both, VFP and .NET, have exceptional string handling capabilities. VFP clearly has the leg up in terms of provided functions. Many tasks that are very easy in VFP require a few lines of code in .NET.

.NET on the other hand has the performance advantage. This comes as no surprise, since .NET compiles into highly optimized code that even gets JIT compiled and optimized on each individual machine the code runs on. In addition, the .NET garbage collector reduces memory management overhead. However, it is important to realize that .NET strings can be memory intensive when used incorrectly. Many novice .NET developers make the mistake of ignoring the StringBuilder object.

Markus Egger and Rod Paddock