Производительность F #: Что делает этот код таким медленным?

Этот код F # является попыткой решить Проект Эйлера, задача № 58:

let inc = function
| n -> n + 1
let is_prime = function
| 2 -> true
| n when n < 2 || n%2=0-> false
| n ->
[3..2..(int (sqrt (float n)))]
|> List.tryFind (fun i -> n%i=0)
|> Option.isNone
let spir = Seq.initInfinite (fun i ->
let n = i%4
let a = 2 * (i/4 + 1)
(a*n) + a + (a-1)*(a-1))
let rec accum se p n =
match se with
| x when p*10 < n && p <> 0 -> 2*(n/4) + 1
| x when is_prime (Seq.head x) -> accum (Seq.tail x) (inc p) (inc n)
| x -> accum (Seq.tail x) p (inc n)
| _ -> 0
printfn "%d" (accum spir 0 1)

Я не знаю время работы этой программы, потому что я отказался ждать ее завершения. Вместо этого я написал этот код обязательно на C ++:

#include "stdafx.h"#include "math.h"#include <iostream>

using namespace std;

int is_prime(int n)
{
if (n % 2 == 0) return 0;
for (int i = 3; i <= sqrt(n); i+=2)
{
if (n%i == 0)
{
return 0;
}
}
return 1;
}

int spir(int i)
{
int n = i % 4;
int a = 2 * (i / 4 + 1);
return (a*n) + a + ((a - 1)*(a - 1));
}

int main()
{
int n = 1, p = 0, i = 0;
cout << "start" << endl;
while (p*10 >= n || p == 0)
{
p += is_prime(spir(i));
n++; i++;
}
cout << 2*(i/4) + 1;

return 0;
}

Приведенный выше код выполняется менее чем за 2 секунды и получает правильный ответ.

Что заставляет код F # работать так медленно? Даже после использования некоторых из инструментов профилирования, упомянутых в старый пост Stackoverflow, Я до сих пор не могу понять, какие дорогостоящие операции происходят.

С постом rmunn я смог придумать другую реализацию, которая получит ответ чуть менее чем за 30 секунд:

let inc = function
| n -> n + 1
let is_prime = function
| 2 -> true
| n when n < 2 || n%2=0-> false
| n ->
[3..2..(int (sqrt (float n)))]
|> List.tryFind (fun i -> n%i=0)
|> Option.isNone
let spir2 =
List.unfold (fun state ->
let p = fst state
let i = snd state
let n = i%4
let a = 2 * (i/4 + 1)
let diag = (a*n) + a + (a-1)*(a-1)
if p*10 < (i+1) && p <> 0 then
printfn "%d" (2*((i+1)/4) + 1)
None
elif is_prime diag then
Some(diag, (inc p, inc i))
else Some(diag, (p, inc i))) (0, 0)

С информативным постом FuleSnabel его is_prime Функция заставляет вышеуказанный код выполняться менее чем за одну десятую секунды, что делает его быстрее, чем код C ++:

let inc = function
| n -> n + 1
let is_prime = function
| 1                 -> false
| 2                 -> true
| v when v % 2 = 0  -> false
| v ->
let stop = v |> float |> sqrt |> int
let rec loop vv =
if vv <= stop then
if (v % vv) <> 0 then
loop (vv + 2)
else
false
else
true
loop 3
let spir2 =
List.unfold (fun state ->
let p = fst state
let i = snd state
let n = i%4
let a = 2 * (i/4 + 1)
let diag = (a*n) + a + (a-1)*(a-1)
if p*10 < (i+1) && p <> 0 then
printfn "%d" (2*((i+1)/4) + 1)
None
elif i <> 3 && is_prime diag then
Some(diag, (inc p, inc i))
else Some(diag, (p, inc i))) (0, 0)

12

Решение

Здесь нет Seq.tail функция в основной библиотеке F # (ОБНОВЛЕНИЕ: Да, смотрите комментарии), так что я предполагаю, что вы используете Seq.tail функция от FSharpx.Collections. Если вы используете другую реализацию Seq.tail, это, вероятно, похоже — и это почти наверняка причина ваших проблем, потому что это не O (1), как вы думаете. Получение хвоста Список равно O (1) из-за того, как List реализован (как последовательность cons-ячеек). Но получение хвоста Seq в конечном итоге создает новый Seq из оригинального перечислимого, отбрасывая один предмет из него и возвращая остальные его предметы. Когда вы проходите через accum цикл во второй раз, вы звоните Seq.tail на этом «пропустить 1, а затем вернуться» seq. Итак, теперь у вас есть Seq который я назову S2, который запрашивает у S1 IEnumerable, пропускает первый элемент S1 и возвращает остальное. S1, когда запрашивается его первый элемент, запрашивает S0 (исходный Seq) для перечислимого, пропускает свой первый элемент, а затем возвращает остаток. Таким образом, чтобы S2 пропустил два элемента, он должен был создать два seqs. Теперь, когда вы просите Seq.tail из S2 вы создаете S3, который запрашивает у S2 IEnumerable, который запрашивает у S1 IEnumerable, который запрашивает у S0 IEnumerable … и так далее. Это эффективно O (N ^ 2), когда вы думал Вы писали операцию O (N).

