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...
- Simpler
- More testable / maintainable
- Used in Prod at big companies
- Easy to just try out yourself - (Build a simple F# web API with Giraffe)
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.
- "It depends". Every system / usecase is different. These benchmarks will not accurately apply to your usecase so do your own research in your own environment.
- All benchmarks are lies. Even when they don't mean to be. More in: Improving F# / Giraffe web benchmarks by 6.56x
- This doesn't really matter. Performance is rarely the bottleneck in most systems.
- Best effort calculation - this is for fun, my numbers may be wrong.
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:
- Usage:
- Nuget
- GitHub
- Performance:
- Usage:
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# 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.