Essay - Published: 2023.05.24 | create | csharp | ctech | fsharp |
DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
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#?
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):
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.
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.
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:
langPerformance = SUM( frameworkPerf * frameworkRelativeUsage )frameworkRelativeUsage = frameworkUsage / langUsageThis 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.
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# Frameworks:
F# Frameworks:
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.
Running the numbers, we get:
I'll copy my calculation code below if you want to look at it. It got a little big.
That's it. This data is fun but likely not that useful.
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
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.