A colleague of mine directed me to a further minor refinement of the ‘final’ Exchange() code I posted the other day. The change is minor but yields a worthwhile performance improvement, but my main reason for bothering to post (yet!) another update is an excuse to introduce the testing framework I developed that allowed me to quickly assess any benefit.
First The Epilogue
The suggestion made was to replace the triplet of CopyMemory() calls with a byte-wise copy routine, so this code replaces that in the case else :
ap8 := PByte(@A); bp8 := PByte(@B); i32 := aSize; while (i32 > 0) do begin i8 := ap8^; ap8^ := bp8^; bp8^ := i8; Inc(ap8); Inc(bp8); Dec(i32); end;
And the declaration of the p: Pointer variable is replaced by two variables:
ap8, bp8: PByte;
I suspect that for extremely large amounts of data CopyMemory() is likely to prove faster but I think the likelihood that this routine will see extensive use in such cases is remote enough not to worry.
Assessing The Impact
To see what difference the code change had I ran some performance test cases, installed the suggested replacement code and then re-ran the tests. The results of the original code, using CopyMemory() came out as follows:
Which also demonstrates the significant performance advantage that those local variable special cases provide. Replacing the CopyMemory() code with the byte-wise copy obviously had no impact on the fixed-size test cases, but led to a doubling of efficiency in the case of the record data type (12 bytes in size in the test case):
The slight variation in results in cases 3.1 thru 3.5 were typical variances seen from one run of tests to another. The doubling of the result for the 3.6 case though showed that the improvement was real. So this is the version that will now take up residence in my library.
In addition to the performance test results, this second image also shows the output confirming that the changes had not broken the implementation in in the case of a large record type (2.6 Exchange_Record).
I should mention that the routine in it’s final state I think now very closely resembles a Pascal version of the ASM routine that PhiS kindly posted in the comments.
But as Joe mentioned, also in the comments, this routine, whilst useful, is hardly something around which entire systems are built, and for the sorts of uses to which I typically put it I have now spent enough time trying to eek CPU cycles out of it.
So instead I thought I’d quickly introduce SmokeTest – my own testing framework.
Oh No, Not Another Testing Framework ?
Yes, I’m afraid so.
What was wrong with DUnit, for example? Nothing, probably. Except that the last time I looked at it it all looked a little too complicated to get going for something that should have been – or should at least have seemed – a lot more straightforward than it appeared it was.
Implementing my own framework was also going to provide me with a reason to work on some other areas of interest – Threading and RTTI – and I could immediately see how my multi-cast events were going to come into the picture.
As it turned out, even the simple GUI was to be the source of some inspiration.
Skip To The End
I said that getting started with DUnit seemed to complicated to me, so after all my effort how did my results compare? This is jumping ahead quite a bit, to a finished testing framework but it shows the level of simple, intuitive code that I expected of a testing framework. It was my goal, as well as my end result.
Here’s the source for my Exchange() test project:
program Smoketest.Exchange; uses Deltics.SmokeTest, Test.Exchange; begin TestSuite.Initialize; TExchangeTest.Create; TExchangePerformance.Create(2, pmSeconds); TestSuite.Ready; end.
Nothing too controversial here. We initialize the TestSuite (there are parameters we can use to tailor a particular suite, but in this case I’m using defaults).
One big difference though is that I then create my test cases directly, no need to create suites to contain them. The framework will take care of that for me.
The first real and significant difference is the availability to me of performance tests, as well as regular tests. These tests do require constructor parameters where I provide some number N and indicate how that N is to be treated, allowing me to specify whether the methods in that performance test case should run for N seconds or for N iterations.
The call to TestSuite.Ready summons my GUI console from where I may then run the tests and view results etc.
The Test Cases
So much for the project. What about the test cases themselves?
unit Test.Exchange; interface uses Deltics.SmokeTest; type TExchangeTest = class(TTestCase) procedure Exchange_Byte; procedure Exchange_Int64; procedure Exchange_Integer; procedure Exchange_Word; procedure Exchange_Extended; procedure Exchange_Record; end; TExchangePerformance = class(TPerformanceCase) procedure Exchange_Byte; procedure Exchange_Int64; procedure Exchange_Integer; procedure Exchange_Word; procedure Exchange_Extended; procedure Exchange_Record; end;
Again, nothing much to discuss here, except that as with the project itself there is nothing extraneous. There are test cases and test methods and that’s all. No housekeeping, no infrastructure. In short, no clutter.
As you might expect, the implementation of each of the test case methods are very similar to each other, so just to give an idea here are one test case method and one performance case. First a test case method:
procedure TExchangeTest.Exchange_Integer; var iA, iB: Integer; begin iA := INT_A; iB := INT_B; Exchange(iA, iB, sizeof(iA)); Test['iA'].Expect(iA, vrEqual, INT_B); Test['iB'].Expect(iB, vrEqual, INT_A); end;
A pretty predictable test. First I place some recognisable values in two integer variables (INT_A and INT_B are unit constants). I then call Exchange(), the method under test, and finally test for the expected results. The details of testing results can wait for another time.
A performance case is even simpler since there is no test framework code involved, just code whose performance I wish to test:
procedure TExchangePerformance.Exchange_Integer; var iA, iB: Integer; begin Exchange(iA, iB, sizeof(iA)); end;
Notice that I don’t even initialise the variables in this case let alone test the results. My TExchangeTest is designed to test correctness. TExchangePerformance is exercising code to determine it’s efficiency so there needs to be a minimum of “fuss” to muddy the results.
There’s also no timing or measurement code – that’s all taken care of by the framework.
Where’s This All Going?
As I said previously, implementing my own testing framework gave me a platform on which to explore a number of areas I’d been mulling over, and talking about the testing framework will lead us into those areas.
Even if the testing framework isn’t directly of interest to you, either because you already use another framework or just don’t do such tests, the areas that it takes us into may be.
Threading – The test suite runs in it’s own thread, separate from the GUI. Rather than use the VCL TThread, I created for myself a wholly new encapsulation of threading. The threading implementation uses states and multicast notifications. In this implementation a simple thread is restartable without involving a thread pool. This is not the TMotile that I have previously mentioned – that evolved some time later.
Published Methods – Extracting RTTI from classes is straightforward, but can be a bit messy especially in the case of published methods.
GUI Consistency – Not as powerful as skinning, but ensuring a consistent look across a GUI and enabling that look to be changed easily. It involves multicast events (again), and devises a way to develop VCL controls that are able to synchronize cosmetic properties with each other globally or in groups.
So… when will you release SmokeTest as open source? 🙂
Hello Jolyon,
You mentioned that the ASM routine was a bit slower. Just wondering whether that turned out to be consistent or within what would be expected from stochasticity, and whether that was true when testing with simple types or when trying to exchange larger data structures, or both. Just curious.
@Denis – just as soon as I’ve knocked it into acceptable shape for public consumption and once I’m happy to go ahead with codeplex as my source hosting provider.
🙂
@PhiS – it did seem consistent, but actually compared to the eventual implementation there’s not a lot in it now – I’ve stuck with the Pascal version simply because it’s clearer to me, being only a bear of little brain.
Once I’ve got SmokeTest into a fit state for release (see above) you’ll be able to plug it in and perhaps improve on it if you’re interested to do so.
🙂
I just tried the static class method approach and it works quite well:
type
TReplacer = class
public
class procedure Exchange(var A, B: T); static;
end;
class procedure TReplacer.Exchange(var A, B: T);
var
Temp: T;
begin
Temp := A;
A := B;
B := Temp;
end;
In Consumer code you would then use:
TReplacer.Exchange(I, J);
Of course, not perfect but at least it works with every data type you can come up with…
Hi Daniel,
Yes, a static, generic class method works, but has two problems for me:
1) Requires Delphi 2009 or later.
2) Requires a class purely to contain the method – this adds unnecessary cumber to the usage. Typically the amount of “usage” code is far greater than the implementation code, so a lighter implementation that adds weight to the usage is getting priorities back assward).
Furthermore it simply shouldn’t be necessary in Win32 Delphi – it may be necessary because of the way generics have been implemented, but that is not the same as saying that it *is* necessary.
This is just one aspect of the generics implementation that I am finding to be increasingly idiosynchratic and frustratingly limited.
Note that although I wrote more about the 2nd problem, the 1st is actually the more significant to me right now.
Thansk for replying. CodeGear can’t fix problem #1 anymore, so let’s talk about #2 instead 😉
IMO the static class function has one advantage: You can’t pass a wrong type, because the compiler will tell you. The size parameter however can get very wrong if used incorrectly (sometimes compiler guys decide to change things…like double the size of Chars 😉 ) Plus I *think* your implementation doesn’t work with strings atm.
Hello again Daniel,
Yes, your observation w.r.t type safety and the correctness of “size” are perfectly valid, although the chances of that latter mistake are eliminated if you sizeof() one of the params, rather than the type.
Personally I’m willing to accept some responsibility for getting my code right, and am prepared to undertake that in return for less drudgery on my part.
Exchange(a, b, sizeof(a));
vs
TSomeClass.Exchange(a, b);
The biggest problem with a generics based approach is the fact that neither the implementation – nor any code that uses it – is portable to older Delphi versions.
Incidentally, it does handle strings. A variable of type “String” is actually of type “Pointer” so is handled by the default “common case” of a value type size of 4.
Note to self: 64-bit pointers will affect this!
It is as I wrote in https://www.deltics.co.nz/blog/?p=338#comment-234:
The generic approach would be fine if there was a “common case”.
De facto my trial version of Delphi 2009 is not smart enough for generics to generate common code (e.g. for integer, pointer or any other 4 byte entities) or to implement inlining.
Do you have a better Delphi?
Also, I would not like to always specify the type such as in
There should be a way to let the compiler do this work like the C++ compilers do.
By the way: Strings are thereby changed the hard way, i.e. there is a real temporary string variable with all the reference counting and finally stuff.
Argh! Even surrounding the example with
does not work.
Here a test. What about the following:?
TSomeClass<T>.Exchange(a, b); // html
TSomeClassT.Exchange(a, b); // lt and gt in angle brackets