File write speed concern comparison

Hi,

Working on an ARM32 environment with ‘slow’ storage media. In trying SQLite on Arm32 I recognized that inserts were so slow that I could notice it simply by looking at it.

I wrote two programs, one in Go, one in C. I compiled both for ARM32,

The C compilation was compiled from the command line with zero additional flags (no optimization).

The Go compilation was tried with and without ARM7 specific compilation, with optimization (go build -ldflags=“-s -w” -o binspeedtest-go .") and without.

The results concern me greatly

C total write time: between 2 and 5 milliseconds

Go total write time: between 116 and 147 milliseconds

Am I doing something wrong, am I missing something? I expect GoLang to use the same system calls as C, with minimal overhead, which is not what I see here.

Please note that I do know the effect of buffering / and that that will improve my results, but that is not what this topic is about. Sadly, even with buffering it does not get close to what I see in C.

file, err := os.Create("golang-optimized.bin")
utils.Debug.Panic(err)
defer file.Close()

buf := make([]byte, 15)

start := time.Now()
for i := 0; i < 1000; i++ {
    _, _ = file.Write(buf)
}
end := time.Now()
utils.Print.Ln("Write blank buffer of 15 bytes", end.Sub(start).Milliseconds())

C

typedef struct {
uint8_t age;
char name[50];
uint64_t key;
} SaveToDisk;

#define cycles 1000

long long getMilliTime(void) {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return ((long long)tv.tv_sec * 1000) + (tv.tv_usec / 1000);
}

int main() {
    FILE *fl = fopen(“benchmark-c.bin”, “wb”);
    if (fl == NULL) {
        perror(“error opening benchmark-c.bin”);
        exit(EXIT_FAILURE);
    }

    SaveToDisk s = {53, “1”, 0x8888888888888888};

    long long start = getMilliTime();

    for (int n = 0; n < cycles; n++) {
        fwrite(&s, sizeof(SaveToDisk), 1, fl);
        //    sleep(1);
    }

    long long stop = getMilliTime();
    long long duration = stop - start;
    float_t avg = (float_t)duration / cycles;

    printf(“Start: %lld\nStop: %lld\n”, start, stop);
    printf("The program took %lld millis to complete %d cycles at  average of %f "
“per write\n”,
        duration, cycles, avg);

    fclose(fl);
    return 0;
}

More information:

  1. Was done with Golang 1.24, updated to 1.26 - no notable difference.
  2. Updated the C struct as packed 1, no difference to C (just slightly smaller output file)
  3. Added GoLang code to do syscall, no notable difference.

Thinking about doing the same test in Python, Zig or Rust… Still hoping I am doing something wrong as this definitely is concern (e.g. using SQLite it is up to 50ms for an insert… I have to do a total activity in less than 80ms, meaning SQLite (the inherent writing) is posing serious risk, and sadly as shown, going binary serialization is showing that I may not be that much better off as the SQLite problem inherits itself from Golang.

Python, the slowest of the lot, takes 11 ms… which is double that of C, 8 times quicker than Go. Am I being stupid, what am I missing?

By doing all C calls in GoLang, I have got the write times from 80ms-190ms to 11ms/ same as Python. This however is only with one allocation and 1 free…

With an alloc/free on every iteration takes it from 11ms to 17ms.

Is there any other quickfix/less intrusive thing I can do to improve the Go os.File?

Using unsafe pointers to byte arrays I get it sub 10ms for a 1000 writes of 15 bytes each.
Increasing the array size to 65 doubles the time taken (which I did not expect) from my understanding of (as all writes fits into 9 pages).

I will take this off air now… but still perplexed and worried to the lack of speed in Golang File lib.

My understanding is that C buffers writes by default. So, you are mostly measuring syscall performance here. Try something like this:

package main

import (
	"bufio"
	"log"
	"os"
	"time"
)

func main() {
	const (
		iterations = 1000
		recordSize = 15
	)

	// preallocate the record once
	record := make([]byte, recordSize)
	copy(record, "example payload")

	f, err := os.Create("out.dat")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	// 32KB buffer reduces syscalls dramatically
	w := bufio.NewWriterSize(f, 32*1024)
	start := time.Now()
	for i := 0; i < iterations; i++ {
		if _, err := w.Write(record); err != nil {
			log.Fatal(err)
		}
	}

	// Flush buffer to disk
	if err := w.Flush(); err != nil {
		log.Fatal(err)
	}
	end := time.Now()
	log.Printf("Time %v microseconds", end.Sub(start).Microseconds())

}