One situation in which anonymous methods are particularly useful is for ad-hoc profiling and testing scenarios. For both profiling and testing, it is code itself that one wants to work with. I.e. the question you are trying to answer is "test this code", or "time this code". Using code as a parameter is a natural fit for this problem.
I'll start with a very simple profile harness. It does little more than run a code block a number of times and report on how long it took. Here's the definition of the class:
TBenchmarker = class private const DefaultIterations = 3; DefaultWarmups = 1; var FReportSink: TProc<string,Double>; FWarmups: Integer; FIterations: Integer; FOverhead: Double; class var FFreq: Int64; class procedure InitFreq; public constructor Create(const AReportSink: TProc<string,Double>); class function Benchmark(const Code: TProc; Iterations: Integer = DefaultIterations; Warmups: Integer = DefaultWarmups): Double; overload; procedure Benchmark(const Name: string; const Code: TProc); overload; function Benchmark<T>(const Name: string; const Code: TFunc<T>): T; overload; property Warmups: Integer read FWarmups write FWarmups; property Iterations: Integer read FIterations write FIterations; end;
This API has a couple of interesting features. The constructor takes method reference argument indicating where output - the results of the benchmarking - will go. This idiom makes it easy for users of the class to direct the output. It's loosely coupled, just like an event (after all, an event - a method pointer - is almost the same thing as a method reference), but it also doesn't require that the user create a whole method just for the purpose of sending the output off to whereever it ends up (on the console, in a UI, etc.).
Secondly, the Benchmark overloaded methods themselves - these take the actual code to be measured. The Benchmark overloads will execute the passed-in code a number of times for warmup, and then a number of times for the actual measurement. The return value of the class function version is the number of seconds an average iteration took; the instance function versions send off the result to the reporting sink.
These two idioms - data sinks and sources, and taking in a block of code which can be executed a carefully constructed context - are common usage patterns of anonymous methods. I'll get into them further in later posts.
Here's the Benchmark class in practice:
procedure UseIt; const CallDepth = 10000; var b: TBenchmarker; x: TSomeClass; intf: ISomeInterface; begin b := TBenchmarker.Create(procedure(Name: string; Time: Double) begin Writeln(Format('%-20s took %15.9f ms', [Name, Time * 1000])); end); try b.Warmups := 100; b.Iterations := 100; x := TSomeClass.Create; intf := x; b.Benchmark('Static call', procedure begin x.StaticCall(x, CallDepth); end); b.Benchmark('Virtual call', procedure begin x.VirtCall(x, CallDepth); end); b.Benchmark('Interface call', procedure begin intf.IntfCall(intf, CallDepth); end); finally b.Free; end; end;
The benefits of variable capture should be clear here. The segments of code to be benchmarked can freely access variables in the outer scope.
Finally, here's the complete implementation, including TBenchmark definition and use:
program am_profiler; {$APPTYPE CONSOLE} uses Windows, SysUtils; type TBenchmarker = class private const DefaultIterations = 3; DefaultWarmups = 1; var FReportSink: TProc<string,Double>; FWarmups: Integer; FIterations: Integer; FOverhead: Double; class var FFreq: Int64; class procedure InitFreq; public constructor Create(const AReportSink: TProc<string,Double>); class function Benchmark(const Code: TProc; Iterations: Integer = DefaultIterations; Warmups: Integer = DefaultWarmups): Double; overload; procedure Benchmark(const Name: string; const Code: TProc); overload; function Benchmark<T>(const Name: string; const Code: TFunc<T>): T; overload; property Warmups: Integer read FWarmups write FWarmups; property Iterations: Integer read FIterations write FIterations; end; { TBenchmarker } constructor TBenchmarker.Create(const AReportSink: TProc<string, Double>); begin InitFreq; FReportSink := AReportSink; FWarmups := DefaultWarmups; FIterations := DefaultIterations; // Estimate overhead of harness FOverhead := Benchmark(procedure begin end, 100, 3); end; class procedure TBenchmarker.InitFreq; begin if (FFreq = 0) and not QueryPerformanceFrequency(FFreq) then raise Exception.Create('No high-performance counter available.'); end; procedure TBenchmarker.Benchmark(const Name: string; const Code: TProc); begin FReportSink(Name, Benchmark(Code, Iterations, Warmups) - FOverhead); end; class function TBenchmarker.Benchmark(const Code: TProc; Iterations, Warmups: Integer): Double; var start, stop: Int64; i: Integer; begin InitFreq; for i := 1 to Warmups do Code; QueryPerformanceCounter(start); for i := 1 to Iterations do Code; QueryPerformanceCounter(stop); Result := (stop - start) / FFreq / Iterations; end; function TBenchmarker.Benchmark<T>(const Name: string; const Code: TFunc<T>): T; var start, stop: Int64; i: Integer; begin for i := 1 to FWarmups do Result := Code; QueryPerformanceCounter(start); for i := 1 to FIterations do Result := Code; QueryPerformanceCounter(stop); FReportSink(Name, (stop - start) / FFreq / Iterations - FOverhead); end; type ISomeInterface = interface procedure IntfCall(const Intf: ISomeInterface; depth: Integer); end; TSomeClass = class(TInterfacedObject, ISomeInterface) public procedure VirtCall(Inst: TSomeClass; depth: Integer); virtual; procedure StaticCall(Inst: TSomeClass; depth: Integer); procedure IntfCall(const Intf: ISomeInterface; depth: Integer); end; { TSomeClass } procedure TSomeClass.IntfCall(const Intf: ISomeInterface; depth: Integer); begin if depth > 0 then Intf.IntfCall(Intf, depth - 1); end; procedure TSomeClass.StaticCall(Inst: TSomeClass; depth: Integer); begin if depth > 0 then StaticCall(Inst, depth - 1); end; procedure TSomeClass.VirtCall(Inst: TSomeClass; depth: Integer); begin if depth > 0 then VirtCall(Inst, depth - 1); end; procedure UseIt; const CallDepth = 10000; var b: TBenchmarker; x: TSomeClass; intf: ISomeInterface; begin b := TBenchmarker.Create(procedure(Name: string; Time: Double) begin Writeln(Format('%-20s took %15.9f ms', [Name, Time * 1000])); end); try b.Warmups := 100; b.Iterations := 100; x := TSomeClass.Create; intf := x; b.Benchmark('Static call', procedure begin x.StaticCall(x, CallDepth); end); b.Benchmark('Virtual call', procedure begin x.VirtCall(x, CallDepth); end); b.Benchmark('Interface call', procedure begin intf.IntfCall(intf, CallDepth); end); finally b.Free; end; end; begin try UseIt; except on E:Exception do Writeln(E.Classname, ': ', E.Message); end; end.
1 comment:
Hi Barry,
Thanks to your articles it's become more and more clear the use cases for "anonmeths" (why just not naming them closures as other program languages that supports the idiom usually have ?) ... Abstracting the algorithms, the inline plugg-ability and variables capture cover the most obvious usage of closures.
In this regard you can highlight this sort of practice (maybe using some other examples) for various usage for the generic collections mixed with various predicates and algorithms for finders, filtering map/reduce ... etc...
Of course you can "inspire" a lot from the other languages usages (JavScript, C#, (J)Ruby, Groovy) ...
Please keep going with this very interesting series covering the closures and generics ...
Post a Comment