Боюсь, у меня сейчас нет времени, чтобы найти решение для вас; с помощью List.tail не поможет, так как вам нужна бесконечная последовательность. Но, возможно, просто зная о Seq.tail Гоча достаточно, чтобы вы начали, поэтому я отправлю этот ответ сейчас, даже если он не завершен.

Если вам нужна дополнительная помощь, прокомментируйте этот ответ, и я вернусь к нему, когда у меня будет время — но это может произойти не в течение нескольких дней, поэтому, надеюсь, другие также ответят на ваш вопрос.

12

Другие решения

Написание перформанта F # очень возможно, но требует некоторого знания шаблонов, которые имеют высокую относительную стоимость процессора в тесном цикле. Я рекомендую использовать такие инструменты, как ILSpy, чтобы найти скрытые накладные расходы.

Например, можно представить, что F # расширяет это выражение в эффективный цикл for:

[3..2..(int (sqrt (float n)))]
|> List.tryFind (fun i -> n%i=0)
|> Option.isNone

Однако в настоящее время это не так. Вместо этого он создает List который охватывает диапазон с помощью внутренних операторов и передает его List.tryFind, Это дорого по сравнению с реальной работой, которую мы любим делать (операция с модулем). ILSpy декомпилирует приведенный выше код в нечто вроде этого:

public static bool is_prime(int _arg1)
{
switch (_arg1)
{
case 2:
return true;
default:
return _arg1 >= 2 && _arg1 % 2 != 0 && ListModule.TryFind<int>(new Program.Original.is_prime@10(_arg1), SeqModule.ToList<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(3, 2, (int)Math.Sqrt((double)_arg1))))) == null;
}
}

Эти операторы не так эффективны, как могли бы быть (AFAIK, в настоящее время это улучшается), но независимо от того, насколько эффективно выделение List а затем искать его не будет бить цикл.

Это означает, что is_prime не так эффективно, как могло бы быть. Вместо этого можно сделать что-то вроде этого:

let is_prime = function
| 1                 -> false
| 2                 -> true
| v when v % 2 = 0  -> false
| v ->
let stop = v |> float |> sqrt |> int
let rec loop vv =
if vv <= stop then
(v % vv) <> 0 && loop (vv + 2)
else
true
loop 3

Эта версия is_prime полагается на оптимизацию хвостового вызова в F # для расширения цикла в эффективный цикл for (вы можете увидеть это с помощью ILSpy). ILSpy декомпилирует цикл примерно так:

while (vv <= stop)
{
if (_arg1 % vv == 0)
{
return false;
}
int arg_13_0 = _arg1;
int arg_11_0 = stop;
vv += 2;
stop = arg_11_0;
_arg1 = arg_13_0;
}

Этот цикл не выделяет память и является довольно эффективным циклом. Можно увидеть некоторые бессмысленные задания, но, надеюсь, JIT: er их устранит. я уверен is_prime может быть улучшено еще дальше.

Когда используешь Seq в исполняемом коде нужно помнить, что он ленив и не использует памятки по умолчанию (см. Seq.cache). Поэтому можно легко снова и снова выполнять одну и ту же работу (см. Ответ @rmunn).

К тому же Seq не особенно эффективен из-за того, как IEnumerable/IEnumerator разработаны. Лучшими вариантами являются, например, Nessos Streams (доступно на nuget).

На случай, если вам интересно, я сделал быструю реализацию, основанную на простом Push Stream, который выглядит прилично производительным:

// Receiver<'T> is a callback that receives a value.
//  Returns true if it wants more values, false otherwise.
type Receiver<'T> = 'T -> bool
// Stream<'T> is function that accepts a Receiver<'T>
//  This means Stream<'T> is a push stream (as opposed to Seq that uses pull)
type Stream<'T>   = Receiver<'T> -> unit

// is_prime returns true if the input is prime, false otherwise
let is_prime = function
| 1                 -> false
| 2                 -> true
| v when v % 2 = 0  -> false
| v ->
let stop = v |> float |> sqrt |> int
let rec loop vv =
if vv <= stop then
(v % vv) <> 0 && loop (vv + 2)
else
true
loop 3

// tryFind looks for the first value in the input stream for f v = true.
//  If found tryFind returns Some v, None otherwise
let tryFind f (s : Stream<'T>) : 'T option =
let res = ref None
s (fun v -> if f v then res := Some v; false else true)
!res

// diagonals generates a tuple stream of all diagonal values
//  The first value is the side length, the second value is the diagonal value
let diagonals : Stream<int*int> =
fun r ->
let rec loop side v =
let step  = side - 1
if r (side, v + 1*step) && r (side, v + 2*step) && r (side, v + 3*step) && r (side, v + 4*step) then
loop (side + 2) (v + 4*step)
if r (1, 1) then loop 3 1

// ratio computes the streaming ratio for f v = true
let ratio f (s : Stream<'T>) : Stream<float*'T> =
fun r ->
let inc r = r := !r + 1.
let acc   = ref 0.
let count = ref 0.
s (fun v -> (inc count; if f v then inc acc); r (!acc/(!count), v))

let result =
diagonals
|> ratio (snd >> is_prime)
|> tryFind (fun (r, (_, v)) -> v > 1 && r < 0.1)
8

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector