Computers Counting Part 4: Handwritten Assembly
How fast can computers count?
This is the 4th post in a series of writing programs to see how fast computers can count. The series will start with naive methods, then will make use of optimization and later parallelism within the CPU, then will utilize a GPU, and the finale will use a dedicated FPGA for counting.
Previous posts:
Hardware specs:
- CPU: AMD Ryzen 9 3900X (12x Zen 2 cores)
- GPU: Nvidia GeForce RTX 5060
- RAM: 32GB DDR4
- FPGA: PYNQ-Z2
Software specs:
- OS: Fedora 43
- Compiler: gcc 15.2.1
Do we even need this?
If you go back to the last part, you’ll notice that the -O1 compiled option is already very well optimized. Here it is for your convienence:
call gettimeofday
movl $100000000, %eax
.p2align 3
.L2:
subq $1, %rax
jne .L2
movq %rsp, %rdi
movl $0, %esi
call gettimeofdayThere is only 1 instruction between the start of the loop and branch instruction, and that instruction is the counter instruction. The count instruction is technically a decrement instruction, so I didn’t count this, as I want to count upwards. So yes, I do need this, but only on technicality.
The hand-optimized assembly
I am no expert at writing assembly code, so I just took the -O1 version and made some tweaks:
- Count up from 0 to 100,000,000 rather than count down. This does require an extra instruction (cmpl) to compare to 100,000,000.
- Used 32-bit registers, since I am counting within the signed 32-bit range. This range ends at a bit over 2 billion. You can see this with the
lat the end of the assembly instructions, as the l stands for “long”, or 32-bit. You can also see this with theeat the beginning of the registers, as the e stands for “extended”, or 32-bit. - Put the counter and comparison number into registers eax and ebx, respectively, for quick and easy access.
Here it is in all its glory:
call gettimeofday
movl $0, %eax
movl $100000000, %ebx
.p2align 3
.L2:
addl $1, %eax
cmpl %ebx, %eax
jb .L2
movq %rsp, %rdi
movl $0, %esi
call gettimeofdayTo compile and run this assembly, I just need to invoke gcc as if the assembly is C code: gcc hand-optimized.s -o hand-optimized. Running the executable gives me this output:
Counted to 100 million in 26496 microseconds.
Success! This is faster than anything that has been ran before on this blog. It’s also remarkably consistent, as I ran the program 10 times and the slowest run took 26884 microseconds. Note that I’ve been using the same OS and compiler version throughout my blogs, so I’m not sure why it’s so much more consistent all of the sudden.
The final form?
So far, I have gone from writing naive count programs to hand-writing assembly. Surely this is the final step, we can’t go faster than assembly. Yes, if we don’t utilize a nifty thing called parallelism. This tool will allow drastically faster counting if used well. Stay tuned for the next part!
Summary table of counting speed
| Method | Increments / second |
|---|---|
| Very naive JavaScript | 12.6M |
| Naive JavaScript | 369M |
| Naive C | 1.55B |
| Optimized C (-O0) | 2.02B |
| Optimized C (-O3) | 3.00B |
| Hand-optimized assembly | 3.77B |