Benchmarking RamSQL

#ramsql #go

In a previous article, I was taking stock on RamSQL and concluded it was coherent to go for a stable v1 instead of archiving and dropping the entire project.

In the tentative roadmap I wrote, the first point of order was to correctly handling transactions. This would means rewriting the entire project to have everything moving within a transaction instead of shoeing it in.

Since I was breaking everything, I took the opportunity to introduce benchmarks:

bench:
	go test -bench=. -count 6 | tee newbench.txt
	benchstat bench.txt newbench.txt | tee benchstat.txt

with SQLite in:memory: as a baseline

func BenchmarkRamSQLSelectHashMap10K(b *testing.B) {
	db, err := sql.Open("ramsql", "BenchmarkSQLSelectHashMap10K")
	if err != nil {
		b.Fatalf("cannot open ramsql db")
	}

	n := 10000
	setupInsertN(b, db, n)
	benchmarkSelectHashMap(b, db)
}

func BenchmarkSQLiteSelectHashMap10K(b *testing.B) {
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		b.Fatalf("cannot open sqlite")
	}

	n := 10000
	setupInsertN(b, db, n)
	benchmarkSelectHashMap(b, db)
}

func benchmarkSelectHashMap(b *testing.B, db *sql.DB) {

	var id int64
	var email string

	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		query := `SELECT account.id, account.email FROM account WHERE id = $1`
		rows, err := db.Query(query, n)
		if err != nil {
			b.Fatalf("cannot query rows: %s", err)
		}

		for rows.Next() {
			err = rows.Scan(&id, &email)
			if err != nil {
				b.Fatalf("cannot scan rows: %s", err)
			}
		}
	}

	_ = id
	_ = email

	_, err := db.Exec(`DROP TABLE account`)
	if err != nil {
		b.Fatalf("sql.Exec: %s", err)
	}
}

go test output a mean time for each operation:

$ go test -bench=. -count 6 | tee newbench.txt
goos: linux
goarch: amd64
pkg: github.com/proullon/ramsql
cpu: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
BenchmarkRamSQLSelect-8      	    3766	    297732 ns/op
BenchmarkRamSQLSelect-8      	    4114	    286406 ns/op
BenchmarkRamSQLSelect-8      	    3974	    287603 ns/op
BenchmarkRamSQLSelect-8      	    3874	    286818 ns/op
BenchmarkRamSQLSelect-8      	    4036	    286082 ns/op
BenchmarkRamSQLSelect-8      	    3924	    285952 ns/op
BenchmarkSQLiteSelect-8      	  110434	     10706 ns/op
BenchmarkSQLiteSelect-8      	  112434	     10805 ns/op
BenchmarkSQLiteSelect-8      	   99657	     11055 ns/op
BenchmarkSQLiteSelect-8      	  111702	     10689 ns/op
BenchmarkSQLiteSelect-8      	  111669	     10756 ns/op
BenchmarkSQLiteSelect-8      	  111285	     10638 ns/op
BenchmarkRamSQLSelect10K-8   	     160	   7384466 ns/op
BenchmarkRamSQLSelect10K-8   	     162	   7938318 ns/op
BenchmarkRamSQLSelect10K-8   	     160	   7456492 ns/op
BenchmarkRamSQLSelect10K-8   	     159	   7502256 ns/op
BenchmarkRamSQLSelect10K-8   	     164	   7540972 ns/op
BenchmarkRamSQLSelect10K-8   	     144	   7451605 ns/op
BenchmarkSQLiteSelect10K-8   	  109460	     10979 ns/op
BenchmarkSQLiteSelect10K-8   	  105711	     11033 ns/op
BenchmarkSQLiteSelect10K-8   	  104372	     11035 ns/op
BenchmarkSQLiteSelect10K-8   	  107712	     11063 ns/op
BenchmarkSQLiteSelect10K-8   	  107395	     11060 ns/op
BenchmarkSQLiteSelect10K-8   	  108666	     11116 ns/op
BenchmarkRamSQLInsert10-8    	    4424	    268468 ns/op
BenchmarkRamSQLInsert10-8    	    4477	    298751 ns/op
BenchmarkRamSQLInsert10-8    	    4242	    280029 ns/op
BenchmarkRamSQLInsert10-8    	    3828	    283994 ns/op
BenchmarkRamSQLInsert10-8    	    4328	    283040 ns/op
BenchmarkRamSQLInsert10-8    	    4436	    276176 ns/op
BenchmarkSQLiteInsert10-8    	    3459	    332308 ns/op
BenchmarkSQLiteInsert10-8    	    3721	    328695 ns/op
BenchmarkSQLiteInsert10-8    	    3709	    330266 ns/op
BenchmarkSQLiteInsert10-8    	    3670	    343643 ns/op
BenchmarkSQLiteInsert10-8    	    3680	    320361 ns/op
BenchmarkSQLiteInsert10-8    	    3819	    325789 ns/op
BenchmarkRamSQLSetup-8       	 1430150	       832.9 ns/op
BenchmarkRamSQLSetup-8       	 1436343	       835.1 ns/op
BenchmarkRamSQLSetup-8       	 1439770	       833.9 ns/op
BenchmarkRamSQLSetup-8       	 1331280	       806.1 ns/op
BenchmarkRamSQLSetup-8       	 1485778	       807.0 ns/op
BenchmarkRamSQLSetup-8       	 1486251	       806.3 ns/op
BenchmarkSQLiteSetup-8       	 1477396	       810.5 ns/op
BenchmarkSQLiteSetup-8       	 1464448	       809.5 ns/op
BenchmarkSQLiteSetup-8       	 1380595	       814.2 ns/op
BenchmarkSQLiteSetup-8       	 1482067	       811.0 ns/op
BenchmarkSQLiteSetup-8       	 1481173	       813.0 ns/op
BenchmarkSQLiteSetup-8       	 1480530	       811.3 ns/op
PASS
ok  	github.com/proullon/ramsql	87.537s

It’s nice and all but useless alone. I use benchstat to get a diff between current version and the last committed one:

$ benchstat bench.txt newbench.txt | tee benchstat.txt
goos: linux
goarch: amd64
pkg: github.com/proullon/ramsql
cpu: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
                  │  bench.txt   │           newbench.txt            │
                  │    sec/op    │   sec/op     vs base              │
RamSQLSelect-8      287.8µ ±  4%   286.6µ ± 4%       ~ (p=0.818 n=6)
SQLiteSelect-8      10.98µ ± 11%   10.73µ ± 3%       ~ (p=0.065 n=6)
RamSQLSelect10K-8   8.246m ±  5%   7.479m ± 6%  -9.30% (p=0.004 n=6)
SQLiteSelect10K-8   11.54µ ± 11%   11.05µ ± 1%  -4.28% (p=0.002 n=6)
RamSQLInsert10-8    312.3µ ±  4%   281.5µ ± 6%  -9.86% (p=0.002 n=6)
SQLiteInsert10-8    341.5µ ±  1%   329.5µ ± 4%  -3.53% (p=0.041 n=6)
RamSQLSetup-8       855.3n ±  4%   820.0n ± 2%       ~ (p=0.065 n=6)
SQLiteSetup-8       844.1n ±  2%   811.1n ± 0%  -3.90% (p=0.002 n=6)
geomean             46.84µ         44.61µ       -4.75%

This is so cool, it’s easy to spot performance variation between last commit (“bench.txt”) and current one (“vs base”).

In the example above, we can see the change gains 10% on SELECT and INSERT. We can also see that RamSQL SELECT is 100x slower than SQLite (7479µs vs 11µs)…

To go further into performance investigation, one can

$ go test -trace=driver.out ./driver
$ go tool trace --pprof=syscall driver.out
$ go tool pprof -traces syscall.pprof

RamSQL version v0.1.3, after refactoring, is much more correct:

                          │  bench.txt  │            newbench.txt             │
                          │   sec/op    │    sec/op     vs base               │
RamSQLSelectBTree-8         49.68µ ± 4%   54.52µ ±  4%   +9.73% (p=0.002 n=6)
SQLiteSelectBTree-8         10.92µ ± 5%   11.43µ ±  3%   +4.63% (p=0.026 n=6)
RamSQLSelectBTree10K-8      543.6µ ± 2%   682.3µ ± 17%  +25.51% (p=0.002 n=6)
SQLiteSelectBTree10K-8      11.20µ ± 2%   12.50µ ± 12%  +11.63% (p=0.002 n=6)
RamSQLSelectHashMap10K-8    24.92µ ± 2%   30.73µ ±  7%  +23.32% (p=0.002 n=6)
SQLiteSelectHashMap10K-8    8.450µ ± 2%   8.651µ ±  4%        ~ (p=0.132 n=6)
RamSQLSelectBTree100K-8     14.00m ± 2%   16.09m ± 11%  +14.93% (p=0.002 n=6)
SQLiteSelectBTree100K-8     11.33µ ± 2%   12.70µ ±  7%  +12.04% (p=0.002 n=6)
RamSQLSelectHashMap100K-8   26.47µ ± 2%   29.76µ ±  4%  +12.42% (p=0.002 n=6)
SQLiteSelectHashMap100K-8   8.581µ ± 4%   9.469µ ±  8%  +10.35% (p=0.002 n=6)
RamSQLInsert10-8            177.9µ ± 2%   211.9µ ±  5%  +19.13% (p=0.002 n=6)
SQLiteInsert10-8            330.6µ ± 4%   106.7µ ±  4%  -67.73% (p=0.002 n=6)
RamSQLSetup-8               834.9n ± 1%   841.6n ±  2%   +0.81% (p=0.004 n=6)
SQLiteSetup-8               826.9n ± 0%   855.4n ±  2%   +3.45% (p=0.002 n=6)
geomean                     31.66µ        32.26µ         +1.89%
  • INSERT, Setup have the same order of magnitude between RamSQL and SQLite
  • SELECT works well when RamSQL can use a HashMap index
  • B-Tree indexes are clearly TODO