Плохая производительность F # по сравнению, например, с Java. Что я делаю не так?

#performance #f# #sequence

#Производительность #f# #последовательность

Вопрос:

В настоящее время я пытаюсь заняться программированием с помощью F #. Поэтому я написал простой скрипт F # для вычисления PI очень глупым способом. Я написал этот алгоритм на многих языках программирования, но на этот раз он выполняется довольно медленно, и я не знаю, что я делаю не так. По сравнению с моей версией Java это примерно в 10-20 раз медленнее (примерно 10-15 сек на моей машине, работающей в VS Code, нажав Alt Enter), что много (на мой взгляд).

Идея алгоритма состоит в том, чтобы бросать дротики в квадрат размером 1 на 1 с кругом в нем. Круг касается краев квадрата. Если дротик попадает в круг, он засчитывается. После того, как все дротики брошены, вы просто делите количество попаданий дротиков на общее количество дротиков и умножаете на 4, чтобы получить число ПИ.

Я перепробовал много способов получить мою ошибку, но не смог ее найти.

  • Я попытался заменить генерацию случайных чисел фиксированными значениями -> не сделал многого
  • Я попытался заменить математику.Sqrt с постоянным значением -> мало что сделал
  • Я заменил вызовы 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. Я попробую это и расскажу вам о результатах.

Пока спасибо за вашу помощь.

Комментарии:

1. Возможно, вы захотите создать консольный проект и попробовать сделать это в сборке выпуска. Вероятно, это будет скорее прямое сравнение.

2. Спасибо за идею. Я попробовал это с консольным приложением. Протестирована конфигурация «Release» и «Debug». Оба варианта такие же медленные, как и вариант VS Code: (

3. Можете ли вы показать свою реализацию Java?

4. @FlyingHubert Я думаю, что что-то не так — я получаю 15458 мс при отладке и 9469мс при сборке релиза. Они должны существенно отличаться.

5. В настоящее время я работаю и сегодня днем представлю реализацию Java.

Ответ №1:

Вы можете сделать это примерно в 2 раза быстрее, заменив отложенную последовательность простой изменяемой переменной и for циклом (в этом нет ничего плохого в коде F #, критичном к производительности):

 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. Спасибо за советы. Сейчас моя производительность лучше, чем с вариантом Java. Я только что измерил приблизительно 1,9 с для вашего кода F #, работающего как сборка релиза в моем решении. Но у вас есть идея, почему последовательности в этом примере такие медленные? Мне, как новичку в F #, кажется, что эта последовательность должна иметь отличную производительность, и я подумал, что мне не нужно использовать императивный код, чтобы получить тот же результат. Может быть, у вас есть идея, почему an может мне сказать. Большое спасибо, кстати.

2.@FlyingHubert Seq — это интерфейс IEnumerable, который предоставляет итератор, но это сделает его немного медленнее, и вы будете торговать эффективностью памяти (потому что она ленива) с производительностью процессора. Seq / IEnumerable имеют неприятную привычку перечисляться несколько раз при цепочке. Если вы сомневаетесь, используйте массив для повышения производительности. Я не уверен, почему seq должна быть отличная производительность. Зависит от реализации. Императивный код, как правило, ближе к архитектуре ПК и быстрее, декларативный код абстрагируется больше, но за счет производительности.