Can I ‘streamline’ this benchmarking code?

I’m using VSCode and Benchmark.NET to do some benchmarking on some F# code.

In this specific case, I’d like to have two types of benchmark: “Full” (RunStrategy.Throughput) and “Quick” (RunStrategy.Monitoring with low iteration count).

For each of these types I’d like to have two sample sizes: “Small” (10,000 samples) and “Large” (1,000,000 samples).

In order to try and avoid repetition of code I tried to implement the benchmarks via an interface but it’s starting to look so ‘messy’ that I’m tempted to just copy and paste some much simpler ‘basic’ benchmark code and make a few changes to that instead.

I currently have the code below which is pretty awful to read (to me at least) and there’s lots of repetition.

Is there any way I can reduce the amount of code while still retaining the same amount of flexibility?

(You can’t run the code as it is because I haven’t included the ancillary code.)

Note: No-one told me to write it this way, it just ‘evolved’ into this to a point where it started working, so I’d be happy to tear it all down and start again if that would lead to an improvement.

open System 
open FSharp.Collections.ParallelSeq 
open BenchmarkDotNet.Running 
open BenchmarkDotNet.Attributes 
open BenchmarkDotNet.Engines 

type IBenchmark =
    abstract member Sequential : unit -> int list 
    abstract member Parallel : unit -> int list 
    abstract member ParallelWithDepth : unit -> int list 
    abstract member ListSort : unit -> int list 
    abstract member ListParallel : unit -> int list 

module Harness = 

    let smallSample = 10_000 
    let largeSample = 1_000_000 

    [<AbstractClass>] 
    type Base() = 

        let rand = Random(int(DateTime.Now.Ticks)) 

        let mutable samples = List<int>.Empty 

        abstract member Sequential : unit -> int list 

        default _.Sequential() = 
            samples |> QuickSort.quicksortSequential 

        abstract member Parallel : unit -> int list 

        default _.Parallel() = 
            samples |> QuickSort.quicksortParallel 

        abstract member ParallelWithDepth : unit -> int list 

        default _.ParallelWithDepth() = 
            samples |> (QuickSort.quicksortParallelWithDepth QuickSort.ParallelismHelpers.MaxDepth) 

        abstract member ListSort : unit -> int list 

        default _.ListSort() = 
            samples |> List.sort 

        abstract member ListParallel : unit -> int list 

        default _.ListParallel() = 
            samples |> PSeq.sort |> PSeq.toList 

        member _.InitialiseSamples num = samples <- List.init num (fun _ -> rand.Next()) 

        interface IBenchmark with 

            member this.Sequential() = this.Sequential() 

            member this.Parallel() = this.Parallel() 

            member this.ParallelWithDepth() = this.ParallelWithDepth() 

            member this.ListSort() = this.ListSort() 

            member this.ListParallel() = this.ListParallel() 

    [< MemoryDiagnoser >] 
    type FullSmall() as this = 
        inherit Base() 
        do 
            this.InitialiseSamples smallSample 
        [<Benchmark>]
        override _.Sequential() = base.Sequential() 
        [<Benchmark>] 
        override _.Parallel() = base.Parallel() 
        [<Benchmark>] 
        override _.ParallelWithDepth() = base.ParallelWithDepth() 
        [<Benchmark>] 
        override _.ListSort() = base.ListSort() 
        [<Benchmark>] 
        override _.ListParallel() = base.ListParallel() 

    [< MemoryDiagnoser >] 
    type FullLarge() as this = 
        inherit Base() 
        do 
            this.InitialiseSamples largeSample 
        [<Benchmark>]
        override _.Sequential() = base.Sequential() 
        [<Benchmark>] 
        override _.Parallel() = base.Parallel() 
        [<Benchmark>] 
        override _.ParallelWithDepth() = base.ParallelWithDepth() 
        [<Benchmark>] 
        override _.ListSort() = base.ListSort() 
        [<Benchmark>] 
        override _.ListParallel() = base.ListParallel() 

    [<
        MemoryDiagnoser;
        SimpleJob(RunStrategy.Monitoring, launchCount = 1, warmupCount = 3, iterationCount = 5)
    >] 
    type QuickSmall() as this = 
        inherit Base() 
        do 
            this.InitialiseSamples smallSample 
        [<Benchmark>]
        override _.Sequential() = base.Sequential() 
        [<Benchmark>] 
        override _.Parallel() = base.Parallel() 
        [<Benchmark>] 
        override _.ParallelWithDepth() = base.ParallelWithDepth() 
        [<Benchmark>] 
        override _.ListSort() = base.ListSort() 
        [<Benchmark>] 
        override _.ListParallel() = base.ListParallel() 
            
    [<
        MemoryDiagnoser;
        SimpleJob(RunStrategy.Monitoring, launchCount = 1, warmupCount = 3, iterationCount = 5)
    >] 
    type QuickLarge() as this = 
        inherit Base() 
        do 
            this.InitialiseSamples largeSample 
        [<Benchmark>]
        override _.Sequential() = base.Sequential() 
        [<Benchmark>] 
        override _.Parallel() = base.Parallel() 
        [<Benchmark>] 
        override _.ParallelWithDepth() = base.ParallelWithDepth() 
        [<Benchmark>] 
        override _.ListSort() = base.ListSort() 
        [<Benchmark>] 
        override _.ListParallel() = base.ListParallel() 

