Setup
To have a hands on experience of different speeds between different programming languages, I decided to see how fast different languages would calculate and store the prime numbers between 1 and 10,000,000. The seven languages I choose are:
- C
- Go
- Java
- JavaScript
- Python
- Ruby
- Rust
To set this up, I first wrote the following python code:
import math
from datetime import datetime
def is_prime(num):
if num < 2:
return False
for i in range(2, int(math.sqrt(num)) + 1):
if num % i == 0:
return False
return True
start = datetime.now()
primes = []
for num in range(int(1e7)):
if is_prime(num):
primes.append(num)
print('Python: ', datetime.now() - start)
The code determines whether an integer n is prime or not by checking if any of the integers in the range [2, sqrt(n)] has a quotient with a zero remainder. Then, it iterates through the integers in the range [1, 10,000,000], storing the integer if it is indeed prime.
Although I'm not interested in viewing or using the list of prime numbers, it is import to store the information so that the language's memory management is tested.
Then, using ChatGPT, I converted this Python code into the other 6 languages.
C:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#include <time.h>
bool is_prime(int num) {
if (num < 2) {
return false;
}
for (int i = 2; i <= sqrt(num); i++) {
if (num % i == 0) {
return false;
}
}
return true;
}
int main() {
clock_t start = clock();
int *primes = malloc(10000000 * sizeof(int));
int count = 0;
for (int num = 2; num < 10000000; num++) { // Start at num=2, not num=0
if (is_prime(num)) {
primes[count++] = num;
}
}
printf("C: %f seconds\n", ((double)clock() - start) / CLOCKS_PER_SEC);
free(primes); // Free the dynamically allocated memory
return 0;
}
Go:
package main
import (
"fmt"
"math"
"time"
)
func isPrime(num int) bool {
if num < 2 {
return false
}
for i := 2; i <= int(math.Sqrt(float64(num))); i++ {
if num%i == 0 {
return false
}
}
return true
}
func main() {
start := time.Now()
primes := []int{}
for num := 0; num < int(1e7); num++ {
if isPrime(num) {
primes = append(primes, num)
}
}
fmt.Println("Go: ", time.Since(start))
}
Java:
import java.time.Duration;
import java.time.Instant;
import java.lang.Math;
import java.util.ArrayList;
import java.util.List;
public class primes {
public static boolean isPrime(int num) {
if (num < 2) {
return false;
}
for (int i = 2; i <= Math.sqrt(num); i++) {
if (num % i == 0) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Instant start = Instant.now();
List<Integer> primes = new ArrayList<Integer>();
for (int num = 0; num < 10000000; num++) {
if (isPrime(num)) {
primes.add(num);
}
}
System.out.println("Java: " + Duration.between(start, Instant.now()).toMillis() + " ms");
}
}
JavaScript:
function isPrime(num) {
if (num < 2) {
return false;
}
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) {
return false;
}
}
return true;
}
const start = new Date();
const primes = [];
for (let num = 0; num < 1e7; num++) {
if (isPrime(num)) {
primes.push(num);
}
}
console.log(`JavaScript: ${(new Date() - start)/1000} seconds`);
Ruby:
def is_prime(num)
return false if num < 2
(2..Math.sqrt(num).to_i).each do |i|
return false if num % i == 0
end
true
end
start = Time.now
primes = []
(0...1e7.to_i).each do |num|
primes << num if is_prime(num)
end
puts "Ruby: #{Time.now - start} seconds"
Rust:
use std::time::{Instant, Duration};
use std::f64::consts::SQRT_2;
fn is_prime(num: u32) -> bool {
if num < 2 {
return false;
}
for i in 2..=((num as f64).sqrt() as u32) {
if num % i == 0 {
return false;
}
}
true
}
fn main() {
let start = Instant::now();
let mut primes = Vec::new();
for num in 0..10_000_000 {
if is_prime(num) {
primes.push(num);
}
}
let end = Instant::now();
let time_elapsed = end - start;
println!("Rust: {:?}", time_elapsed);
}
Results
Language | Race 1 | Race 2 | Race 3 | Average |
---|---|---|---|---|
C | 8.9s | 8.9s | 8.9s | 8.9s |
Go | 11.8s | 11.6s | 11.6s | 11.7s |
Java | 4.6s | 4.7s | 4.6s | 4.6s |
JavaScript | 4.4s | 4.4s | 4.4s | 4.4s |
Python | 99.5s | 88.9s | 88.2s | 92.2s |
Ruby | 64.4s | 68.6s | 64.4s | 65.8s |
Rust | 25.2s | 25.3s | 25.3s | 25.3s |
Winner: JavaScript
Slowest: Python (20x slower than JavaScript)
Discussion
What can we conclude from this test? Well, what we can't conclude is that JavaScript is twice as fast as C, nor that Python is 10x slower than C. We can deduce that for calculating prime numbers in this way, Java and JavaScript would be excellent choices with regards to speed and Python and Ruby would be relatively poor choices. It is important to note that this was just testing one application of these languages, and does not give a clear picture to the overall efficiency of the language. Clearly, Python and Ruby are significantly slower than Java/JavaScript/C, so it may be safe to assume that these languages are slower in general. Indeed, one of Python's biggest criticisms and reasons it is not used is that it is slow.
An important factor in the discussion of language speed is static typing versus dynamic typing. In short, static typing means that the type of a variable is checked at compile-time, and dynamic typing means that the type of a variable is checked at run-time. The languages tested that are statically typed are C, Go, Java, and Rust, where JavaScript, Python, and Ruby are dynamically typed.
Why is JavaScript so much faster than the other two dynamically typed languages? While there are factors other than typing that affect speed, this difference is largely due to a concept called Just-In-Time (JIT) Compilation link. It is partly because JavaScript is so widely used and popular that the large community has developed this and other features of JavaScript that help speed it up.
Top comments (5)
The numbers don't make sense. JITs take time and Rust should be closer to C. Looking at the rust code it seems that for C you pre-allocate the result heap but in rust you use a vector which will mean it will constantly need to resize the heap.
In Java you used
ArrayList<Integer>
which isn't the equivalent of the C code soint[]
pre-allocated should be faster and closer to the C style.Still, I don't get why the C code will perform so badly. Did you try with O2, O3 etc.?
Thank you for your comment and for pointing out some potential issues with my experiment. As I mentioned in the post, there are many factors that can influence the performance of code, and I maintain that the results I presented should be taken with a grain of salt.
I appreciate your observations about the differences in the implementation of the code across languages, and I agree that this could have contributed to the discrepancies between C, Rust, and Java. I did not even think about this when I generated all the other codes from Python using ChatGPT. The points you made are certainly valid factors to consider when comparing the performance of these languages.
Additionally, I acknowledge that my use of the standard gcc compiler for C may not have been the most optimal choice. I did not even consider choice of compiler, but this could certainly be an interesting avenue for future investigation.
Rust is so slow b/c it default to be build on debug mode.
On my system (Intel i5-7600K/Fedora 37/rustc 1.69,go 1.20) the differences are very clear:
23.3s
, pretty much what you have-C opt-level=1
it goes down to4.1s
-C opt-level=2
it goes down to4.0s
-C opt-level=1 -C target-cpu=native
it goes down to3.7s
-C opt-level=2 -C target-cpu=native
it goes down to4.1s
.For Go you need to switch to integer type
uint32
.On my system that brought the time down from
13s
(similar to yours) to3.6s
.I am surprised you didnt realise these numbers had to be wrong. Rust being 6 times slower than Javascript ? :)
I am a total Rust newbie so was messing around with a Prime number generator in Rust, A bog standard chatgpt prime number gen to 10 million running on a Ryzen 5800 gave me about 2.3 seconds in release mode.(not 25.3 Secs) I agree with other post, you were testing in debug mode.
A few mins more tinkering with a simple optimisation of the rust code got it down to about 1.3 Secs. Then adding a dependency of Rayon (parallel lib) and a one word change of iter to par_iter in the loop got it down to run time of 160 mS for primes up to 10 million. Slight improvement on 25 Secs
Next day --> Did a bit more tinkering and arguing with chatgpt to optimise the Rust prime generator. Using a Sieve method, I got the time down for primes to 10 million to 25.0 milliseconds. Yay, a 1000 fold speed up from the above figure in the table :)
As it was easy to do for me, thought I would throw in another couple of results running on my 5800x.
Lua 5.1 took 13.6 Seconds
Luajit (Based on Lua 5.1) took 0.97 Seconds
Pretty remarkable result from Luajit