// Copyright 2015, Joe Tsai. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE.md file. // Benchmark tool to compare performance between multiple compression // implementations. Individual implementations are referred to as codecs. // // Example usage: // $ go build // $ ./bench \ // -formats fl \ // -tests encRate,decRate \ // -codecs std,ds,cgo \ // -globs twain.txt \ // -levels 1,6,9 \ // -sizes 1e4,1e5,1e6 // // // BENCHMARK: fl:encRate // benchmark std MB/s delta cgo MB/s delta // twain.txt:1:1e4 9.88 1.00x 48.89 4.95x // twain.txt:1:1e5 26.70 1.00x 64.99 2.43x // twain.txt:1:1e6 31.95 1.00x 65.56 2.05x // twain.txt:6:1e4 7.31 1.00x 30.67 4.19x // twain.txt:6:1e5 8.33 1.00x 17.22 2.07x // twain.txt:6:1e6 8.05 1.00x 15.99 1.99x // twain.txt:9:1e4 8.15 1.00x 30.04 3.69x // twain.txt:9:1e5 6.59 1.00x 12.82 1.95x // twain.txt:9:1e6 6.32 1.00x 11.40 1.80x // // BENCHMARK: fl:decRate // benchmark std MB/s delta ds MB/s delta cgo MB/s delta // twain.txt:1:1e4 49.61 1.00x 74.15 1.49x 163.81 3.30x // twain.txt:1:1e5 60.25 1.00x 91.25 1.51x 177.38 2.94x // twain.txt:1:1e6 61.75 1.00x 95.82 1.55x 181.11 2.93x // twain.txt:6:1e4 52.16 1.00x 77.25 1.48x 174.30 3.34x // twain.txt:6:1e5 72.23 1.00x 108.01 1.50x 195.31 2.70x // twain.txt:6:1e6 76.59 1.00x 116.80 1.53x 203.88 2.66x // twain.txt:9:1e4 52.97 1.00x 77.58 1.46x 172.88 3.26x // twain.txt:9:1e5 72.35 1.00x 108.37 1.50x 197.15 2.72x // twain.txt:9:1e6 76.82 1.00x 118.02 1.54x 204.87 2.67x // // // RUNTIME: 2m42.434570856s package main import ( "flag" "fmt" "math" "os" "path/filepath" "strings" "time" ) func main() { flag.CommandLine.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) flag.CommandLine.VisitAll(func(f *flag.Flag) { if strings.HasPrefix(f.Name, "test.") { return } typ, usage := flag.UnquoteUsage(f) s := fmt.Sprintf(" -%s %s\n %s (default %q)\n", f.Name, typ, usage, f.DefValue) fmt.Fprint(os.Stderr, s) }) } setDefaults() flag.Var(&formats, "formats", "List of formats to benchmark") flag.Var(&tests, "tests", "List of different benchmark tests") flag.Var(&codecs, "codecs", "List of codecs to benchmark") flag.Var(&paths, "paths", "List of paths to search for test files") flag.Var(&globs, "globs", "List of globs to match for test files") flag.Var(&levels, "levels", "List of compression levels to benchmark") flag.Var(&sizes, "sizes", "List of input sizes to benchmark") flag.Parse() files := getFiles(paths, globs) ts := time.Now() runBenchmarks(files, codecs, formats, tests, levels, sizes) te := time.Now() fmt.Printf("RUNTIME: %v\n", te.Sub(ts)) } type file struct{ Abs, Rel string } // getFiles returns a list of files found by applying the glob matching on // all the specified paths. This function ignores any errors. func getFiles(paths []string, globs []string) []file { var fs []file for _, p := range paths { for _, g := range globs { ms, _ := filepath.Glob(filepath.Join(p, g)) for _, m := range ms { r, err1 := filepath.Rel(p, m) fi, err2 := os.Stat(m) if err1 == nil && err2 == nil && !fi.IsDir() { fs = append(fs, file{Abs: m, Rel: r}) } } } } return fs } func runBenchmarks(files []file, codecs []string, formats []Format, tests []Test, levels, sizes []int) { for _, f := range formats { // Get lists of encoders and decoders that exist. var encs, decs []string for _, c := range codecs { if _, ok := encoders[f][c]; ok { encs = append(encs, c) } } for _, c := range codecs { if _, ok := decoders[f][c]; ok { decs = append(decs, c) } } for _, t := range tests { var results [][]Result var names, codecs []string var title, suffix string // Check that we can actually do this fmt.Printf("BENCHMARK: %s:%s\n", enumToFmt[f], enumToTest[t]) if len(encs) == 0 { fmt.Println("\tSKIP: There are no encoders available.") fmt.Println("") continue } if len(decs) == 0 && t == TestDecodeRate { fmt.Println("\tSKIP: There are no decoders available.") fmt.Println("") continue } // Progress ticker. var cnt int tick := func() { total := len(codecs) * len(files) * len(levels) * len(sizes) pct := 100.0 * float64(cnt) / float64(total) fmt.Printf("\t[%6.2f%%] %d of %d\r", pct, cnt, total) cnt++ } // Perform the This may take some time. switch t { case TestEncodeRate: codecs, title, suffix = encs, "MB/s", "" results, names = BenchmarkEncoderSuite(f, encs, files, levels, sizes, tick) case TestDecodeRate: ref := getReferenceEncoder(f) codecs, title, suffix = decs, "MB/s", "" results, names = BenchmarkDecoderSuite(f, decs, files, levels, sizes, ref, tick) case TestCompressRatio: codecs, title, suffix = encs, "ratio", "x" results, names = BenchmarkRatioSuite(f, encs, files, levels, sizes, tick) default: panic("unknown test") } // Print all of the results. printResults(results, names, codecs, title, suffix) fmt.Println() } fmt.Println() } } // The decompression speed benchmark works by decompressing some pre-compressed // data. In order for the benchmarks to be consistent, the same encoder should // be used to generate the pre-compressed data for all the trials. // // encRefs defines the priority order for which encoders to choose first as the // reference compressor. If no compressor is found for any of the listed codecs, // then a random encoder will be chosen. var encRefs = []string{"std", "cgo", "ds"} func getReferenceEncoder(f Format) Encoder { for _, c := range encRefs { if enc, ok := encoders[f][c]; ok { return enc // Choose by priority } } for _, enc := range encoders[f] { return enc // Choose any random encoder } return nil // There are no encoders } func printResults(results [][]Result, names, codecs []string, title, suffix string) { // Allocate result table. cells := make([][]string, 1+len(names)) for i := range cells { cells[i] = make([]string, 1+2*len(codecs)) } // Label the first row. cells[0][0] = "benchmark" for i, c := range codecs { cells[0][1+2*i] = c + " " + title cells[0][2+2*i] = "delta" } // Insert all rows. for j, row := range results { cells[1+j][0] = names[j] for i, r := range row { if r.R != 0 && !math.IsNaN(r.R) && !math.IsInf(r.R, 0) { cells[1+j][1+2*i] = fmt.Sprintf("%.2f", r.R) + suffix } if r.D != 0 && !math.IsNaN(r.D) && !math.IsInf(r.D, 0) { cells[1+j][2+2*i] = fmt.Sprintf("%.2f", r.D) + "x" } } } // Compute the maximum lengths. maxLens := make([]int, 1+2*len(codecs)) for _, row := range cells { for i, s := range row { if maxLens[i] < len(s) { maxLens[i] = len(s) } } } // Print padded versions of all cells. for _, row := range cells { fmt.Print("\t") for i, s := range row { switch { case i == 0: // Column 0 row[i] = s + strings.Repeat(" ", maxLens[i]-len(s)) case i%2 == 1: // Column 1, 3, 5, 7, ... row[i] = strings.Repeat(" ", 6+maxLens[i]-len(s)) + s case i%2 == 0: // Column 2, 4, 6, 8, ... row[i] = strings.Repeat(" ", 2+maxLens[i]-len(s)) + s } fmt.Print(row[i]) } fmt.Println() } }