[<EntryPoint>] 

let main args = 

    let lowerArgs = args |> Array.map (fun s -> s.ToLower()) 

    let runType = lowerArgs[0] 
    let sampleLength = lowerArgs[1] 

    let summary = 
        match runType, sampleLength with 
        | "full" , "small" -> BenchmarkRunner.Run<Harness.FullSmall>() 
        | "full" , "large" -> BenchmarkRunner.Run<Harness.FullLarge>() 
        | "quick" , "small" -> BenchmarkRunner.Run<Harness.QuickSmall>() 
        | "quick" , "large" -> BenchmarkRunner.Run<Harness.QuickLarge>() 
        | _ -> failwith (sprintf "!*!* Invalid Arguments: runType = %s , sampleLength = %s" runType sampleLength)  

    summary |> printfn "%A" 

    0

This seems a lot simpler, and I think works (not sure since I don’t have the initial code)

module Harness = 

    let smallSample = 10_000 
    let largeSample = 1_000_000 

    [<AbstractClass>] 
    type Base(numSamples) = 

        let rand = Random(int(DateTime.Now.Ticks)) 

        let samples = List.init numSamples (fun _ -> rand.Next())  

        [<Benchmark>]
        member _.Sequential() =
            samples |> QuickSort.quicksortSequential


        [<Benchmark>]
        member _.Parallel() = 
            samples |> QuickSort.quicksortParallel


        [<Benchmark>]
        member _.ParallelWithDepth() = 
            samples |> (QuickSort.quicksortParallelWithDepth QuickSort.ParallelismHelpers.MaxDepth)

        [<Benchmark>]
        member _.ListSort() = 
            samples |> List.sort 


        [<Benchmark>]
        member _.ListParallel() = 
            samples |> PSeq.sort |> PSeq.toList 

    [< MemoryDiagnoser >] 
    type FullSmall() = 
        inherit Base(smallSample) 

    [< MemoryDiagnoser >] 
    type FullLarge() = 
        inherit Base(largeSample) 

    [<
        MemoryDiagnoser;
        SimpleJob(RunStrategy.Monitoring, launchCount = 1, warmupCount = 3, iterationCount = 5)
    >] 
    type QuickSmall() = 
        inherit Base(smallSample) 
            
    [<
        MemoryDiagnoser;
        SimpleJob(RunStrategy.Monitoring, launchCount = 1, warmupCount = 3, iterationCount = 5)
    >] 
    type QuickLarge() = 
        inherit Base(largeSample) 

That works, thanks.

It looks similar to code that I had before which compiled but didn’t run because I was getting an error, while running the benchmark, along the lines of “There is no public method Sequential in the class FullSmall” (I can’t remember the exact wording).

That’s why I added all of the extra code ‘exposing’ the method names in the classes.

I was using an interface which your version doesn’t (I believe this gets more tricky when interfaces are concerned for some reason or other).

I think I gave the wrong code above but can’t be sure (it’s been through so many slightly different versions).

I might have been trying something like this:

let benchmark = 
        match runType, sampleLength with 
        | "full" , "large" -> Harness.FullLarge 
        | "full" , "small" -> Harness.FullSmall
        | "quick" , "large" -> Harness.QuickLarge 
        | "quick" , "small" -> Harness.QuickSmall
        | _ -> failwith (sprintf "!*!* Invalid Arguments: runType = %s , sampleLength = %s" runType sampleLength)  

BenchmarkRunner.Run<benchmark>() |> printfn "%A"

… but I can’t remember.

It might take me more time to figure out what I did originally than is really necessary, so I think I’ll just go with what you have given me and hope it doesn’t come up again.

Thanks again.