Performance impact of dynamic vs strictly typed language — Javascript vs F#
If you are unfamiliar with the concept of statically typed languages and dynamic languages, I will go over the key points of both concepts and demonstrate the performance impact of both (For web applications).
There are several trains of thoughts about whether dynamic languages are more productive to develop software than statically typed languages.
Let's define properly what it means to be a Static or Dynamic language:
A language is statically typed if the type of a variable is known at compile time
Therefore, a dynamic language would be the complete opposite, which is, a language that the type of a variable IS NOT KNOWN at compile time. (Or just, has no compiler at all, so it’s never known)
Static vs Dynamic programming languages— What are the main difference and which one should I chose?
The main difference between both usually boils down to :
- You usually become less productive (towards the goal of the software) with a Static language because you have to write types and properly define each of your variables types (and functions)
- Static languages have compilers to ensure that the types you define are allowed to be used in the context you call them.
- Static languages will throw errors at compile time/IDE, sometimes pretty shady about how there is a mistype usage (Mainly applies to languages with type inference)
- Dynamic languages don’t compile, they simply just immediately run and never complains about the code you write, allowing faster development cycles
- Dynamic languages crash at runtime, it is usually very hard to test every code path you create leaving uncertainty about your code functionality at all times
You might be wondering what’s the point of a static language if there are so many downsides to it. It slows you down, It makes you write more code to arrive at the same result, you have to deal with compiler complaints…
This graphic elaborate really well the reasons why. As your application grows bigger, you will eventually hit a point where there is just simply too much to test and try. You can’t be possibly testing all the different code path all the time manually, otherwise, you will have to write many unit tests to do that.
You will be compelled to add several guard functions to your dynamic code in order to prevent runtime errors and validate every entry because there is no compiler in the background to check your code and let you know if you messed up something by changing something else.
Rewriting code becomes a scary task, maintaining something done by one of the old devs is complex because you have no idea what is the data structure of each variable.
The cost to use a dynamic language is real when your application gets bigger and it’s not exactly an easy task if you want to swap to another language when your project has been in development for several months/years.
How to approach static language knowing that they are a ‘slower’ option to bring result
Sometime FAST does not mean GOOD. FAST is a common theme nowadays, we like to get things done faster so that we can move on to other things.
FAST food is convenient because you get access to food really quickly, but then you know it wasn't GOOD for your health and you have to pay the consequences.
Building a considerably big application using a dynamic language would be doing a similar choice to eating fast food. Yes, you may have something out there faster, but then you will have to deal with many growing pains over time.
Using a static language help with growing pains such as :
- Fewer bugs reported by users (The only bugs you get are about your own logic that is wrong, not because of some type mismatch issue or missing guards)
- Because more time is spent on thinking about the type architecture, better code usually comes out reducing logic issues
- Refactoring code is a lot less scary, the compiler will complain about a bunch of things (slowing you down in what you are doing), and this is great because you get to fix all issues you create by modifying the code, which ensures the application behave properly — If it compiles, it works!
- A good typing system will enforce you to handle more cases than you would normally do, resulting in a better user experience
- Types allow future developers to understand your code a lot easier, the structure is well defined and that becomes documentation in the code itself.
You might be getting faster results with a dynamic language, which is fine if you are building something small that won't expand into a whole business with teams of developers working and maintaining it.
However, if you want your application to be reliable and easy to maintain over time, you should really consider your choice of language as it’s not always an easy task to change your codebase.
Benchmarking a dynamic language (Javascript) against a Strictly typed language (F#/Fable) — Both for the web.
In order to write good and secure dynamic code, you often need to write down guard functions before checking something in order to prevent runtime errors.
In a static language, you need a lot less of those, so I wanted to benchmark and see how much of a difference you get in term of performance by choosing to use a strictly typed language.
Let’s test a simple Javascript function and compare its performance
For the purpose of the test, I have written a simple function that generates employee objects, insert them into an array and then add up the age of all employee to finally determine the average age. I had to write a couple of guards in my Javascript to ensure whoever calls my function must call it properly (and prevent runtime crash).
In order to properly test and see how well this code runs, there is a test bench included in the code to gather timing data for couple use cases. We will do the same testing for the same functions written with F# as well.
function getAverageAgeOfEmployees(employees) {
// Ensure we receive the proper input
if (Array.isArray(employees) === false) {
throw new Error('You must provide an array')
}
var calculatedScores = 0
for (var i = 0; i < employees.length; i++) {
// Validate that the age we received is a number to prevent runtime crashes
if (
employees[i] &&
employees[i].age &&
typeof employees[i].age !== 'number'
) {
throw new Error('All employee age must be a number!')
} else {
calculatedScores += employees[i].age
}
}
return calculatedScores / employees.length
}
function generatePeople(num) {
var arr = []
for (var i = 0; i < num; i++) {
arr.push({
age: Math.floor(Math.random() * 101),
})
}
return arr
}
function testBench(num) {
var start = new Date()
getAverageAgeOfEmployees(generatePeople(num))
var end = new Date()
var difference = (end - start) / 1000
console.log(
'The time it took to process ' +
num +
' person was ' +
difference +
' seconds'
)
}
testBench(9999)
testBench(99999)
testBench(1000000) //1 million
Let’s test the same function written in F# and transpiled to Javascript and run the same set of performance test on the generated code.
For this use case, I have decided to use F# with Fable (It’s a compiler that transpile F# code to Javascript code). You will find the same adapted code below.
Since F# is a statically typed language, we don’t have the same need of guard functions due to the compiler and our types protecting us from some of those issues.
The code we will use to run our tests
module AverageEmployeeAge
type Person = {
age: int
}
let generatePeople num =
let random = System.Random()
Array.init num (fun index -> {age = random.Next(1, 100)})
let getAverageAgeOfEmployees (employees: Person array) =
let total =
Array.fold (fun acc empl -> acc + empl.age) 0 employees
total / employees.Length
let testBench num =
let start = System.DateTime.Now
let people = generatePeople num
ignore <| getAverageAgeOfEmployees people
let endTime = System.DateTime.Now
let timeTook = (endTime - start).TotalSeconds;
printfn "The time it took to process %i person was %f seconds" num timeTook
testBench 9999
testBench 99999
testBench 1000000 //1 million
F#/Fable TestBench for validating timings and performance
View, run and compile the F# code yourself! You can find your own timings here by running the code.
The final results
In order to properly benchmark my tests, I have run three different tests
- Fable with F# compiled to Javascript
- The guarded Javascript version of the code shown above
- The unguarded Javascript version of the code shown above
I have run all the three different test on the same hardware 10 times to gather data and build up a graphic. The graphic shows the average time it took to execute each function on my hardware.
I have dropped the Test #1 and Test #2 from the graphic because the difference was too little to be of use.
You will notice that Fable and the Unguarded Javascript version of the test both perform on very similar performance level on the 1 million tests, but a significant difference for the guarded Javascript one (an increase of around 100ms).
For the 10 million tests, you will notice that Fable eventually as well drop a little bit in performance compared to the UNGUARDED Javascript version, but is still up by a marginal difference compared to the GUARDED version of the test (~600ms).
Following normal development pattern, you will usually build single-responsibility components guarded because you build re-usable components that will be used in multiple contexts for which you want to properly handle errors and ensure it is working properly.
Unfortunately, in Javascript, you have no other choice to write actual runtime executed code to validate those entries, which turns out to be costly down the line, but if you think you are above all else and can write unguarded Javascript code, then you will get slightly better performance out of it (Since Fable has some imported libraries used for certain data manipulation).
In term of a normal comparison (guarded vs guarded), we can see here that for the security you get from a strictly typed language, you gain a considerable amount of performance compared to writing guard functions all over the place in your code.
We have run a pretty simple setup here, imagine the impact this actually have on a much larger application that does things a lot more complicated than this.
Because we have less guarding code to write, static languages boast better performance (at least for the ones that transpiles to Javascript on the web) and that eventually, that comes to play a role in the experience the user has with your application.
It is certainly a cost to take when developing with a strictly typed language today, but it’s a cost that covers for itself more than once in the future when you don’t have to deal with all the issues that it a dynamic language would bring you.
Take the extra 10–20 minutes to think about your structure, type it properly, do the job correctly, it will go a long way into building stronger, faster and more reliable applications.
I hope this article will be a good starting point to many and invite them to do further investigations when choosing their architecture and programming language next time a new project begins.
It is also very possible for an ongoing project to slowly adopt a typed language by creating micro-services that are imported into your current codebase, slowly chipping pieces out until you have migrated fully.