Is C# faster than F#?

Date: 2023-05-24 | create | ctech | fsharp | csharp |

You're an experienced Software Engineer picking a tech stack for your new project. Maybe it's for work. Maybe it's for fun. But you've secretly wanted to try out functional programming for awhile because it's edgy.

But you've got some blockers. While edgy and cool and maybe you'll learn something, is it fit for production? Sure there's plenty of code influencers espousing nonsense about how FP is...

But is it right for your project? Is it the perfect tool in every way? Is it fast enough to handle the 100s of weekly users your project will receive?

In this post we're going to answer these questions:

  • Q1: Is C# faster than F#?
  • Q2: Does it matter?

Executive summary

Q1: Is C# faster than F#?

C# and F# are about the same speed for similar web workloads / benchmarks though in this brief analysis F# came out ahead. Performance varies widely based on different implementations but both run on dotnet so on average see about the same speed.

Data - Average Requests Per Second (web benchmarks):

  • F#: 156k
  • C#: 134k

Q2: Does it matter?

It depends. Is performance a major bottleneck for your stack?

For most, the answer is no. So in this case use the one that fits your product / org / team the best.

Caveats

Before we get into the approach and calculations that led me to these conclusions, I must first lay out a few warnings for the usage of this data / insights.

Approach

There are tons of different benchmarks out there each with their own approaches, pros, and cons. We are going to try to find an approach that minimizes effort but maximizes representative power.

When I'm looking for benchmarks, I'm typically trying to understand what is the median relative performance one might expect from building a system in one language vs another. In particular, I do not care how fast someone was able to make a language go via unsafe, non-mainstream language features (i.e. a non-street-legal race car). Instead I want to understand how a normal, mainstream stack performs (i.e. a stock Honda / Toyota).

We're going to codify this as follows:

  • Goal: Usage-weighted performance
    • langPerformance = SUM( frameworkPerf * frameworkRelativeUsage )
    • frameworkRelativeUsage = frameworkUsage / langUsage
  • Constraints:
    • Web benchmarks only (real-world usecase)
    • Average multiple benchmarks, weight by usage (approximate average stack performance)
  • Data:

This is similar to previous benchmark analyses I've done in the past (see: Top F# Backend Web Frameworks in 2023) but this time we're going to try to weight the averages by estimated usage to try to approximate the performance of the average stack in this language.

Data

I'm limiting the web frameworks we include in the calculations to the ~top 3 cause I'm lazy and this is generally representative (anecdatally top 1 web framework in most languages has significant market share).

C# and F# performance vs usage data

C# Frameworks:

  • Asp.NET
  • Beetlex
  • FastEndoints

F# Frameworks:

  • Giraffe
  • Falco
  • Saturn

Note: I am filtering out both Suave and WebSharper from the F# frameworks due to lack of TechEmpower benchmarks and outlier-like benchmarks in Web Frameworks. This is probably not fair but #besteffort.

Calculations

Running the numbers, we get:

  • F#: 156k
  • C#: 134k

I'll copy my calculation code below if you want to look at it. It got a little big.

Next Steps

That's it. This data is fun but likely not that useful.

Appendix

Code Calculations - using F# notebooks. Learn more in Read CSVs in F# / .NET Interactive Notebooks

#r "nuget: FSharp.Data"

open FSharp.Data
open System
// open Csv

type FrameworkData = {
    AverageWebBenchmarkRequestsPerSecond : int32
    TotalUsage : int32
}

type LanguageData = {
    Name : string
    Frameworks : FrameworkData list
    TotalUsage : int32
}

type LanguageCalculations = {
    Name : string
    AverageWeightedBenchmarkRequestsPerSecond : int32
}

let unwrap (s : string option) : string =
    match s with 
    | Some s -> s 
    | None -> failwith "Expected Some but got None!"

let nonEmptyStringOrNone (s : string) : string option =
    match s with 
    | s when s.Length = 0 -> None 
    | _ -> Some s

let getNumFromString (s : string) : int32 =
    int32 (Math.Round((float s)))

let getLanguageFromRow (row : CsvRow) : string option =
    (nonEmptyStringOrNone row.Columns[0])

