【问题标题】:A More Efficient Haversine Function更高效的半正弦函数
【发布时间】:2017-01-12 20:02:44
【问题描述】:

在我的previous question 中,我希望根据函数结果加快列表选择。现在,我的瓶颈是函数本身。

这是一个基本的Haversine函数,使用下面的代码:

private static double Haversine(double lat1, double lat2, double lon1, double lon2)
{            
    const double r = 6371e3; // meters
    var dlat = (lat2 - lat1)/2;
    var dlon = (lon2 - lon1)/2;

    var q = Math.Pow(Math.Sin(dlat), 2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Pow(Math.Sin(dlon), 2);
    var c = 2 * Math.Atan2(Math.Sqrt(q), Math.Sqrt(1 - q));

    var d = r * c;
    return d / 1000;
}

那么...为什么需要这么快?问题是我把它称为很多。想想 16,500,000 次以北。

显然,这很多。在我的用例中,我向它传递了它必须从中获取位置数据的对象,然后将纬度和经度转换为弧度,这进一步增加了时间(仅增加了大约 15%)。我不知道对此我能做些什么,但我确实知道,通过将它以弧度(如上)的形式纯粹传递,它需要大约 4.5 秒 - 这是我实现中处理时间的 75% 以上。为 q 和 c 赋值的行似乎占用了最多的时间。

因为它被称为 a lot,所以我希望它有点更快。我对多线程解决方案持开放态度(目前我自己也在研究一个),但考虑到我上一个问题(上面链接)中的用例,实现起来可能有点困难。

【问题讨论】:

  • 最好的优化是少调用它...快速查看其他问题表明您需要“最近的 5”或其他东西。这使得这有点像XY question
  • 为什么转换成弧度或从弧度转换会招致如此沉重的惩罚?它实际上只是一个单一的乘法。当然,它是一个浮点运算,但它仍然应该远不及那些三角函数那么昂贵。
  • 另外,我正在查看 Wikipedia 上的半正弦公式的定义,并没有提到需要做 Atan 的任何地方。您实际上试图让此方法返回的值是多少?
  • @HenkHolterman 我认为这始终是看待事物的好方法,也是我经常考虑的事情。如果您可以立即看到可以使用改进的东西,我会打开它。与此同时,这是一项昂贵的手术,我想瘦身。
  • @Abion47 这只是我使用的实现。我认为无论有没有都可以做到,无论哪种方式,距离都是一样的。 我正在寻找两个纬度-经度对之间的距离。 将弧度转换为 1650 万次需要大约 1-1.5 秒。实际上,如果我愿意,我可以将所有值存储为弧度,所以认为这不是问题。作为记录,我使用的 Haversine 公式是从这里提取的 movable-type.co.uk/scripts/latlong.html

标签: c# math optimization trigonometry


【解决方案1】:

这是我能得到答案的最优化(据我所知,这是答案可能可能在不对公式本身进行一些向导级优化的情况下得到的最优化的答案):

private static double Haversine(double lat1, double lat2, double lon1, double lon2)
{
    const double r = 6371; // meters

    var sdlat = Math.Sin((lat2 - lat1) / 2);
    var sdlon = Math.Sin((lon2 - lon1) / 2);
    var q = sdlat * sdlat + Math.Cos(lat1) * Math.Cos(lat2) * sdlon * sdlon;
    var d = 2 * r * Math.Asin(Math.Sqrt(q));

    return d;
}

在我的机器上,这个公式在运行 1650 万次时,运行时间几乎正好是 3 秒,而上述版本的运行时间接近 5 秒。

但是,我认为最大的优化可能是在实际调用此方法的系统中。 500 个纬度-经度对中的每一对 33,000 次?这是一个可能迫切需要优化自身的系统。对于初学者,您可以首先计算配对的线性距离平方,并且只处理低于某个阈值的配对。或者您可以维护一个查找表以避免多次计算同一对。或者,根据这 33,000 个数字的来源,您可以确定优先级,这样您就不需要几乎调用该方法。

【讨论】:

  • 嘿,我从来没有说过它是完美的:)。是的,在这种情况下,我最终选择了 10 个最近的坐标位置(通过线性测量),然后通过计算 Haversine 距离将其缩小到 5 个,这将计算时间减少了一半。我计划在我的用例完成之前进行很多更改,但我认为保持不变的一件事是我将提供一个坐标,并返回接近该坐标的坐标。
  • 对比GeoCoordinate.GetDistanceTo方法的结果差别很大,我觉得微软的一致。
  • 在过去使用 C++ 时,乘以 0.5 比除以 2 更快。这是否也加快了 C# 的速度?
  • @SimonHughes 理论上是的,但它的性能提升是如此之小,以至于它完全微不足道。看看这个fiddle 的结果,即使运行十亿次比较也表明使用乘法而不是除法的收益只有几毫秒——如此之小,以至于它落在了基准测试的误差范围内。在你的程序中总会有其他事情比试图对程序的算法进行微观管理更好。
  • @SimonHughes 完全有可能的另一件事是,在最后一个示例中,C# 编译器可能已经完全优化了算法,因此基准测试实际上只是测试单个赋值的性能。为了解决这个问题,您需要使用从返回动态值的函数中获得的值。 那个方法的问题在于,两个算术运算的性能差异将完全被生成数字的函数的性能所掩盖。
【解决方案2】:

对我来说这更准确

public static class Haversine {
  public static double calculate(double lat1, double lon1, double lat2, double lon2) {
    var R = 6372.8; // In kilometers
    var dLat = toRadians(lat2 - lat1);
    var dLon = toRadians(lon2 - lon1);
    lat1 = toRadians(lat1);
    lat2 = toRadians(lat2);

    var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + Math.Sin(dLon / 2) * Math.Sin(dLon / 2) * Math.Cos(lat1) * Math.Cos(lat2);
    var c = 2 * Math.Asin(Math.Sqrt(a));
    return R * c;
  }

  public static double toRadians(double angle) {
    return Math.PI * angle / 180.0;
  }
}

void Main() {
  Console.WriteLine(String.Format("The distance between coordinates {0},{1} and {2},{3} is: {4}", 36.12, -86.67, 33.94, -118.40, Haversine.calculate(36.12, -86.67, 33.94, -118.40)));
}

// Returns: The distance between coordinates 36.12,-86.67 and 33.94,-118.4 is: 2887.25995060711

【讨论】:

  • 我回来晚了,但是这种方法犯了一个严重的错误,即计算 Math.Sin(dLat / 2)Math.Sin(dLon / 2) 两次,而不是计算一次并缓存结果。算术很便宜,但三角函数很昂贵,因此在不需要时进行三角函数会使算法效率不高。
猜你喜欢
  • 2014-03-10
  • 2011-01-15
  • 2016-05-12
  • 1970-01-01
  • 2023-03-22
  • 2018-09-20
  • 1970-01-01
  • 1970-01-01
  • 2022-07-19
相关资源
最近更新 更多