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.