From 24d6f3fb659a45d636b076e95ab93392d9a9f516 Mon Sep 17 00:00:00 2001 From: Egon Elbre Date: Tue, 13 Oct 2020 12:19:15 +0300 Subject: [PATCH] f32: optimize Affine2D MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit encode/decode seem to introduce significant overhead. Inline them manually. It'll make code harder to read, however the performance wins are significant. name \ time/op before after delta TransformOffset-32 2.64ns ± 0% 0.25ns ± 0% ~ (p=0.100 n=3+3) TransformScale-32 2.64ns ± 0% 0.25ns ± 1% ~ (p=0.100 n=3+3) TransformRotate-32 2.65ns ± 0% 0.24ns ± 3% ~ (p=0.100 n=3+3) TransformTranslateMultiply-32 42.5ns ± 0% 12.9ns ± 0% ~ (p=0.100 n=3+3) TransformScaleMultiply-32 42.6ns ± 0% 12.9ns ± 0% ~ (p=0.100 n=3+3) TransformMultiply-32 42.2ns ± 0% 12.9ns ± 2% ~ (p=0.100 n=3+3) Signed-off-by: Egon Elbre --- f32/affine.go | 71 +++++++++++------------------ f32/affine_test.go | 111 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 44 deletions(-) diff --git a/f32/affine.go b/f32/affine.go index 44e914bb..99bcb474 100644 --- a/f32/affine.go +++ b/f32/affine.go @@ -24,9 +24,9 @@ type Affine2D struct { // in row major order. The rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D { return Affine2D{ - a: sx, b: hx, c: ox, - d: hy, e: sy, f: oy, - }.encode() + a: sx - 1, b: hx, c: ox, + d: hy, e: sy - 1, f: oy, + } } // Offset the transformation. @@ -69,14 +69,13 @@ func (a Affine2D) Shear(origin Point, radiansX, radiansY float32) Affine2D { // Mul returns A*B. func (A Affine2D) Mul(B Affine2D) (r Affine2D) { - A, B = A.decode(), B.decode() - r.a = A.a*B.a + A.b*B.d - r.b = A.a*B.b + A.b*B.e - r.c = A.a*B.c + A.b*B.f + A.c - r.d = A.d*B.a + A.e*B.d - r.e = A.d*B.b + A.e*B.e - r.f = A.d*B.c + A.e*B.f + A.f - return r.encode() + r.a = (A.a+1)*(B.a+1) + A.b*B.d - 1 + r.b = (A.a+1)*B.b + A.b*(B.e+1) + r.c = (A.a+1)*B.c + A.b*B.f + A.c + r.d = A.d*(B.a+1) + (A.e+1)*B.d + r.e = A.d*B.b + (A.e+1)*(B.e+1) - 1 + r.f = A.d*B.c + (A.e+1)*B.f + A.f + return r } // Invert the transformation. Note that if the matrix is close to singular @@ -85,70 +84,54 @@ func (a Affine2D) Invert() Affine2D { if a.a == 0 && a.b == 0 && a.d == 0 && a.e == 0 { return Affine2D{a: 0, b: 0, c: -a.c, d: 0, e: 0, f: -a.f} } - a = a.decode() + a.a += 1 + a.e += 1 det := a.a*a.e - a.b*a.d a.a, a.e = a.e/det, a.a/det a.b, a.d = -a.b/det, -a.d/det temp := a.c a.c = -a.a*a.c - a.b*a.f a.f = -a.d*temp - a.e*a.f - return a.encode() + a.a -= 1 + a.e -= 1 + return a } // Transform p by returning a*p. func (a Affine2D) Transform(p Point) Point { - a = a.decode() return Point{ - X: p.X*a.a + p.Y*a.b + a.c, - Y: p.X*a.d + p.Y*a.e + a.f, + X: p.X*(a.a+1) + p.Y*a.b + a.c, + Y: p.X*a.d + p.Y*(a.e+1) + a.f, } } // Elems returns the matrix elements of the transform in row-major order. The // rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) { - a = a.decode() - return a.a, a.b, a.c, a.d, a.e, a.f -} - -func (a Affine2D) encode() Affine2D { - // since we store with identity matrix subtracted - a.a -= 1 - a.e -= 1 - return a -} - -func (a Affine2D) decode() Affine2D { - // since we store with identity matrix subtracted - a.a += 1 - a.e += 1 - return a + return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f } func (a Affine2D) scale(factor Point) Affine2D { - a = a.decode() return Affine2D{ - a.a * factor.X, a.b * factor.X, a.c * factor.X, - a.d * factor.Y, a.e * factor.Y, a.f * factor.Y, - }.encode() + (a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X, + a.d * factor.Y, (a.e+1)*factor.Y - 1, a.f * factor.Y, + } } func (a Affine2D) rotate(radians float32) Affine2D { sin, cos := math.Sincos(float64(radians)) s, c := float32(sin), float32(cos) - a = a.decode() return Affine2D{ - a.a*c - a.d*s, a.b*c - a.e*s, a.c*c - a.f*s, - a.a*s + a.d*c, a.b*s + a.e*c, a.c*s + a.f*c, - }.encode() + (a.a+1)*c - a.d*s - 1, a.b*c - (a.e+1)*s, a.c*c - a.f*s, + (a.a+1)*s + a.d*c, a.b*s + (a.e+1)*c - 1, a.c*s + a.f*c, + } } func (a Affine2D) shear(radiansX, radiansY float32) Affine2D { tx := float32(math.Tan(float64(radiansX))) ty := float32(math.Tan(float64(radiansY))) - a = a.decode() return Affine2D{ - a.a + a.d*tx, a.b + a.e*tx, a.c + a.f*tx, - a.a*ty + a.d, a.b*ty + a.e, a.f*ty + a.f, - }.encode() + (a.a + 1) + a.d*tx - 1, a.b + (a.e+1)*tx, a.c + a.f*tx, + (a.a+1)*ty + a.d, a.b*ty + (a.e + 1) - 1, a.f*ty + a.f, + } } diff --git a/f32/affine_test.go b/f32/affine_test.go index 69da14b5..4077b8d9 100644 --- a/f32/affine_test.go +++ b/f32/affine_test.go @@ -13,6 +13,16 @@ func eq(p1, p2 Point) bool { return math.Abs(math.Sqrt(float64(dx*dx+dy*dy))) < tol } +func eqaff(x, y Affine2D) bool { + tol := 1e-5 + return math.Abs(float64(x.a-y.a)) < tol && + math.Abs(float64(x.b-y.b)) < tol && + math.Abs(float64(x.c-y.c)) < tol && + math.Abs(float64(x.d-y.d)) < tol && + math.Abs(float64(x.e-y.e)) < tol && + math.Abs(float64(x.f-y.f)) < tol +} + func TestTransformOffset(t *testing.T) { p := Point{X: 1, Y: 2} o := Point{X: 2, Y: -3} @@ -84,6 +94,49 @@ func TestTransformMultiply(t *testing.T) { } } +func TestPrimes(t *testing.T) { + xa := NewAffine2D(9, 11, 13, 17, 19, 23) + xb := NewAffine2D(29, 31, 37, 43, 47, 53) + + pa := Point{X: 2, Y: 3} + pb := Point{X: 5, Y: 7} + + for _, test := range []struct { + x Affine2D + p Point + exp Point + }{ + {x: xa, p: pa, exp: Pt(64, 114)}, + {x: xa, p: pb, exp: Pt(135, 241)}, + {x: xb, p: pa, exp: Pt(188, 280)}, + {x: xb, p: pb, exp: Pt(399, 597)}, + } { + got := test.x.Transform(test.p) + if !eq(got, test.exp) { + t.Errorf("%v.Transform(%v): have %v, want %v", test.x, test.p, got, test.exp) + } + } + + for _, test := range []struct { + x Affine2D + exp Affine2D + }{ + {x: xa, exp: NewAffine2D(-1.1875, 0.6875, -0.375, 1.0625, -0.5625, -0.875)}, + {x: xb, exp: NewAffine2D(1.5666667, -1.0333333, -3.2000008, -1.4333333, 1-0.03333336, 1.7999992)}, + } { + got := test.x.Invert() + if !eqaff(got, test.exp) { + t.Errorf("%v.Invert(): have %v, want %v", test.x, got, test.exp) + } + } + + got := xa.Mul(xb) + exp := NewAffine2D(734, 796, 929, 1310, 1420, 1659) + if !eqaff(got, exp) { + t.Errorf("%v.Mul(%v): have %v, want %v", xa, xb, got, exp) + } +} + func TestTransformScaleAround(t *testing.T) { p := Pt(-1, -1) target := Pt(-6, -13) @@ -119,3 +172,61 @@ func TestMulOrder(t *testing.T) { t.Error("multiplication / transform order not as expected") } } + +func BenchmarkTransformOffset(b *testing.B) { + p := Point{X: 1, Y: 2} + o := Point{X: 0.5, Y: 0.5} + aff := Affine2D{}.Offset(o) + + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformScale(b *testing.B) { + p := Point{X: 1, Y: 2} + s := Point{X: 0.5, Y: 0.5} + aff := Affine2D{}.Scale(Point{}, s) + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformRotate(b *testing.B) { + p := Point{X: 1, Y: 2} + a := float32(math.Pi / 2) + aff := Affine2D{}.Rotate(Point{}, a) + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformTranslateMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} + +func BenchmarkTransformScaleMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Scale(Point{}, Point{X: 0.4, Y: -0.5}) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} + +func BenchmarkTransformMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Rotate(Point{}, math.Pi/7) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +}