Почему F # настолько медленнее, чем С#? (контрольная цифра числа)
Я думал, что F # должен быть быстрее, чем С#, я сделал, вероятно, плохой тест, и С# получил 16239мс, в то время как F # сделал еще хуже на 49583ms. Может ли кто-нибудь объяснить, почему это так? Я рассматриваю возможность оставить F # и вернуться на С#. Можно ли получить тот же результат в F # с более быстрым кодом?
Вот код, который я использовал, я сделал его равным, как я мог.
F # (49583 м)
open System
open System.Diagnostics
let stopwatch = new Stopwatch()
stopwatch.Start()
let mutable isPrime = true
for i in 2 .. 100000 do
for j in 2 .. i do
if i <> j && i % j = 0 then
isPrime <- false
if isPrime then
printfn "%i" i
isPrime <- true
stopwatch.Stop()
printfn "Elapsed time: %ims" stopwatch.ElapsedMilliseconds
Console.ReadKey() |> ignore
С# (16239 мс)
using System;
using System.Diagnostics;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
bool isPrime = true;
for (int i = 2; i <= 100000; i++)
{
for (int j = 2; j <= i; j++)
{
if (i != j && i % j == 0)
{
isPrime = false;
break;
}
}
if (isPrime)
{
Console.WriteLine(i);
}
isPrime = true;
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: " + stopwatch.ElapsedMilliseconds + "ms");
Console.ReadKey();
}
}
}
Ответы
Ответ 1
Программа F # медленнее, потому что ваши программы не эквивалентны. Ваш код С# имеет оператор break
во внутреннем цикле for
, но ваша программа F # этого не делает. Таким образом, для каждого четного числа код С# останавливается после проверки делимости на 2, а программа F # будет проверять каждое число от 2 до i
. С такой большой разницей в работе, на самом деле удивительно, что код F # всего в три раза медленнее!
Теперь F # намеренно не имеет оператора break
, поэтому вы не можете полностью перевести код С# непосредственно на F #. Но вы можете использовать функции, которые включают логику короткого замыкания. Например, в комментариях Аарон М. Эшбах предложил следующее:
{2 .. 100000}
|> Seq.filter (fun i -> {2 .. i-1} |> Seq.forall (fun j -> i % j <> 0))
|> Seq.iter (printfn "%i")
Это использует Seq.forall
, который делает короткое замыкание: он проверяет каждый вход в последовательности на условие, и если условие когда-либо возвращает false
, оно останавливается и больше не проверяет. (Потому что функции в модуле Seq
ленивы и не будут делать больше работы, чем это абсолютно необходимо для получения их вывода). Это похоже на break
в коде С#.
Я пройду это шаг за шагом, чтобы вы могли увидеть, как это работает:
{2 .. 100000}
Это создает ленивую последовательность int, которая начинается с 2 и доходит до (и включая) 100000.
|> Seq.filter (fun i -> (some expression involving i))
Я разбил следующую строку на две части: внешнюю часть Seq.filter
и внутреннее выражение, включающее i
. Часть Seq.filter
принимает последовательность и фильтрует ее: для каждого элемента в последовательности вызовите его i
и оцените выражение. Если это выражение оценивается как true
, сохраните элемент и передайте его на следующий шаг в цепочке. Если выражение false
, то выбросьте этот элемент.
Теперь выражение с участием i
:
{2 .. i-1} |> Seq.forall (fun j -> i % j <> 0)
Сначала создается ленивая последовательность, начинающаяся с 2 и возрастающая до i
минус одна включительно. (Или вы могли бы подумать об этом как о старте с 2 и подойти к i
, но не включая i
). Затем он проверяет, выполняет ли каждый элемент этой последовательности определенное условие (что функция Seq.forall
). И, как деталь реализации Seq.forall
, потому что он ленив и не работает больше, чем это абсолютно необходимо, в тот момент, когда он найдет false
результат, он остановится и больше не пройдет через входную последовательность. (Поскольку, как только вы найдете один встречный пример, функция forall
больше не может возвращать true, поэтому она останавливается, как только ее результат известен.) И каково выражение, которое проверяется в Seq.forall
? Это fun j → я % j <> 0
. Таким образом, j
- это внутренняя переменная цикла, i
- внешняя переменная (назначенная в части Seq.filter
), а логика такая же, как и ваши циклы С#.
Теперь помните, что мы находимся внутри Seq.filter
. Поэтому, если Seq.forall
возвращает true, тогда Seq.filter
будет сохранять значение i
. Но если Seq.forall
возвращает false, то Seq.filter
откажется от этого значения i
от перехода к следующему шагу.
Наконец, у нас есть эта строка как следующий (и окончательный) шаг:
|> Seq.iter (printfn "%i")
То, что это делает, в точности то же самое, что:
for number in inputSoFar do
printfn "%i" number
(printfn "%i")
может выглядеть необычно для вас, если вы новичок в F #. Это currying, и это очень полезная концепция и одна, с которой важно привыкнуть. Поэтому немного подумайте об этом: в F # следующие две строки кода полностью эквивалентны:
(fun y -> someFunctionCall x y)
(someFunctionCall x)
Так что fun item → printfn "%i" item
всегда можно заменить printfn "%i
. И Seq.iter
является эквивалентом цикла for
:
inputSoFar |> Seq.iter (someFunctionCall x)
точно эквивалентен:
for item in inputSoFar do
someFunctionCall x item
Итак, у вас есть это: почему ваша программа F # работает медленнее, а также как написать программу F #, которая будет следовать той же логике, что и С#, но будет иметь эквивалент оператора break
в нем.
Ответ 2
Я знаю, что есть уже принятый ответ, но просто хотел добавить это.
Заработал много С# на протяжении многих лет, но не много F #. Следующее более похоже на код С#.
open System
open System.Diagnostics
let stopwatch = new Stopwatch()
stopwatch.Start()
let mutable loop = true
for i in 2 .. 100000 do
let mutable j = 2
while loop do
if i <> j && i % j = 0 then
loop <- false
else
j <- j + 1
if j >= i then
printfn "%i" i
loop <- false
loop <- true
stopwatch.Stop()
printfn "Elapsed time: %ims" stopwatch.ElapsedMilliseconds
И в моих тестах на LinqPad вышеупомянутое быстрее, чем решение, предложенное Аароном Эшбахом.
Он также выходит с удивительно похожим IL.
Ответ 3
Как уже упоминалось, код не делает то же самое, и вам нужно использовать методы для обеспечения того, чтобы внутренний цикл был остановлен после нахождения штриха.
Кроме того, вы печатаете значения до стандартного. Обычно это нежелательно, когда вы выполняете тесты производительности ЦП, поскольку значительное количество времени может быть изменением ввода/вывода результатов тестов.
Во всяком случае, несмотря на то, что есть принятый ответ, я решил немного поработать с этим, чтобы сравнить различные предлагаемые решения с некоторыми из моих собственных.
Выполнение производительности выполняется в режиме x64
на.NET 4.7.1.
Я сравнил различные предлагаемые решения F # плюс некоторые из моих собственных вариантов:
Running 'Original(F#)' with 100000 (10512)...
... it took 14533 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Original(C#)' with 100000 (10512)...
... it took 1343 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Aaron' with 100000 (10512)...
... it took 5027 ms with (3, 1, 0) cc and produces 9592 GOOD primes
Running 'SteveJ' with 100000 (10512)...
... it took 1640 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Dumetrulo1' with 100000 (10512)...
... it took 1908 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Dumetrulo2' with 100000 (10512)...
... it took 970 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Simple' with 100000 (10512)...
... it took 621 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'PushStream' with 100000 (10512)...
... it took 1627 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Unstalling' with 100000 (10512)...
... it took 551 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'Vectors' with 100000 (10512)...
... it took 1076 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'VectorsUnstalling' with 100000 (10512)...
... it took 1072 ms with (0, 0, 0) cc and produces 9592 GOOD primes
Running 'BestAttempt' with 100000 (10512)...
... it took 4 ms with (0, 0, 0) cc and produces 9592 GOOD primes
-
Original(F#)
- Исходный код F # от OP изменился, чтобы не использовать stdout -
Original(C#)
- Исходный код С# от OP изменен, чтобы не использовать stdout -
Aaron
. Идиоматический подход с использованием Seq
. Как и ожидалось, Seq
и производительность обычно не сочетаются друг с другом. -
SteveJ
- @SteveJ попытался имитировать код С# в F # -
Dumetrulo1
- @dumetrulo реализовал алгоритм в хвостовой рекурсии -
Dumetrulo2
- @dumetrulo улучшил алгоритм, выполнив +2 вместо +1 (не нужно проверять четные числа). -
Simple
Моя попытка использовать подобный подход к Dumetrulo2
с рекурсией хвоста. -
PushStream
- моя попытка использования упрощенного потокового потока (Seq
- поток pull) -
Unstalling
- Моя попытка попытаться удалить CPU в случае, если используемые инструкции имеют задержку -
Vectors
- моя попытка использовать System.Numerics.Vectors
для выполнения нескольких делений на операцию (aka SIMD). К сожалению, векторная библиотека не поддерживает mod
поэтому мне пришлось подражать ей. -
VectorsUnstalling
- Моя попытка улучшить Vectors
, пытаясь удалить CPU. -
BestAttempt
- как Simple
но проверяет номера до sqrt n
при определении, является ли это простым.
Завершение
- Петли F # не
continue
не break
. Хвост-рекурсия в F # - это ИМО лучший способ реализовать циклы, которые необходимо break
. - При сравнении производительности языков следует сравнить наилучшую производительность или сравнить производительность идиоматических решений? Я лично считаю, что наилучшая производительность - это правильный путь, но я знаю, что люди не согласны со мной (я написал версию mandelbrot для тестирования игры для F # с сопоставимой производительностью с C, но она не была принята, потому что стиль рассматривался как не- -дидиоматический для F #).
-
Seq
in F # к сожалению добавляет значительные накладные расходы. Мне трудно заставить себя использовать его, даже когда накладные расходы не актуальны. - Современные инструкции для процессоров имеют разные номера для пропускной способности и латентности. Это означает, что иногда, чтобы ускорить работу, необходимо обработать несколько независимых выборок во внутреннем цикле, чтобы позволить модулю выполнения не по порядку изменять программу, чтобы скрыть задержку. Если ваш процессор имеет гиперпоточность и вы запускаете алгоритм по нескольким потокам, то гиперпоточность может смягчить задержку "автоматически".
- Отсутствие
mod
векторов предотвратило попытку использования SIMD для достижения какой-либо производительности по сравнению с решением без SIMD. - Если я
Unstalling
попытку циклы Unstalling
же раз, сколько и код С#, конечный результат равен 1100 ms
в F # по сравнению с 1343 ms
в С#. Таким образом, F # можно сделать очень похожим на С#. Если применить еще несколько трюков, это займет всего 4 ms
но для С# это будет одинаково. Во всяком случае, довольно прилично идти от почти 15 sec
до 4 ms
.
Надеюсь, кому-то это было интересно
Полный исходный код:
module Common =
open System
open System.Diagnostics
let now =
let sw = Stopwatch ()
sw.Start ()
fun () -> sw.ElapsedMilliseconds
let time i a =
let inline cc i = GC.CollectionCount i
let ii = i ()
GC.Collect (2, GCCollectionMode.Forced, true)
let bcc0, bcc1, bcc2 = cc 0, cc 1, cc 2
let b = now ()
let v = a ii
let e = now ()
let ecc0, ecc1, ecc2 = cc 0, cc 1, cc 2
v, (e - b), ecc0 - bcc0, ecc1 - bcc1, ecc2 - bcc2
let limit = 100000
// pi(x) ~= limit/(ln limit - 1)
// Using pi(x) ~= limit/(ln limit - 2) to over-estimate
let estimate = float limit / (log (float limit) - 1.0 - 1.0) |> round |> int
module Original =
let primes limit =
let ra = ResizeArray Common.estimate
let mutable isPrime = true
for i in 2 .. limit do
for j in 2 .. i do
if i <> j && i % j = 0 then
isPrime <- false
if isPrime then
ra.Add i
isPrime <- true
ra.ToArray ()
module SolutionAaron =
let primes limit =
{2 .. limit}
|> Seq.filter (fun i -> {2 .. i-1} |> Seq.forall (fun j -> i % j <> 0))
|> Seq.toArray
module SolutionSteveJ =
let primes limit =
let ra = ResizeArray Common.estimate
let mutable loop = true
for i in 2 .. limit do
let mutable j = 2
while loop do
if i <> j && i % j = 0 then
loop <- false
else
j <- j + 1
if j >= i then
ra.Add i
loop <- false
loop <- true
ra.ToArray ()
module SolutionDumetrulo1 =
let rec isPrimeLoop (ra : ResizeArray<_>) i j limit =
if i > limit then ra.ToArray ()
elif j > i then
ra.Add i
isPrimeLoop ra (i + 1) 2 limit
elif i <> j && i % j = 0 then
isPrimeLoop ra (i + 1) 2 limit
else
isPrimeLoop ra i (j + 1) limit
let primes limit =
isPrimeLoop (ResizeArray Common.estimate) 2 2 limit
module SolutionDumetrulo2 =
let rec isPrimeLoop (ra : ResizeArray<_>) i j limit =
let incr x = if x = 2 then 3 else x + 2
if i > limit then ra.ToArray ()
elif j > i then
ra.Add i
isPrimeLoop ra (incr i) 2 limit
elif i <> j && i % j = 0 then
isPrimeLoop ra (incr i) 2 limit
else
isPrimeLoop ra i (incr j) limit
let primes limit =
isPrimeLoop (ResizeArray Common.estimate) 2 2 limit
module SolutionSimple =
let rec isPrime i j k =
if i < k then
(j % i) <> 0 && isPrime (i + 2) j k
else
true
let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
if i < limit then
if isPrime 3 i i then
ra.Add i
isPrimeLoop ra (i + 2) limit
else
ra.ToArray ()
let primes limit =
let ra = ResizeArray Common.estimate
ra.Add 2
isPrimeLoop ra 3 limit
module SolutionPushStream =
type Receiver<'T> = 'T -> bool
type PushStream<'T> = Receiver<'T> -> bool
module Details =
module Loops =
let rec range e r i =
if i <= e then
if r i then
range e r (i + 1)
else
false
else
true
open Details
let range s e : PushStream<int> =
fun r -> Loops.range e r s
let filter p (t : PushStream<'T>) : PushStream<'T> =
fun r -> t (fun v -> if p v then r v else true)
let forall p (t : PushStream<'T>) : bool =
t p
let toArray (t : PushStream<'T>) : _ [] =
let ra = ResizeArray 16
t (fun v -> ra.Add v; true) |> ignore
ra.ToArray ()
let primes limit =
range 2 limit
|> filter (fun i -> range 2 (i - 1) |> forall (fun j -> i % j <> 0))
|> toArray
module SolutionUnstalling =
let rec isPrime i j k =
if i + 6 < k then
(j % i) <> 0 && (j % (i + 2)) <> 0 && (j % (i + 4)) <> 0 && (j % (i + 6)) <> 0 && isPrime (i + 8) j k
else
true
let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
if i < limit then
if isPrime 3 i i then
ra.Add i
isPrimeLoop ra (i + 2) limit
else
ra.ToArray ()
let primes limit =
let ra = ResizeArray Common.estimate
ra.Add 2
ra.Add 3
ra.Add 5
ra.Add 7
ra.Add 11
ra.Add 13
ra.Add 17
ra.Add 19
ra.Add 23
isPrimeLoop ra 29 limit
module SolutionVectors =
open System.Numerics
assert (Vector<int>.Count = 4)
type I4 = Vector<int>
let inline (%%) (i : I4) (j : I4) : I4 =
i - (j * (i / j))
let init : int [] = Array.zeroCreate 4
let i4 v0 v1 v2 v3 =
init.[0] <- v0
init.[1] <- v1
init.[2] <- v2
init.[3] <- v3
I4 init
let i4_ (v0 : int) =
I4 v0
let zero = I4.Zero
let one = I4.One
let two = one + one
let eight = two*two*two
let step = i4 3 5 7 9
let rec isPrime (i : I4) (j : I4) k l =
if l + 6 < k then
Vector.EqualsAny (j %% i, zero) |> not && isPrime (i + eight) j k (l + 8)
else
true
let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
if i < limit then
if isPrime step (i4_ i) i 3 then
ra.Add i
isPrimeLoop ra (i + 2) limit
else
ra.ToArray ()
let primes limit =
let ra = ResizeArray Common.estimate
ra.Add 2
ra.Add 3
ra.Add 5
ra.Add 7
ra.Add 11
ra.Add 13
ra.Add 17
ra.Add 19
ra.Add 23
isPrimeLoop ra 29 limit
module SolutionVectorsUnstalling =
open System.Numerics
assert (Vector<int>.Count = 4)
type I4 = Vector<int>
let init : int [] = Array.zeroCreate 4
let i4 v0 v1 v2 v3 =
init.[0] <- v0
init.[1] <- v1
init.[2] <- v2
init.[3] <- v3
I4 init
let i4_ (v0 : int) =
I4 v0
let zero = I4.Zero
let one = I4.One
let two = one + one
let eight = two*two*two
let sixteen = two*eight
let step = i4 3 5 7 9
let rec isPrime (i : I4) (j : I4) k l =
if l + 14 < k then
// i - (j * (i / j))
let i0 = i
let i8 = i + eight
let d0 = j / i0
let d8 = j / i8
let n0 = i0 * d0
let n8 = i8 * d8
let r0 = j - n0
let r8 = j - n8
Vector.EqualsAny (r0, zero) |> not && Vector.EqualsAny (r8, zero) |> not && isPrime (i + sixteen) j k (l + 16)
else
true
let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
if i < limit then
if isPrime step (i4_ i) i 3 then
ra.Add i
isPrimeLoop ra (i + 2) limit
else
ra.ToArray ()
let primes limit =
let ra = ResizeArray Common.estimate
ra.Add 2
ra.Add 3
ra.Add 5
ra.Add 7
ra.Add 11
ra.Add 13
ra.Add 17
ra.Add 19
ra.Add 23
isPrimeLoop ra 29 limit
module SolutionBestAttempt =
let rec isPrime i j k =
if i < k then
(j % i) <> 0 && isPrime (i + 2) j k
else
true
let inline isqrt i = (i |> float |> sqrt) + 1. |> int
let rec isPrimeLoop (ra : ResizeArray<_>) i limit =
if i < limit then
if isPrime 3 i (isqrt i) then
ra.Add i
isPrimeLoop ra (i + 2) limit
else
ra.ToArray ()
let primes limit =
let ra = ResizeArray Common.estimate
ra.Add 2
isPrimeLoop ra 3 limit
[<EntryPoint>]
let main argv =
let testCases =
[|
"Original" , Original.primes
"Aaron" , SolutionAaron.primes
"SteveJ" , SolutionSteveJ.primes
"Dumetrulo1" , SolutionDumetrulo1.primes
"Dumetrulo2" , SolutionDumetrulo2.primes
"Simple" , SolutionSimple.primes
"PushStream" , SolutionPushStream.primes
"Unstalling" , SolutionUnstalling.primes
"Vectors" , SolutionVectors.primes
"VectorsUnstalling" , SolutionVectors.primes
"BestAttempt" , SolutionBestAttempt.primes
|]
do
// Warm-up
printfn "Warm up"
for _, a in testCases do
for i = 0 to 100 do
a 100 |> ignore
do
let init () = Common.limit
let expected = SolutionSimple.primes Common.limit
for testCase, a in testCases do
printfn "Running '%s' with %d (%d)..." testCase Common.limit Common.estimate
let actual, time, cc0, cc1, cc2 = Common.time init a
let result = if expected = actual then "GOOD" else "BAD"
printfn " ... it took %d ms with (%d, %d, %d) cc and produces %d %s primes" time cc0 cc1 cc2 actual.Length result
0
Ответ 4
Если вы хотите, чтобы итеративная функция F # полностью эквивалентна циклам for в С#, вы можете использовать следующую функцию хвостового рекурсивно:
let rec isPrimeLoop i j limit =
if i > limit then ()
elif j > i then
stdout.WriteLine (string i)
isPrimeLoop (i + 1) 2 limit
elif i <> j && i % j = 0 then
isPrimeLoop (i + 1) 2 limit
else
isPrimeLoop i (j + 1) limit
Как вы можете видеть, из-за того, как он себя называет, флаг isPrime
больше не нужен. Вместо вложенных циклов вызовите его следующим образом:
let sw = System.Diagnostics.Stopwatch.StartNew ()
isPrimeLoop 2 2 100000
sw.Stop ()
printfn "Elapsed time: %ims" sw.ElapsedMilliseconds
PS: Вы можете значительно сократить время, проверив только нечетные числа после 2:
let rec isPrimeLoop i j limit =
let incr x = if x = 2 then 3 else x + 2
if i > limit then ()
elif j > i then
stdout.WriteLine (string i)
isPrimeLoop (incr i) 2 limit
elif i <> j && i % j = 0 then
isPrimeLoop (incr i) 2 limit
else
isPrimeLoop i (incr j) limit