Authors: Chiu, Lance & Gonzaga, Rainer
This project converts a grayscale image represented by an 8 bit unsigned integer array (0-255) into a single precision floating point array (0.0 - 1.0). The C program covers memory allocation, I/O, output validation, and benchmarking, while the core math conversion is executed using x86-64 assembly with scalar SIMD floating-point instructions.
- OS: Windows (64-bit)
- Compiler: GCC v5.0+ (mingw-w64)
- Assembler: NASM v2.14+
- Build Tool: GNU Make v3.81+
make: Compiles the C and Assembly source files and builds the executable.make run: Builds and immediately runs the program.make clean: Removes thebuild/directory and all compiled object files.
To measure performance, both the x86-64 assembly routine and a plain C equivalent were each called 30 times per image size, with results averaged using QueryPerformanceCounter. Pixel values were generated randomly for each test.
| Image size | Pixel count | C avg. time (30 runs) | x86-64 asm avg. time (30 runs) | Speedup |
|---|---|---|---|---|
| 10 x 10 | 100 | 0.000000153 s | 0.000000053 s | 2.87x |
| 100 x 100 | 10,000 | 0.000012050 s | 0.000006640 s | 1.81x |
| 1000 x 1000 | 1,000,000 | 0.000802970 s | 0.000674457 s | 1.19x |
For both implementations, execution time scales roughly with pixel count, which makes sense since each pixel goes through the same fixed set of operations with no branching involved. More pixels just means repeating that same cheap work more times.
The assembly version is faster than the C version at every size. One instruction we had to be careful with is cvtsi2ss xmm0, r9d, which only writes the lower 32 bits of xmm0 and leaves the rest of the register untouched. If xmm0 isn't cleared beforehand, the CPU can treat that as a dependency on its value from the previous loop iteration and stall waiting for it, even though the old value isn't actually used. We avoid this by zeroing xmm0 with xorps xmm0, xmm0 right before cvtsi2ss every iteration. XOR-ing a register with itself is a special case CPUs detect and treat as just setting it to zero, with no dependency on whatever value was there before. That keeps the loop running at full speed instead of stalling on a dependency that doesn't need to exist.
At the largest size (1000 x 1000), we think both versions are more limited by memory access than by computation, since each pixel is only 1 byte in and 4 bytes out, and the divide itself is cheap on modern hardware.
10 x 10 benchmark:
100 x 100 benchmark:
1000 x 1000 benchmark:
You may find the demo video locally in public/demo_video.mp4, or you can view it on Google Drive.



