draw2d
draw2d copied to clipboard
Performance is ~10-30x worse than Cairo
Perhaps this doesn't qualify as an issue per-se, but I wanted to use draw2d as the renderer in my program (instead of Cairo). However, even in simple cases, draw2dimg performs at least 10-30x worse than the equivalent Cairo code via cgo.
Maybe I'm doing something wrong? (I hope so)
Simple benchmark test below draws filled squares with an outline, performing ~30x worse on draw2d than Cairo. When the drawn surface is 1:1 blitted to another surface, the gap closes to ~10x
package main
import (
"flag"
"image"
"image/color"
"image/png"
"log"
"os"
"testing"
"github.com/ungerik/go-cairo"
"github.com/llgcode/draw2d/draw2dimg"
)
var dumpImage bool
func TestMain(m *testing.M) {
flag.BoolVar(&dumpImage, "dump", false, "Dump a PNG image from each benchmark for comparison")
flag.Parse()
os.Exit(m.Run())
}
func BenchmarkDraw2D(b *testing.B) {
img := image.NewRGBA(image.Rect(0, 0, 500, 500))
ctx := draw2dimg.NewGraphicContext(img)
for n := 0; n < b.N; n++ {
ctx.SetStrokeColor(color.RGBA{0xff, 0x00, 0x00, 0xff})
ctx.SetFillColor(color.RGBA{0x4d, 0x4d, 0x4d, 0xff})
ctx.SetLineWidth(2)
ctx.MoveTo(1, 1)
ctx.LineTo(499, 1)
ctx.LineTo(499, 499)
ctx.LineTo(1, 499)
ctx.Close()
ctx.FillStroke()
}
if dumpImage {
f, err := os.Create("draw2d.png")
if err != nil {
log.Fatal(err)
}
if err := png.Encode(f, img); err != nil {
f.Close()
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
}
}
func BenchmarkCairo(b *testing.B) {
img := cairo.NewSurface(cairo.FORMAT_ARGB32, 500, 500)
for n := 0; n < b.N; n++ {
img.Rectangle(0, 0, 500, 500)
img.SetSourceRGB(0.3, 0.3, 0.3)
img.SetLineWidth(4)
img.FillPreserve()
img.SetSourceRGB(1.0, 0.0, 0.0)
img.Stroke()
}
if dumpImage {
img.WriteToPNG("cairo.png")
}
}
Results:
$ go test -v -bench=. -test.parallel 1
goos: linux
goarch: amd64
pkg: github.com/usedbytes/drawbench
BenchmarkDraw2D-4 1000 2049011 ns/op
BenchmarkCairo-4 30000 50037 ns/op
PASS
ok github.com/usedbytes/drawbench 4.278s
The README says that draw2d is a pure-go alternative to Cairo, but unfortunately the performance difference is too high for my use-case.
Any idea on what the bottleneck might be? Comparable performance would be a long-term goal.
Thanks for the benchmark comparison. I need to make more test but, I feel it comes from the fill operation. I didn't work a lot on performance. The package I use for this is https://github.com/golang/freetype/raster
Yes, it does seem to be the raster code (*RGBAPainter).Paint
| Flat | Flat% | Sum% | Cum | Cum% | Name | Inlined? |
|---|---|---|---|---|---|---|
| 2.20s | 95.24% | 95.24% | 2.20s | 95.24% | github.com/golang/freetype/raster.(*RGBAPainter).Paint | |
| 0.04s | 1.73% | 96.97% | 0.04s | 1.73% | github.com/golang/freetype/raster.(*Rasterizer).findCell | |
| 0.04s | 1.73% | 98.70% | 2.24s | 96.97% | github.com/golang/freetype/raster.(*Rasterizer).Rasterize | |
| 0.01s | 0.43% | 99.13% | 0.05s | 2.16% | github.com/golang/freetype/raster.(*Rasterizer).saveCell | |
| 0.01s | 0.43% | 99.57% | 0.06s | 2.60% | github.com/golang/freetype/raster.(*Rasterizer).setCell | |
| 0 | 0.00% | 99.57% | 2.30s | 99.57% | github.com/llgcode/draw2d/draw2dimg.(*GraphicContext).FillStroke | |
| 0 | 0.00% | 99.57% | 0.06s | 2.60% | github.com/llgcode/draw2d/draw2dimg.(*FtLineBuilder).LineTo | |
| 0 | 0.00% | 99.57% | 0.06s | 2.60% | github.com/llgcode/draw2d/draw2dbase.Transformer.LineTo | |
| 0 | 0.00% | 99.57% | 0.06s | 2.60% | github.com/llgcode/draw2d/draw2dbase.Flatten | |
| 0 | 0.00% | 99.57% | 0.03s | 1.30% | github.com/llgcode/draw2d/draw2dbase.DemuxFlattener.LineTo | |
| 0 | 0.00% | 99.57% | 2.24s | 96.97% | github.com/llgcode/draw2d/draw2dimg.(*GraphicContext).paint | |
| 0 | 0.00% | 99.57% | 0.06s | 2.60% | github.com/llgcode/draw2d/draw2dbase.(*Transformer).LineTo | |
| 0 | 0.00% | 99.57% | 0.03s | 1.30% | github.com/llgcode/draw2d/draw2dbase.(*LineStroker).End | |
| 0 | 0.00% | 99.57% | 0.03s | 1.30% | github.com/llgcode/draw2d/draw2dbase.(*DemuxFlattener).LineTo | |
| 0 | 0.00% | 99.57% | 0.03s | 1.30% | github.com/llgcode/draw2d/draw2dbase.(*DemuxFlattener).End | |
| 0 | 0.00% | 99.57% | 0.06s | 2.60% | github.com/golang/freetype/raster.(*Rasterizer).Add1 | |
| 0 | 0.00% | 99.57% | 0.06s | 2.60% | github.com/llgcode/draw2d/draw2dimg.FtLineBuilder.LineTo | |
| 0 | 0.00% | 99.57% | 2.30s | 99.57% | github.com/usedbytes/drawbench.BenchmarkDraw2D | |
| 0 | 0.00% | 99.57% | 2.30s | 99.57% | testing.(*B).launch | |
| 0 | 0.00% | 99.57% | 2.30s | 99.57% | testing.(*B).runN | |
| 0 | 0.00% | 99.57% | 0.03s | 1.30% | github.com/llgcode/draw2d/draw2dbase.DemuxFlattener.End |
Collected using the testing package's built-in profiling:
$ go test -cpuprofile cpu.prof -bench .
goos: linux
goarch: amd64
pkg: github.com/usedbytes/drawbench
BenchmarkDraw2D-4 1000 2092810 ns/op
BenchmarkCairo-4 30000 44773 ns/op
PASS
ok github.com/usedbytes/drawbench 4.314s
$ go tool pprof -http=:8000 cpu.prof
The Cairo counterparts (cairo_stroke and cairo_fill) take a similar percentage of the time, but are faster in absolute terms. I imagine they're just much more highly optimised.