Skip to content

Commit

Permalink
Speed up SinCosDegrees() by manual range reduction.
Browse files Browse the repository at this point in the history
SinCosDegrees() is fortunate enough to take in its arguments in degrees
(360 degrees = 1 turn) instead of radians (2pi radians = 1 turn).
Accurate range reduction in degrees is much simpler than in radians;
45 is a whole number and exactly representable as a double, unlike pi/2.
This means that we can get greater accuracy _and_ higher speed than the
current code of fmod() + internal sincos() range reduction, by doing it
ourselves, mostly branch-free. (In particular, fmod() appears to be very
slow on macOS.) This gives a small boost in MotionMark for the Leaves
and/or Multiply subtests, which both make heavy use of rotate() in CSS.

MotionMark (mac-m1_mini_2020-perf, 95% CI, only significant results):
motionmark_ramp_leaves                    [ +0.7%,  +1.0%]

Same with win-10-perf:
motionmark_ramp_multiply                  [ +0.6%,  +1.9%]

Change-Id: Iedf1822d2995408531a623d0768373703941e3e2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4537360
Reviewed-by: danakj <danakj@chromium.org>
Commit-Queue: Steinar H Gunderson <sesse@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1150624}
  • Loading branch information
Steinar H. Gunderson authored and Chromium LUCI CQ committed May 30, 2023
1 parent 230315b commit 109a4bf
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 25 deletions.
12 changes: 12 additions & 0 deletions base/numerics/math_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ namespace base {
constexpr double kPiDouble = 3.14159265358979323846;
constexpr float kPiFloat = 3.14159265358979323846f;

// pi/180 and 180/pi. These are correctly rounded from the true
// mathematical value, unlike what you'd get from e.g.
// 180.0f / kPiFloat.
constexpr double kDegToRadDouble = 0.017453292519943295769;
constexpr float kDegToRadFloat = 0.017453292519943295769f;
constexpr double kRadToDegDouble = 57.295779513082320876798;
constexpr float kRadToDegFloat = 57.295779513082320876798f;

// sqrt(1/2) = 1/sqrt(2).
constexpr double kSqrtHalfDouble = 0.70710678118654752440;
constexpr float kSqrtHalfFloat = 0.70710678118654752440f;

// The mean acceleration due to gravity on Earth in m/s^2.
constexpr double kMeanGravityDouble = 9.80665;
constexpr float kMeanGravityFloat = 9.80665f;
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified third_party/blink/web_tests/transforms/matrix-02-expected.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ui/gfx/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ test("gfx_unittests") {
"geometry/linear_gradient_unittest.cc",
"geometry/mask_filter_info_unittest.cc",
"geometry/rrect_f_unittest.cc",
"geometry/sin_cos_degrees_unittest.cc",
"geometry/transform_operations_unittest.cc",
"geometry/transform_unittest.cc",
"image/buffer_w_stream_unittest.cc",
Expand Down
1 change: 1 addition & 0 deletions ui/gfx/geometry/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ component("geometry") {
"resize_utils.h",
"rounded_corners_f.cc",
"rounded_corners_f.h",
"sin_cos_degrees.h",
"size.cc",
"size.h",
"size_conversions.cc",
Expand Down
8 changes: 4 additions & 4 deletions ui/gfx/geometry/angle_conversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
namespace gfx {

GEOMETRY_EXPORT constexpr double DegToRad(double deg) {
return deg * base::kPiDouble / 180.0;
return deg * base::kDegToRadDouble;
}
GEOMETRY_EXPORT constexpr float DegToRad(float deg) {
return deg * base::kPiFloat / 180.0f;
return deg * base::kDegToRadFloat;
}

GEOMETRY_EXPORT constexpr double RadToDeg(double rad) {
return rad * 180.0 / base::kPiDouble;
return rad * base::kRadToDegDouble;
}
GEOMETRY_EXPORT constexpr float RadToDeg(float rad) {
return rad * 180.0f / base::kPiFloat;
return rad * base::kRadToDegFloat;
}

} // namespace gfx
Expand Down
105 changes: 105 additions & 0 deletions ui/gfx/geometry/sin_cos_degrees.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef UI_GFX_GEOMETRY_SIN_COS_DEGREES_H_
#define UI_GFX_GEOMETRY_SIN_COS_DEGREES_H_

#include "angle_conversions.h"
#include "base/numerics/math_constants.h"

#include <algorithm>
#include <cmath>

namespace gfx {

struct SinCos {
double sin;
double cos;
bool IsZeroAngle() const { return sin == 0 && cos == 1; }
};

inline SinCos SinCosDegrees(double degrees) {
// Some math libraries have poor accuracy with large arguments,
// so range-reduce explicitly before we call sin() or cos(). However, unless
// we're _really_ large (out of range of an int), we can do that faster than
// fmod(), since we have an integer divisor (and as an extra bonus, we've
// already got it precomputed). We pick a pretty arbitrary limit that should
// be safe.
//
// We range-reduce to [0..45]. This should hit the fast path of sincos()
// on most platforms (since no further reduction is needed; reducing
// accurately modulo a trancendental can we slow), using only branches that
// should be possible to do using conditional operations; using a switch
// instead would be possible, but benchmarked much slower on M1.
// For platforms that don't use sincos() (e.g., it seems Clang doesn't
// manage the rewrite on Linux), we also save on having the range reduction
// done only once.
if (degrees > -90000000.0 && degrees < 90000000.0) {
// Make sure 0, 90, 180 and 270 degrees get exact results. (We also have
// precomputed values for 45, 135, etc., but only as a side effect of using
// 45 instead of 90, for the benefit of the range reduction algorithm below.
// The error for e.g. sin(45 degrees) is typically only 1 ulp.)
double n45degrees = degrees / 45.0;
int octant = static_cast<int>(n45degrees);
if (octant == n45degrees) {
constexpr SinCos kSinCosN45[] = {
{0, 1}, {base::kSqrtHalfDouble, base::kSqrtHalfDouble},
{1, 0}, {base::kSqrtHalfDouble, -base::kSqrtHalfDouble},
{0, -1}, {-base::kSqrtHalfDouble, -base::kSqrtHalfDouble},
{-1, 0}, {-base::kSqrtHalfDouble, base::kSqrtHalfDouble}};
return kSinCosN45[octant & 7];
}

if (degrees < 0) {
// This will cause the range-reduction below to move us
// into [0..45], as desired, instead of [-45..0].
--octant;
}
degrees -= octant * 45.0; // Range-reduce to [0..45].

// Deal with 45..90 the same as 45..0. This also covers
// 135..180, 225..270 and 315..360, i.e. the odd octants.
// The relevant trigonometric identities is that
// sin(90 - a) = cos(a) and vice versa; we do the sin/cos
// flip below.
if (octant & 1) {
degrees = 45.0 - degrees;
}

double rad = DegToRad(degrees);
double s = std::sin(rad);
double c = std::cos(rad);

// 45..135 and -135..-45 can be moved into the opposite areas
// simply by flipping the x and y axis (in conjunction with
// the conversion from CW to CCW done above).
using std::swap;
if ((octant + 1) & 2) {
swap(s, c);
}

// For sine, 180..360 (lower half) is the same as 0..180,
// except negative.
if (octant & 4) {
s = -s;
}

// For cosine, 90..270 (right half) is the same as -90..90,
// except negative.
if ((octant + 2) & 4) {
c = -c;
}

return SinCos{s, c};
}

// Slow path for extreme cases.
degrees = std::fmod(degrees, 360.0);
double rad = DegToRad(degrees);
return SinCos{std::sin(rad), std::cos(rad)};
}

} // namespace gfx

#endif // UI_GFX_GEOMETRY_SIN_COS_DEGREES_H_
54 changes: 54 additions & 0 deletions ui/gfx/geometry/sin_cos_degrees_unittest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#define _USE_MATH_DEFINES // To get M_PI on Windows.

#include "ui/gfx/geometry/sin_cos_degrees.h"

#include <math.h>

#include "testing/gtest/include/gtest/gtest.h"

namespace gfx {
namespace {

TEST(SinCosDegreesTest, ExactValues) {
for (int turn = -5 * 360; turn <= 5 * 360; turn += 360) {
EXPECT_EQ(0.0, SinCosDegrees(turn + 0).sin);
EXPECT_EQ(1.0, SinCosDegrees(turn + 0).cos);

EXPECT_EQ(1.0, SinCosDegrees(turn + 90).sin);
EXPECT_EQ(0.0, SinCosDegrees(turn + 90).cos);

EXPECT_EQ(0.0, SinCosDegrees(turn + 180).sin);
EXPECT_EQ(-1.0, SinCosDegrees(turn + 180).cos);

EXPECT_EQ(-1.0, SinCosDegrees(turn + 270).sin);
EXPECT_EQ(0.0, SinCosDegrees(turn + 270).cos);
}
}

TEST(SinCosDegreesTest, CloseToLibc) {
for (int d = -3600; d <= 3600; ++d) {
double degrees = (d * 0.1);
EXPECT_NEAR(sin(degrees * M_PI / 180.0), SinCosDegrees(degrees).sin, 1e-6);
EXPECT_NEAR(cos(degrees * M_PI / 180.0), SinCosDegrees(degrees).cos, 1e-6);
}
}

TEST(SinCosDegreesTest, AccurateRangeReduction) {
EXPECT_EQ(SinCosDegrees(90000123).sin, SinCosDegrees(90000123).sin);
EXPECT_EQ(SinCosDegrees(90000123).cos, SinCosDegrees(90000123).cos);

EXPECT_EQ(SinCosDegrees(90e5).sin, 0.0);
EXPECT_EQ(SinCosDegrees(90e5).cos, 1.0);
}

TEST(SinCosDegreesTest, HugeValues) {
EXPECT_NEAR(SinCosDegrees(360e10 + 20).sin, sin(20 * (M_PI / 180.0)), 1e-6);
EXPECT_NEAR(SinCosDegrees(360e10 + 20).cos, cos(20 * (M_PI / 180.0)), 1e-6);
}

} // namespace
} // namespace gfx
22 changes: 1 addition & 21 deletions ui/gfx/geometry/transform.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "ui/gfx/geometry/quaternion.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/sin_cos_degrees.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/gfx/geometry/vector3d_f.h"

Expand All @@ -34,27 +35,6 @@ double TanDegrees(double degrees) {
return std::tan(DegToRad(degrees));
}

struct SinCos {
double sin;
double cos;
bool IsZeroAngle() const { return sin == 0 && cos == 1; }
};

SinCos SinCosDegrees(double degrees) {
double n90degrees = degrees / 90.0;
int n = static_cast<int>(n90degrees);
if (n == n90degrees) {
n %= 4;
if (n < 0)
n += 4;
constexpr SinCos kSinCosN90[] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
return kSinCosN90[n];
}
// fmod is to reduce errors of DegToRad() with large |degrees|.
double rad = DegToRad(std::fmod(degrees, 360.0));
return SinCos{std::sin(rad), std::cos(rad)};
}

inline bool ApproximatelyZero(double x, double tolerance) {
return std::abs(x) <= tolerance;
}
Expand Down

0 comments on commit 109a4bf

Please sign in to comment.