与 Java 等相比,F# 性能较差。我做错了什么?

F# performance bad compared to e.g. Java. What am I doing wrong?

提问人:FlyingHubert 提问时间:11/11/2020 最后编辑:FlyingHubert 更新时间:11/12/2020 访问量:184

问:

我目前正在尝试使用 F# 进行编程。因此,我编写了一个简单的 F# 脚本,以一种非常愚蠢的方式计算 PI。我已经用许多编程语言编写了这个算法,但这次它的执行速度很慢,我不知道我做错了什么。 与我的 Java 版本相比,这里的速度大约慢了 10-20 倍(在我的机器上通过按 Alt+Enter 在 VS Code 中运行大约需要 10-15 瑞典克),这很多(在我看来)。

算法的想法是向一个 1 x 1 的正方形投掷飞镖,其中有一个圆圈。圆与正方形的边缘接触。如果飞镖击中圆圈,它就会被计算在内。投掷完所有飞镖后,您只需将飞镖命中数除以飞镖总数,然后乘以 4 即可获得 PI。

我尝试了很多方法来解决我的错误,但找不到它。

  • 我试图用固定值替换随机数生成 - >没有做太多事情
  • 我试图用常量值替换 Math.Sqrt -> 没有做太多事情
  • 我用 Arrays 替换了 Seq.xxx 调用,同时希望这能减少开销。->没有做太多

PS:我知道这种计算 Pi 的方法很糟糕。但这不是我在这里试图表达的重点。

open System

let random = Random(DateTime.Now.Millisecond)
let throwDart i = (random.NextDouble(), random.NextDouble())

let distance dart point = let (x1, y1), (x2, y2) = dart, point
                          (x2 - x1, y2 - y1)
let length distance = let (x, y) = distance
                      Math.Sqrt(x * x + y * y)

let isInside circle dartHit = 
        let (center, radius) = circle 
        distance center dartHit |> length  |> (fun x -> x < radius)

let circle = ((0.5, 0.5), 0.5)

let dartCount = 100000000

let dartSeqence = Seq.init dartCount throwDart
let pi = float(dartSeqence |> Seq.filter (isInside circle) |> Seq.length) / float(dartCount) * 4.0

也许有人知道我做错了什么。我希望 F# 能在这个任务中做得很好,因为它是一个非常简单和直接的算法。

提前感谢您的帮助。


更新1:

嗨,再次运行我的 Java 代码后,我有点失望,因为我认为它更快。这是在我的机器上运行大约 4.4 秒的代码:

import java.util.Random;

public class DartThrower{
    Random random;

    public DartThrower(){
        random = new Random();
    }
    public boolean throwDart(){
        double x = random.nextDouble();
        double y = random.nextDouble();
        // wenn Entfernung des Punktes vom Mittelpunkt (0.5|0.5) größer als 0.5 ist wird false ausgegeben
        boolean inTheCircle = Math.sqrt((0.5 - x) * (0.5 - x) + (0.5 - y) * (0.5 - y)) <= 0.5;  
        return inTheCircle;
    }
}

class PiCalculator{
    public static void main(String[] args){
        long start = System.currentTimeMillis();
        double hitCount = 0.0;
        double throwCount = 100000000L;
        DartThrower thrower = new DartThrower();

        for (long i = 0; i < throwCount; i++){
            boolean hit = thrower.throwDart();
            hitCount += hit ? 1 : 0;
        }
        double a = hitCount / throwCount;
        double pi = a * 4.0;
        System.out.println("Pi= " + pi);
        System.out.println("took " + (System.currentTimeMillis() - start) + "ms");
    }
}

对不起,但我认为时间不到 1 秒 - 这就是为什么我谈论的执行速度提高了 10-20 倍。 这是不正确的!在我重新确定自己在时间上的错误之后,我决定为两者添加一些时间戳并得到

  • Java:约4.3秒
  • F#:约13.0秒

因此,在这两者之间考虑 3-4 个因素。如果我尝试使用 @Tomas Petricek 的性能更新,这种差异可能已经过时了。我会尝试并告诉你结果。

感谢您到目前为止的帮助。

性能 F# 序列

评论

2赞 Reed Copsey 11/11/2020
你可能想要创建一个控制台项目,并尝试在发布版本中执行此操作。可能更像是直接比较。
0赞 FlyingHubert 11/11/2020
谢谢你的想法。我用控制台应用程序尝试过。 测试了“发布”和“调试”配置。两者都像 VS Code 变体一样慢:(
2赞 Fyodor Soikin 11/11/2020
你能展示一下你的 Java 实现吗?
3赞 Reed Copsey 11/11/2020
@FlyingHubert 我认为有些不对劲 - 我在调试中得到 15458 毫秒,在发布版本中得到 9469毫秒。这些应该有很大的不同。
0赞 FlyingHubert 11/11/2020
我目前正在工作,今天下午将提供 Java 实现。

答:

6赞 Tomas Petricek 11/11/2020 #1

通过用一个简单的可变变量和循环替换惰性序列,可以使速度提高大约 2 倍(这在性能关键的 F# 代码中没有什么不好的):for

let mutable count = 0
for i in 0 .. dartCount do
  if isInside circle (throwDart i) then count <- count + 1
let pi = float count / float dartCount * 4.0

您可以通过制作函数来增加更多的性能(在我的粗略测试中约为 30%),这可能会消除一些值作为元组的盒子:inline

let inline distance dart point = 
  let (x1, y1), (x2, y2) = dart, point
  (x2 - x1, y2 - y1)

let inline length distance = 
  let (x, y) = distance
  Math.Sqrt(x * x + y * y)

let inline isInside circle dartHit = 
  let (center, radius) = circle 
  length (distance center dartHit) < radius

有了这个,我没有看到任何明显的问题(但我可能遗漏了一些东西)。最好查看您的 Java 代码进行比较,因为可能会有一些微妙的逻辑更改会影响性能。

评论

1赞 FlyingHubert 11/12/2020
谢谢你的提示。我现在的性能比使用 Java 变体更好。我刚刚测量了大约 1.9 秒,您的 F# 代码在我的解决方案中作为发布版本运行。但是你知道为什么在这个例子中序列这么慢吗?对于我这个 F# 初学者来说,这个序列似乎应该有很好的性能,我认为我不必使用命令式代码来获得相同的结果。也许你知道为什么可以告诉我。顺便说一句,非常感谢。
1赞 s952163 11/12/2020
@FlyingHubert是 IEnumerable 接口,它公开了一个迭代器,但这会使它变慢一些,并且您将以 CPU 性能来交换内存效率(因为它是懒惰的)。Seq/IEnumerable 有一个令人讨厌的习惯,即在链接时也会被枚举几次。如果有疑问,请使用数组来提高性能。我不确定为什么应该有很好的表现。取决于实现。命令式代码往往更接近 PC 架构,速度更快,声明式代码抽象得更多,但以牺牲性能为代价。Seqseq