Hello world

Published 2020-01-04 on Drew DeVault's blog

Let’s say you ask your programming language to do the simplest possible task: print out “hello world”. Generally this takes two syscalls: write and exit. The following assembly program is the ideal Linux x86_64 program for this purpose. A perfect compiler would emit this hello world program for any language.

bits 64
section .text
global _start
_start:
	mov rdx, len
	mov rsi, msg
	mov rdi, 1
	mov rax, 1
	syscall

	mov rdi, 0
	mov rax, 60
	syscall

section .rodata
msg: db "hello world", 10
len: equ $-msg

Most languages do a whole lot of other crap other than printing out “hello world”, even if that’s all you asked for.

Test case Source Execution time Total syscalls Unique syscalls Size (KiB)
Assembly (x86_64) test.S 0.00s real 2 2 8.6 KiB*
Zig (small) test.zig 0.00s real 2 2 10.3 KiB
Zig (safe) test.zig 0.00s real 3 3 11.3 KiB
C (musl, static) test.c 0.00s real 5 5 95.9 KiB
C (musl, dynamic) test.c 0.00s real 15 9 602 KiB
C (glibc, static*) test.c 0.00s real 11 9 2295 KiB
C (glibc, dynamic) test.c 0.00s real 65 13 2309 KiB
Rust test.rs 0.00s real 123 21 244 KiB
Crystal (static) test.cr 0.00s real 144 23 935 KiB
Go (static w/o cgo) test.go 0.00s real 152 17 1661 KiB
D (dmd) test.d 0.00s real 152 20 5542 KiB
D (ldc) test.d 0.00s real 181 21 10305 KiB
Crystal (dynamic) test.cr 0.00s real 183 25 2601 KiB
Go (w/cgo) test.go 0.00s real 211 22 3937 KiB
Perl test.pl 0.00s real 255 25 5640 KiB
Java Test.java 0.07s real 226 26 15743 KiB
Node.js test.js 0.04s real 673 40 36000 KiB
Python 3 (PyPy) test.py 0.68s real 884 32 9909 KiB
Julia test.jl 0.12s real 913 41 344563 KiB
Python 3 (CPython) test.py 0.02s real 1200 33 15184 KiB
Ruby test.rb 0.04s real 1401 38 1283 KiB
* See notes for this test case

This table is sorted so that the number of syscalls goes up, because I reckon more syscalls is a decent metric for how much shit is happening that you didn’t ask for (i.e. write("hello world\n"); exit(0)). Languages with a JIT fare much worse on this than compiled languages, but I have deliberately chosen not to account for this.

These numbers are real. This is more complexity that someone has to debug, more time your users are sitting there waiting for your program, less disk space available for files which actually matter to the user.

Environment

Tests were conducted on January 3rd, 2020.

For each language, I tried to write the program which would give the most generous scores without raising eyebrows at a code review. The size of all files which must be present at runtime (interpreters, stdlib, libraries, loader, etc) are included. Binaries were stripped where appropriate.

This was not an objective test, this is just an approximation that I hope will encourage readers to be more aware of the consequences of their abstractions, and their exponential growth as more layers are added.

test.S

bits 64
section .text
global _start
_start:
	mov rdx, len
	mov rsi, msg
	mov rdi, 1
	mov rax, 1
	syscall

	mov rdi, 0
	mov rax, 60
	syscall

section .rodata
msg: db "hello world", 10
len: equ $-msg
nasm -f elf64 test.S
gcc -o test -static -nostartfiles -nostdlib -nodefaultlibs
strip test: 8.6 KiB

Notes

test.zig

const std = @import("std");

pub fn main() !void {
    const stdout = try std.io.getStdOut();
    try stdout.write("hello world\n");
}
# small
zig build-exe test.zig --release-small --strip
# safe
zig build-exe test.zig --release-safe --strip

Notes

test.c

int puts(const char *s);

int main(int argc, char *argv[]) {
    puts("hello world");
    return 0;
}
# dynamic
gcc -O2 -o test test.c
strip test

# static
gcc -O2 -o test -static test.c
strip test

Notes

test.rs

fn main() {
    println!("hello world");
}
rustc -C opt-levels=s test.rs

Notes

test.go

package main

import "os"

func main() {
    os.Stdout.Write([]byte("hello world\n"))
}
# dynamic
go build -o test test.go

# static w/o cgo
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o test -ldflags '-extldflags "-f no-PIC -static"' -buildmode pie -tags 'osusergo netgo static_build' test.go

Aside: it is getting way too goddamn difficult to build static Go binaries.

Notes

Test.java

public class Test {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
javac Test.java
java Test

test.cr

puts "hello world\n"
# Dynamic
crystal build -o test test.cr

# Static
crystal build --static -o test test.cr

Notes

test.js

console.log("hello world");
node test.js

test.jl

println("hello world")
julia test.jl

Notes

test.py

print("hello world")
# cpython
python3 test.py
# pypy
pypy3 test.py

test.pl

print "hello world\n"
perl test.pl

Notes

test.d

import std.stdio;
void main()
{
    writeln("hello world");
}
# dmd
dmd -O test.d
# ldc
ldc -O test.d

test.rb

puts "hello world\n"
ruby test.rb