Skip to content

Commit

Permalink
Fix read example use io.CopyBuffer
Browse files Browse the repository at this point in the history
Due to zero copy optimizations in os.File, using io.CopyBuffer() with
os.File and qcow2reader.Image ends as io.Copy() ignoring our buffer.
This is slow and a bad example for users of the library.

Fix the example by wrapping os.File with a Writer type that implements
only io.Writer. With the wrapper reading the image is 3.63 times faster
(buffer == 2 MiB), and writing to actual file is 1.68 times faster
(buffer == 512 KiB buffer). Reading qcow2 compressed image is only 1.05
times faster (buffer >= 256 KiB).

Based on the benchmarks, change the default buffer size to 512 KiB.

Making the example more complicated is not great but users need to know
how to workaround the "optimizations" in io.CopyBuffer(). I think this
should be fixed in the standard library later.

Benchmark reading Ubuntu 24.04 image in qcow2 uncompressed format:

    % hyperfine -w3 -L b 32768,65536,131072,262144,524288,1048576,2097152 \
                "./go-qcow2reader-example read -buffer-size {b} /var/tmp/images/test.qcow2 >/dev/null"
    Benchmark 1: ./go-qcow2reader-example read -buffer-size 32768 /var/tmp/images/test.qcow2 >/dev/null
      Time (mean ± σ):     693.0 ms ±   5.7 ms    [User: 108.5 ms, System: 585.7 ms]
      Range (min … max):   682.5 ms … 703.7 ms    10 runs

    Benchmark 2: ./go-qcow2reader-example read -buffer-size 65536 /var/tmp/images/test.qcow2 >/dev/null
      Time (mean ± σ):     438.5 ms ±  17.1 ms    [User: 76.3 ms, System: 362.1 ms]
      Range (min … max):   423.6 ms … 477.0 ms    10 runs

    Benchmark 3: ./go-qcow2reader-example read -buffer-size 131072 /var/tmp/images/test.qcow2 >/dev/null
      Time (mean ± σ):     309.2 ms ±   9.3 ms    [User: 62.6 ms, System: 246.6 ms]
      Range (min … max):   301.4 ms … 332.9 ms    10 runs

    Benchmark 4: ./go-qcow2reader-example read -buffer-size 262144 /var/tmp/images/test.qcow2 >/dev/null
      Time (mean ± σ):     254.4 ms ±   4.2 ms    [User: 54.1 ms, System: 200.6 ms]
      Range (min … max):   248.5 ms … 263.0 ms    11 runs

    Benchmark 5: ./go-qcow2reader-example read -buffer-size 524288 /var/tmp/images/test.qcow2 >/dev/null
      Time (mean ± σ):     222.7 ms ±   2.5 ms    [User: 49.5 ms, System: 170.2 ms]
      Range (min … max):   217.9 ms … 228.2 ms    13 runs

    Benchmark 6: ./go-qcow2reader-example read -buffer-size 1048576 /var/tmp/images/test.qcow2 >/dev/null
      Time (mean ± σ):     206.0 ms ±   6.8 ms    [User: 49.5 ms, System: 151.6 ms]
      Range (min … max):   199.2 ms … 226.3 ms    14 runs

    Benchmark 7: ./go-qcow2reader-example read -buffer-size 2097152 /var/tmp/images/test.qcow2 >/dev/null
      Time (mean ± σ):     191.1 ms ±   2.6 ms    [User: 47.7 ms, System: 139.0 ms]
      Range (min … max):   187.0 ms … 197.1 ms    15 runs

    Summary
      ./go-qcow2reader-example read -buffer-size 2097152 /var/tmp/images/test.qcow2 >/dev/null ran
        1.08 ± 0.04 times faster than ./go-qcow2reader-example read -buffer-size 1048576 /var/tmp/images/test.qcow2 >/dev/null
        1.17 ± 0.02 times faster than ./go-qcow2reader-example read -buffer-size 524288 /var/tmp/images/test.qcow2 >/dev/null
        1.33 ± 0.03 times faster than ./go-qcow2reader-example read -buffer-size 262144 /var/tmp/images/test.qcow2 >/dev/null
        1.62 ± 0.05 times faster than ./go-qcow2reader-example read -buffer-size 131072 /var/tmp/images/test.qcow2 >/dev/null
        2.29 ± 0.09 times faster than ./go-qcow2reader-example read -buffer-size 65536 /var/tmp/images/test.qcow2 >/dev/null
        3.63 ± 0.06 times faster than ./go-qcow2reader-example read -buffer-size 32768 /var/tmp/images/test.qcow2 >/dev/null

