Benchmarking RamSQL

#ramsql #go

Dans un article précédent, je faisais le point sur RamSQL et j’avais conclu sur la cohérence de faire une v1 stable au lieu de purement et simplement abandonner le projet.

Dans la roadmap que j’avais esquissé, le premier point était de gérer correctement les transactions. Cette opération nécessitait de réécrire quasiment entièrement le projet pour baser le reste du code dans une transaction.

Quitte à tout casser, j’en ai profité pour ajouter des benchmarks:

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

avec comme baseline SQLite en mode :memory:

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 nous sort un temps moyen pour chaque opération:

$ 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

C’est bien mais en l’état c’est inutilisable, donc je passe ensuite benchstat pour faire un diff entre la version actuelle et la dernière version committed:

$ 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%

Là c’est très cool, parce que je peux facilement voir la différence de performance avec l’ancienne version (“bench.txt”) et la nouvelle (“vs base”).

Par exemple ci-dessus on constate que la modification amène 10% de gain sur le SELECT et le INSERT. On peut aussi voir que le SELECT de RamSQL est 100x plus lent que celui de SQLite (7479µs vs 11µs)…

Pour aller plus loin et mieux comprendre les endroits bloquant, on peut

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

La version v0.1.3 de RamSQL, après refactoring, est beaucoup plus correcte:

                          │  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%
  • le INSERT, Setup est dans le même ordre de grandeur
  • le SELECT aussi lorsque RamSQL peut utiliser un index HashMap
  • les index B-Tree sont TODO