llgcode / draw2d

2D rendering for different output (raster, pdf, svg)
BSD 2-Clause "Simplified" License
1.09k stars 105 forks source link

Performance is ~10-30x worse than Cairo #147

Open usedbytes opened 6 years ago

usedbytes commented 6 years ago

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.

redstarcoder commented 6 years ago

Any idea on what the bottleneck might be? Comparable performance would be a long-term goal.

llgcode commented 6 years ago

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

usedbytes commented 6 years ago

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.