Benchmark copying same image to actual file:

    % hyperfine -w3 -L b 32768,65536,131072,262144,524288,1048576,2097152 \
                "./go-qcow2reader-example read -buffer-size {b} /var/tmp/images/test.qcow2 >/tmp/tmp.img"
    Benchmark 1: ./go-qcow2reader-example read -buffer-size 32768 /var/tmp/images/test.qcow2 >/tmp/tmp.img
      Time (mean ± σ):      1.973 s ±  0.050 s    [User: 0.126 s, System: 1.663 s]
      Range (min … max):    1.910 s …  2.073 s    10 runs

    Benchmark 2: ./go-qcow2reader-example read -buffer-size 65536 /var/tmp/images/test.qcow2 >/tmp/tmp.img
      Time (mean ± σ):      1.494 s ±  0.104 s    [User: 0.097 s, System: 1.249 s]
      Range (min … max):    1.437 s …  1.778 s    10 runs

    Benchmark 3: ./go-qcow2reader-example read -buffer-size 131072 /var/tmp/images/test.qcow2 >/tmp/tmp.img
      Time (mean ± σ):      1.274 s ±  0.049 s    [User: 0.078 s, System: 1.035 s]
      Range (min … max):    1.219 s …  1.373 s    10 runs

    Benchmark 4: ./go-qcow2reader-example read -buffer-size 262144 /var/tmp/images/test.qcow2 >/tmp/tmp.img
      Time (mean ± σ):      1.208 s ±  0.092 s    [User: 0.058 s, System: 0.918 s]
      Range (min … max):    1.156 s …  1.462 s    10 runs

    Benchmark 5: ./go-qcow2reader-example read -buffer-size 524288 /var/tmp/images/test.qcow2 >/tmp/tmp.img
      Time (mean ± σ):      1.174 s ±  0.033 s    [User: 0.048 s, System: 0.851 s]
      Range (min … max):    1.149 s …  1.259 s    10 runs

    Benchmark 6: ./go-qcow2reader-example read -buffer-size 1048576 /var/tmp/images/test.qcow2 >/tmp/tmp.img
      Time (mean ± σ):      1.202 s ±  0.068 s    [User: 0.043 s, System: 0.821 s]
      Range (min … max):    1.148 s …  1.342 s    10 runs

    Benchmark 7: ./go-qcow2reader-example read -buffer-size 2097152 /var/tmp/images/test.qcow2 >/tmp/tmp.img
      Time (mean ± σ):      1.665 s ±  0.936 s    [User: 0.041 s, System: 0.863 s]
      Range (min … max):    1.173 s …  3.863 s    10 runs

    Summary
      ./go-qcow2reader-example read -buffer-size 524288 /var/tmp/images/test.qcow2 >/tmp/tmp.img ran
        1.02 ± 0.07 times faster than ./go-qcow2reader-example read -buffer-size 1048576 /var/tmp/images/test.qcow2 >/tmp/tmp.img
        1.03 ± 0.08 times faster than ./go-qcow2reader-example read -buffer-size 262144 /var/tmp/images/test.qcow2 >/tmp/tmp.img
        1.08 ± 0.05 times faster than ./go-qcow2reader-example read -buffer-size 131072 /var/tmp/images/test.qcow2 >/tmp/tmp.img
        1.27 ± 0.10 times faster than ./go-qcow2reader-example read -buffer-size 65536 /var/tmp/images/test.qcow2 >/tmp/tmp.img
        1.42 ± 0.80 times faster than ./go-qcow2reader-example read -buffer-size 2097152 /var/tmp/images/test.qcow2 >/tmp/tmp.img
        1.68 ± 0.06 times faster than ./go-qcow2reader-example read -buffer-size 32768 /var/tmp/images/test.qcow2 >/tmp/tmp.img

Benchmark reading same image in qcow2 compressed format:

    % hyperfine -w1 -r3 -L b 32768,65536,131072,262144,524288,1048576,2097152 \
                "./go-qcow2reader-example read -buffer-size {b} /var/tmp/images/test.zlib.qcow2 >/dev/null"
    Benchmark 1: ./go-qcow2reader-example read -buffer-size 32768 /var/tmp/images/test.zlib.qcow2 >/dev/null
      Time (mean ± σ):     11.475 s ±  0.020 s    [User: 10.688 s, System: 0.880 s]
      Range (min … max):   11.463 s … 11.499 s    3 runs

    Benchmark 2: ./go-qcow2reader-example read -buffer-size 65536 /var/tmp/images/test.zlib.qcow2 >/dev/null
      Time (mean ± σ):     11.256 s ±  0.083 s    [User: 10.734 s, System: 0.619 s]
      Range (min … max):   11.204 s … 11.351 s    3 runs

    Benchmark 3: ./go-qcow2reader-example read -buffer-size 131072 /var/tmp/images/test.zlib.qcow2 >/dev/null
      Time (mean ± σ):     11.011 s ±  0.027 s    [User: 10.646 s, System: 0.465 s]
      Range (min … max):   10.983 s … 11.037 s    3 runs

    Benchmark 4: ./go-qcow2reader-example read -buffer-size 262144 /var/tmp/images/test.zlib.qcow2 >/dev/null
      Time (mean ± σ):     10.944 s ±  0.025 s    [User: 10.634 s, System: 0.406 s]
      Range (min … max):   10.916 s … 10.961 s    3 runs

    Benchmark 5: ./go-qcow2reader-example read -buffer-size 524288 /var/tmp/images/test.zlib.qcow2 >/dev/null
      Time (mean ± σ):     10.921 s ±  0.011 s    [User: 10.653 s, System: 0.374 s]
      Range (min … max):   10.911 s … 10.933 s    3 runs

    Benchmark 6: ./go-qcow2reader-example read -buffer-size 1048576 /var/tmp/images/test.zlib.qcow2 >/dev/null
      Time (mean ± σ):     10.921 s ±  0.019 s    [User: 10.668 s, System: 0.371 s]
      Range (min … max):   10.901 s … 10.938 s    3 runs

    Benchmark 7: ./go-qcow2reader-example read -buffer-size 2097152 /var/tmp/images/test.zlib.qcow2 >/dev/null
      Time (mean ± σ):     10.897 s ±  0.025 s    [User: 10.659 s, System: 0.350 s]
      Range (min … max):   10.869 s … 10.913 s    3 runs

    Summary
      ./go-qcow2reader-example read -buffer-size 2097152 /var/tmp/images/test.zlib.qcow2 >/dev/null ran
        1.00 ± 0.00 times faster than ./go-qcow2reader-example read -buffer-size 524288 /var/tmp/images/test.zlib.qcow2 >/dev/null
        1.00 ± 0.00 times faster than ./go-qcow2reader-example read -buffer-size 1048576 /var/tmp/images/test.zlib.qcow2 >/dev/null
        1.00 ± 0.00 times faster than ./go-qcow2reader-example read -buffer-size 262144 /var/tmp/images/test.zlib.qcow2 >/dev/null
        1.01 ± 0.00 times faster than ./go-qcow2reader-example read -buffer-size 131072 /var/tmp/images/test.zlib.qcow2 >/dev/null
        1.03 ± 0.01 times faster than ./go-qcow2reader-example read -buffer-size 65536 /var/tmp/images/test.zlib.qcow2 >/dev/null
        1.05 ± 0.00 times faster than ./go-qcow2reader-example read -buffer-size 32768 /var/tmp/images/test.zlib.qcow2 >/dev/null

Fixes: #41
Signed-off-by: Nir Soffer <[email protected]>
  • Loading branch information
nirs committed Nov 3, 2024
1 parent 8255b8a commit ee2b114
Showing 1 changed file with 14 additions and 2 deletions.
16 changes: 14 additions & 2 deletions cmd/go-qcow2reader-example/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func cmdRead(args []string) error {
flag.PrintDefaults()
}
fs.BoolVar(&debug, "debug", false, "enable printing debug messages")
fs.IntVar(&bufferSize, "buffer-size", 65536, "buffer size")
fs.IntVar(&bufferSize, "buffer-size", 512*1024, "buffer size")
fs.Int64Var(&offset, "offset", 0, "offset to read")
fs.Int64Var(&length, "length", -1, "length to read")
if err := fs.Parse(args); err != nil {
Expand Down Expand Up @@ -67,7 +67,19 @@ func cmdRead(args []string) error {

buf := make([]byte, bufferSize)
sr := io.NewSectionReader(img, offset, length)
_, err = io.CopyBuffer(os.Stdout, sr, buf)
w := &Writer{os.Stdout}

_, err = io.CopyBuffer(w, sr, buf)

return err
}

// Writer forces os.File to write using our buffer by hiding it's ReadFrom
// method. io.CopyBuffer is up to XX times faster with this wrrapper.
type Writer struct {
f *os.File
}

func (w *Writer) Write(p []byte) (int, error) {
return w.f.Write(p)
}

0 comments on commit ee2b114

Please sign in to comment.