let getFrameworkFromRow (row : CsvRow) : string option =
    (nonEmptyStringOrNone row.Columns[1]) 

let getWebBenchmarkFromRow (row : CsvRow) : string option =
    (nonEmptyStringOrNone row.Columns[2]) 

let getTechEmpowerBenchmarkFromRow (row : CsvRow) : string option =
    (nonEmptyStringOrNone row.Columns[3]) 

let getGitHubStarsFromRow (row : CsvRow) : string option =
    (nonEmptyStringOrNone row.Columns[4]) 

let getNugetDownloadsFromRow (row : CsvRow) : string option =
    (nonEmptyStringOrNone row.Columns[5]) 

let getAverageBenchmarkFromRow (row : CsvRow) : int32 =
    ([
        (getWebBenchmarkFromRow row) ;
        (getTechEmpowerBenchmarkFromRow row)
    ])
    |> List.choose (
        fun c -> c
    )
    |> List.map (
        fun b ->
            getNumFromString b
    )
    |> System.Linq.Enumerable.Average
    |> fun a -> (int32 (Math.Round((Math.Floor(a)))))

let getTotalUsageFromRow (row : CsvRow) : int32 =
    (getNumFromString (unwrap (getGitHubStarsFromRow row)))
    + (getNumFromString (unwrap (getNugetDownloadsFromRow row)))

let getFrameworkDataFromRow (row : CsvRow) : FrameworkData = 
    let benchmark = getAverageBenchmarkFromRow row
    let totalUsage = getTotalUsageFromRow row

    {
        AverageWebBenchmarkRequestsPerSecond = benchmark
        TotalUsage = totalUsage
    }

let languageDataFromCsv (csv : CsvFile) : LanguageData list =
    csv.Rows 
    |> Seq.groupBy (
        fun r -> unwrap (getLanguageFromRow r)
    )
    |> Seq.map (
        fun group -> 
            let langName = fst group 
            let frameworkRows = snd group

            let frameworkData = 
                frameworkRows 
                |> Seq.map (
                    fun f ->
                        getFrameworkDataFromRow f
                )
                |> Seq.toList

            let totalUsage = 
                frameworkData 
                |> List.sumBy (
                    fun f -> f.TotalUsage
                )

            {
                Name = langName
                Frameworks = frameworkData
                TotalUsage = totalUsage
            }
    )
    |> Seq.toList

let calculateAverageWeightedBenchmarkRequestsPerSecondFromLangData (langData : LanguageData) : int32 =
    let weightedBenchmarks = 
        langData.Frameworks
        |> List.map (
            fun f -> 
                (float f.AverageWebBenchmarkRequestsPerSecond) * ((float f.TotalUsage) / (float langData.TotalUsage))
        )
        |> List.sum
        |> fun s -> int32 s
    weightedBenchmarks

let calculateLanguageCalculationFromData (langDatum : LanguageData) : LanguageCalculations =
    {
        Name = langDatum.Name
        AverageWeightedBenchmarkRequestsPerSecond = calculateAverageWeightedBenchmarkRequestsPerSecondFromLangData langDatum
    }


let calculate (csv : CsvFile) : LanguageCalculations list =
    let languageDataList = languageDataFromCsv csv
    let allCalculations = 
        languageDataList 
        |> List.map (
            fun l -> 
                calculateLanguageCalculationFromData l
        )
    allCalculations

let logCalculations (calculations : LanguageCalculations list) = 
    let sortedCalculations = 
        calculations 
        |> List.sortByDescending (
            fun l -> l.AverageWeightedBenchmarkRequestsPerSecond
        )

    sortedCalculations 
    |> List.iter (
        fun c -> 
            printfn "%A: AverageRequestsPerSecond = %A" c.Name c.AverageWeightedBenchmarkRequestsPerSecond
    )

let csvFileName = "CsharpFsharpUsageValues.csv"

let csv = CsvFile.Load(
    $"{Directory.GetCurrentDirectory()}/{csvFileName}")

let calculations = calculate csv 
logCalculations calculations

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.