You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kev/Drawer/Clipper2Lib-/CPP/BenchMark/GetIntersectPtBenchmark.cpp

580 lines
18 KiB
C++

#include "benchmark/benchmark.h"
#include "clipper2/clipper.h"
#include "clipper2/clipper.core.h"
#include "CommonUtils.h"
#include "ClipFileLoad.h"
#include <iomanip>
#include <cstdlib>
#include <random>
using namespace Clipper2Lib;
enum TextColor {
reset = 0,
//normal text colors ...
red = 31, green = 32, yellow = 33, blue = 34, magenta = 35, cyan = 36, white = 37,
//bold text colors ...
red_bold = 91, green_bold = 92, yellow_bold = 93, blue_bold = 94,
magenta_bold = 95, cyan_bold = 96, white_bold = 97
};
//////////////////////////////////////////////////////////////////////////////////////
// SetConsoleTextColor: a simple class to adjust Console Text Colors (Windows & Linux)
//////////////////////////////////////////////////////////////////////////////////////
struct SetConsoleTextColor
{
private:
TextColor _color;
public:
SetConsoleTextColor(TextColor color) : _color(color) {};
friend std::ostream& operator<< (std::ostream& out, SetConsoleTextColor const& scc)
{
return out << "\x1B[" << scc._color << "m";
}
};
//////////////////////////////////////////////////////////////////////////////////////
// Int128 - class that very minimally supports 128bit integer math
//////////////////////////////////////////////////////////////////////////////////////
class Int128
{
public:
uint64_t lo;
int64_t hi;
Int128(int64_t _lo = 0)
{
lo = (uint64_t)_lo;
if (_lo < 0) hi = -1; else hi = 0;
}
Int128(const Int128& val) : lo(val.lo), hi(val.hi) {}
Int128(const int64_t& _hi, const uint64_t& _lo) : lo(_lo), hi(_hi) {}
Int128& operator = (const int64_t& val)
{
lo = (uint64_t)val;
if (val < 0) hi = -1; else hi = 0;
return *this;
}
bool operator == (const Int128& val) const
{
return (hi == val.hi && lo == val.lo);
}
bool operator != (const Int128& val) const
{
return !(*this == val);
}
bool operator > (const Int128& val) const
{
return (hi != val.hi) ? hi > val.hi: lo > val.lo;
}
bool operator < (const Int128& val) const
{
return (hi != val.hi) ? hi < val.hi : lo < val.lo;
}
bool operator >= (const Int128& val) const
{
return !(*this < val);
}
bool operator <= (const Int128& val) const
{
return !(*this > val);
}
bool is_zero() const
{
return (hi == 0 && lo == 0);
}
bool is_negative() const
{
return (hi < 0);
}
Int128& operator += (const Int128& rhs)
{
hi += rhs.hi;
lo += rhs.lo;
if (lo < rhs.lo) hi++;
return *this;
}
Int128 operator + (const Int128& rhs) const
{
Int128 result(*this);
result += rhs;
return result;
}
Int128& operator -= (const Int128& rhs)
{
*this += -rhs;
return *this;
}
Int128 operator - (const Int128& rhs) const
{
Int128 result(*this);
result -= rhs;
return result;
}
Int128 operator-() const //unary negation
{
return (lo == 0) ? Int128(-hi, 0) : Int128(~hi, ~lo + 1);
}
void negate()
{
if (lo == 0) hi = -hi;
else { hi = ~hi; lo = ~lo + 1; }
}
operator double() const
{
const double shift64 = 18446744073709551616.0; //2^64
if (hi < 0)
{
if (lo == 0) return (double)hi * shift64;
else return -(double)(~lo + ~hi * shift64);
}
else
return (double)(lo + hi * shift64);
}
};
static inline Int128 multiply(int64_t lhs, int64_t rhs)
{
bool negate = (lhs < 0) != (rhs < 0);
if (lhs < 0) lhs = -lhs;
uint64_t int1Hi = uint64_t(lhs) >> 32;
uint64_t int1Lo = uint64_t(lhs & 0xFFFFFFFF);
if (rhs < 0) rhs = -rhs;
uint64_t int2Hi = uint64_t(rhs) >> 32;
uint64_t int2Lo = uint64_t(rhs & 0xFFFFFFFF);
uint64_t a = int1Hi * int2Hi;
uint64_t b = int1Lo * int2Lo;
uint64_t c = int1Hi * int2Lo + int1Lo * int2Hi;
Int128 result;
result.hi = a + (c >> 32);
result.lo = c << 32;
result.lo += b;
if (result.lo < b) result.hi++;
return negate ? -result : result;
};
static int64_t divide(Int128 dividend, Int128 divisor)
{
// this function assumes that the parameter values will
// generate a result that fits into a 64bit integer.
bool negate = (divisor.hi < 0) != (dividend.hi < 0);
if (dividend.hi < 0) dividend = -dividend;
if (divisor.hi < 0) divisor = -divisor;
if (divisor.lo == 0 && divisor.hi == 0)
throw "Int128: divide by zero error";
if (dividend == divisor) return negate ? -1 : 1;
if (divisor > dividend) return 0;
Int128 cntr = Int128(1);
while (divisor.hi >= 0 && divisor <= dividend)
{
divisor.hi <<= 1;
if ((int64_t)divisor.lo < 0) divisor.hi++;
divisor.lo <<= 1;
cntr.hi <<= 1;
if ((int64_t)cntr.lo < 0) cntr.hi++;
cntr.lo <<= 1;
}
divisor.lo >>= 1;
if (divisor.hi & 1)
divisor.lo |= 0x8000000000000000LL;
divisor.hi >>= 1;
cntr.lo >>= 1;
if (cntr.hi & 1)
cntr.lo |= 0x8000000000000000LL;
cntr.hi >>= 1;
Int128 result = Int128(0);
while (cntr.hi != 0 || cntr.lo != 0)
{
if (dividend >= divisor)
{
dividend -= divisor;
result.hi |= cntr.hi;
result.lo |= cntr.lo;
}
divisor.lo >>= 1;
if (divisor.hi & 1)
divisor.lo |= 0x8000000000000000LL;
divisor.hi >>= 1;
cntr.lo >>= 1;
if (cntr.hi & 1)
cntr.lo |= 0x8000000000000000LL;
cntr.hi >>= 1;
}
if (result.hi || (int64_t)result.lo < 0)
return negate ? INT64_MIN : INT64_MAX;
else
return negate ? -(int64_t)result.lo : result.lo;
}
static inline int64_t muldiv(Int128 lhs, int64_t rhs, Int128 divisor)
{
// this function assumes that the parameter values will
// generate a result that fits into a 64bit integer.
int64_t sign = (lhs.is_negative() != divisor.is_negative()) != (rhs < 0) ? -2 : 2;
if (lhs.is_negative()) lhs.negate();
if (divisor.is_negative()) divisor.negate();
if (rhs < 0) rhs = -rhs;
// if 'lhs' is very large, then 'divisor' will be very large too
while (lhs.hi && divisor.hi)
{
// divide dividend and divisor by 2 ...
lhs.lo >>= 1;
if (lhs.hi & 1)
lhs.lo |= 0x8000000000000000LL;
lhs.hi >>= 1;
divisor.lo >>= 1;
if (divisor.hi & 1)
divisor.lo |= 0x8000000000000000LL;
divisor.hi >>= 1;
}
lhs.lo >>= 1; // divide by 2 to avoid casting a 'sign' bit
Int128 result = multiply((int64_t)lhs.lo, rhs);
result.hi += lhs.hi * rhs;
return divide(result, divisor) * sign; // and multiplies by 2
};
/////////////////////////////////////////////////////////
// Several GetIntersectPoint functions for testing
/////////////////////////////////////////////////////////
const int number_of_test_functions = 4;
typedef std::function<bool(const Point64&, const Point64&,
const Point64&, const Point64&, Point64&)> GipFunction;
// GIP_Current: This is Clipper2's current GetIntersectPoint.
// It's definitely the fastest function, but its accuracy declines
// a little when using very large 64bit integers (eg +/-10e17).
static bool GIP_Current(const Point64& ln1a, const Point64& ln1b,
const Point64& ln2a, const Point64& ln2b, Point64& ip)
{
double dx1 = static_cast<double>(ln1b.x - ln1a.x);
double dy1 = static_cast<double>(ln1b.y - ln1a.y);
double dx2 = static_cast<double>(ln2b.x - ln2a.x);
double dy2 = static_cast<double>(ln2b.y - ln2a.y);
double det = dy1 * dx2 - dy2 * dx1;
if (det == 0.0) return false;
double t = ((double)(ln1a.x - ln2a.x) * dy2 - (double)(ln1a.y - ln2a.y) * dx2) / det;
if (t <= 0.0) ip = ln1a;
else if (t >= 1.0) ip = ln1b;
else
{
ip.x = static_cast<int64_t>(ln1a.x + t * dx1);
ip.y = static_cast<int64_t>(ln1a.y + t * dy1);
}
return true;
}
// GIP_Func_F: This is mathVertexLineLineIntersection_F
// https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253
#define CC_MIN(x,y) ((x)>(y)?(y):(x))
#define CC_MAX(x,y) ((x)<(y)?(y):(x))
static bool GIP_Func_F(const Point64& ln1a, const Point64& ln1b,
const Point64& ln2a, const Point64& ln2b, Point64& ip)
{
double ln1dy = (double)(ln1b.y - ln1a.y);
double ln1dx = (double)(ln1a.x - ln1b.x);
double ln2dy = (double)(ln2b.y - ln2a.y);
double ln2dx = (double)(ln2a.x - ln2b.x);
double det = (ln2dy * ln1dx) - (ln1dy * ln2dx);
if (det == 0.0) return false;
int64_t bb0minx = CC_MIN(ln1a.x, ln1b.x);
int64_t bb0miny = CC_MIN(ln1a.y, ln1b.y);
int64_t bb0maxx = CC_MAX(ln1a.x, ln1b.x);
int64_t bb0maxy = CC_MAX(ln1a.y, ln1b.y);
int64_t bb1minx = CC_MIN(ln2a.x, ln2b.x);
int64_t bb1miny = CC_MIN(ln2a.y, ln2b.y);
int64_t bb1maxx = CC_MAX(ln2a.x, ln2b.x);
int64_t bb1maxy = CC_MAX(ln2a.y, ln2b.y);
int64_t originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1;
int64_t originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1;
double ln0c = (ln1dy * (double)(ln1a.x - originx)) + (ln1dx * (double)(ln1a.y - originy));
double ln1c = (ln2dy * (double)(ln2a.x - originx)) + (ln2dx * (double)(ln2a.y - originy));
double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det;
double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det;
ip.x = originx + (int64_t)nearbyint(hitx);
ip.y = originy + (int64_t)nearbyint(hity);
return true;
}
// GIP_F_Mod: GIP_Func_F except replaces nearbyint with static casts.
// Surprisingly, while this function is faster that GIP_Func_F here,
// it's much slower than both GIP_Func_F and GIP_Current when using it
// as a replacement for GetIntersectPoint() in clipper.core.h.
static bool GIP_F_Mod(const Point64& ln1a, const Point64& ln1b,
const Point64& ln2a, const Point64& ln2b, Point64& ip)
{
double ln1dy = (double)(ln1b.y - ln1a.y);
double ln1dx = (double)(ln1a.x - ln1b.x);
double ln2dy = (double)(ln2b.y - ln2a.y);
double ln2dx = (double)(ln2a.x - ln2b.x);
double det = (ln2dy * ln1dx) - (ln1dy * ln2dx);
if (det == 0.0) return false;
int64_t bb0minx = CC_MIN(ln1a.x, ln1b.x);
int64_t bb0miny = CC_MIN(ln1a.y, ln1b.y);
int64_t bb0maxx = CC_MAX(ln1a.x, ln1b.x);
int64_t bb0maxy = CC_MAX(ln1a.y, ln1b.y);
int64_t bb1minx = CC_MIN(ln2a.x, ln2b.x);
int64_t bb1miny = CC_MIN(ln2a.y, ln2b.y);
int64_t bb1maxx = CC_MAX(ln2a.x, ln2b.x);
int64_t bb1maxy = CC_MAX(ln2a.y, ln2b.y);
int64_t originx = (CC_MIN(bb0maxx, bb1maxx) + CC_MAX(bb0minx, bb1minx)) >> 1;
int64_t originy = (CC_MIN(bb0maxy, bb1maxy) + CC_MAX(bb0miny, bb1miny)) >> 1;
double ln0c = (ln1dy * (double)(ln1a.x - originx)) + (ln1dx * (double)(ln1a.y - originy));
double ln1c = (ln2dy * (double)(ln2a.x - originx)) + (ln2dx * (double)(ln2a.y - originy));
double hitx = ((ln1dx * ln1c) - (ln2dx * ln0c)) / det;
double hity = ((ln2dy * ln0c) - (ln1dy * ln1c)) / det;
ip.x = originx + static_cast<int64_t>(hitx);
ip.y = originy + static_cast<int64_t>(hity);
return true;
}
// GIP_128: GetIntersectPoint using 128bit integer precision
// This function is the most precise, but it's also very slow.
static bool GIP_128(const Point64& ln1a, const Point64& ln1b,
const Point64& ln2a, const Point64& ln2b, Point64& ip)
{
int64_t dx1 = ln1b.x - ln1a.x;
int64_t dy1 = ln1b.y - ln1a.y;
int64_t dx2 = ln2b.x - ln2a.x;
int64_t dy2 = ln2b.y - ln2a.y;
Int128 det = multiply(dy1, dx2) - multiply(dy2, dx1);
if (det.is_zero()) return false;
Int128 t_num = multiply(ln1a.x - ln2a.x, dy2) - multiply(ln1a.y - ln2a.y, dx2);
bool is_negative = t_num.is_negative() != det.is_negative();
if (t_num.is_zero() || is_negative)
ip = ln1a;
else if (t_num.is_negative() == -t_num > -det)
ip = ln1b;
else
{
ip.x = ln1a.x + muldiv(t_num, dx1, det);
ip.y = ln1a.y + muldiv(t_num, dy1, det);
}
return true;
}
static inline GipFunction GetGipFunc(int64_t index)
{
switch (index)
{
case 0: return GIP_Current;
case 1: return GIP_Func_F;
case 2: return GIP_F_Mod;
case 3: return GIP_128;
default: throw "Invalid function!";
}
}
static inline std::string GetGipFuncName(int64_t index)
{
switch (index)
{
case 0: return "GIP_Current";
case 1: return "GIP_Func_F ";
case 2: return "GIP_F_Mod ";
case 3: return "GIP_128 ";
default: throw "Invalid function!";
}
}
/////////////////////////////////////////////////////////
// Other miscellaneous functions
/////////////////////////////////////////////////////////
double GetSineFrom3Points(const Point64& a, const Point64& b, const Point64& c)
{
double dpB = DotProduct(a, b, c);
double SqrCosB = dpB * dpB / (DistanceSqr(a, b) * DistanceSqr(b, c));
double cos2B = SqrCosB * 2 - 1; // trig. itentity
return std::sqrt(1 - cos2B); // sin(B) = Sqrt(1-cos(2B))
}
static inline Point64 MakeRandomPoint(int64_t min_val, int64_t max_val)
{
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int64_t> x(min_val, max_val);
std::uniform_int_distribution<int64_t> y(min_val, max_val);
return Point64(x(gen), y(gen));
}
/////////////////////////////////////////////////////////
// global data storage
/////////////////////////////////////////////////////////
struct TestRecord
{
public:
Point64 actual, pt1, pt2, pt3, pt4;
std::vector<Point64> results;
TestRecord(const Point64& intersect_pt,
const Point64& p1, const Point64& p2, const Point64& p3, const Point64& p4) :
actual(intersect_pt), pt1(p1), pt2(p2), pt3(p3), pt4(p4) {
results.resize(number_of_test_functions);
};
};
std::vector<TestRecord> tests;
typedef std::vector<TestRecord>::iterator test_iter;
/////////////////////////////////////////////////////////
// Benchmark callback functions
/////////////////////////////////////////////////////////
static void BM_GIP(benchmark::State& state)
{
int64_t idx = state.range(0);
state.SetLabel(GetGipFuncName(idx));
GipFunction func = GetGipFunc(idx);
for (auto _ : state)
{
for (test_iter test = tests.begin(); test != tests.end(); ++test)
{
Point64 ip;
func(test->pt1, test->pt2, test->pt3, test->pt4, ip);
test->results[idx] = ip;
}
}
}
static void CustomArguments(benchmark::internal::Benchmark* b)
{
for (int i = 0; i < number_of_test_functions; ++i)
b->Args({ i });
}
/////////////////////////////////////////////////////////
// Main Entry
/////////////////////////////////////////////////////////
int main(int argc, char** argv)
{
//setup benchmarking ...
benchmark::Initialize(0, nullptr);
BENCHMARK(BM_GIP)->Apply(CustomArguments);
// the closer test segments are to collinear, the less accurate
// calculations will be in determining their intersection points.
const double min_angle_degrees = 0.5;
const double sine_min_angle = std::sin(min_angle_degrees *PI / 180.0);
bool first_pass = true;
for (int current_pow10 = 12; current_pow10 <= 18; ++current_pow10)
{
// using random coordinates that are restricted to the specified
// power of 10 range, create multiple TestRecords containing
// segment pairs that intersect at their midpoints
int64_t max_coord = static_cast<int64_t>(pow(10, current_pow10));
for (int64_t i = 0; i < 100000; ++i)
{
Point64 ip1 = MakeRandomPoint(-max_coord, max_coord);
Point64 ip2 = MakeRandomPoint(-max_coord, max_coord);
Point64 actual = MidPoint(ip1, ip2);
Point64 ip3 = MakeRandomPoint(-max_coord, max_coord);
Point64 ip4 = ReflectPoint(ip3, actual);
// Exclude segments that are **almost** collinear.
if (std::abs(GetSineFrom3Points(ip1, actual, ip3)) < sine_min_angle) continue;
// Alternatively, just exclude segments that are collinear
//if (!CrossProduct(ip1, actual, ip3)) continue;
tests.push_back(TestRecord(actual, ip1, ip2, ip3, ip4));
}
if (first_pass)
{
// only benchmark the GetIntersectPoint functions once because changing
// the maximum range of coordinates won't affect function performance.
first_pass = false;
std::cout << std::endl << SetConsoleTextColor(green_bold) <<
"Benchmark GetIntersectPoint performance ... " << SetConsoleTextColor(reset) <<
std::endl << std::endl;
benchmark::RunSpecifiedBenchmarks();
std::cout << std::endl << std::endl << SetConsoleTextColor(green_bold) <<
"Compare function accuracy ..." << SetConsoleTextColor(reset) << std::endl <<
"and show how it deteriorates when using very large coordinate ranges." << std::endl <<
"Distance error is the distance between the calculated and actual intersection points." << std::endl <<
"(The largest errors will occur whenever the segments are close to collinear.)" << std::endl;
}
else
{
for (int i = 0; i < number_of_test_functions; ++i)
{
// although we're not benchmarking, we still need to collect the calculated
// intersect points of each TestRecord for each participating function.
// (In first_pass above, benchmark::RunSpecifiedBenchmarks() does this internally.)
Point64 ip;
GipFunction gip_func = GetGipFunc(i);
for (test_iter test = tests.begin(); test != tests.end(); ++test)
{
gip_func(test->pt1, test->pt2, test->pt3, test->pt4, ip);
test->results[i] = ip;
}
}
}
double avg_dists[number_of_test_functions] = { 0 };
double worst_dists[number_of_test_functions] = { 0 };
for (test_iter test = tests.begin(); test != tests.end(); ++test)
for (int i = 0; i < number_of_test_functions; ++i)
{
double dist = Distance(test->actual, test->results[i]);
avg_dists[i] += dist;
if (dist > worst_dists[i]) worst_dists[i] = dist;
}
std::cout << std::endl << SetConsoleTextColor(cyan_bold) <<
"Coordinate ranges between +/-10^" << current_pow10 <<
SetConsoleTextColor(reset) << std::endl;
for (int i = 0; i < number_of_test_functions; ++i)
{
avg_dists[i] /= tests.size();
std::cout << std::fixed << GetGipFuncName(i) <<
": average distance error = " << std::setprecision(2) << avg_dists[i] <<
"; largest dist. = " << std::setprecision(0) << worst_dists[i] << std::endl;
}
tests.clear();
}
return 0;
}