Почему F# намного медленнее, чем C#? (простой номер benchmark)
Я думал, что F# должен быть быстрее, чем C#, я сделал, вероятно, плохой инструмент сравнения, и C# получил 16239ms, а F# сделал хуже на 49583ms. Может кто-нибудь объяснить почему? Я подумываю оставить F# и вернуться на C#. Можно ли получить тот же результат в F# с более быстрым кодом?
вот код, который я использовал, я сделал его как можно более равным.
F# (49583ms)
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
C# (16239ms)
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();
}
}
}
4 ответов
программа F# медленнее, потому что ваши программы не эквивалентны. Ваш код C# имеет break
заявление во внутреннем for
цикл, но ваша программа F# этого не делает. Таким образом, для каждого четного числа код C# остановится после проверки делимости на 2, а программа F# проверит каждое число от 2 до i
. С такой большой разницей в работу, это на самом деле удивительно, что F# код только в три раза медленнее!
Теперь, F# намеренно не было!--9--> оператор, поэтому вы не можете перевести код C# непосредственно на 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
в коде C#.
я пройду через это шаг за шагом, чтобы вы могли видеть, как это работает:
{2 .. 100000}
это создает ленивую последовательность ints, которая начинается с 2 и доходит до (и включая) 100000.
|> Seq.filter (fun i -> (some expression involving i))
я разбил следующую строку на две части: внешний Seq.filter
часть и внутреннее выражение с участием i
. The Seq.filter
часть принимает последовательность и фильтрует ее: для каждого элемента в последовательности, назовите его i
и оцените выражение. Если выражение true
, затем сохраните элемент и передайте его на следующий шаг в цепочке. Если выражение false
, затем выбросьте этот предмет.
теперь, выражение, включающее i
- это:
{2 .. i-1} |> Seq.forall (fun j -> i % j <> 0)
это сначала создает ленивую последовательность, которая начинается с 2 и доходит до i
минус один, включительно. (Или вы можете думать об этом, начиная с 2 и поднимаясь до i
, но не i
). Затем он проверяет, является ли элемент этой последовательности выполняет определенное условие (это возвращает false, то Seq.filter
сбрасывает это значение i
от прохождения до следующего шага.
наконец, у нас есть эта строка в качестве следующего (и окончательного) шага:
|> Seq.iter (printfn "%i")
что это делает в значительной степени то же, что:
for number in inputSoFar do
printfn "%i" number
на (printfn "%i")
вызов может выглядеть необычно, если вы новичок в F#. Это карринг, и это очень полезная концепция, и она важна привыкнуть. Поэтому потратьте некоторое время на размышление об этом: в 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#, которая будет следовать той же логике, что и C#, но будет иметь эквивалент break
заявление в ней.
Я знаю, что уже принят ответ, но просто хотел добавить это.
сделал много C# за эти годы, но не много F#. Следующее Было бы более эквивалентно коду C#.
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.
как уже упоминалось, код не делает то же самое, и вам нужно использовать методы, чтобы гарантировать, что внутренний цикл остановлен после того, как найдено простое число.
кроме того, вы печатаете значения по стандарту. Это обычно нежелательно, когда вы делаете тесты производительности процессора, так как значительное количество времени может быть ввод-вывод искажает результаты тестов.
в любом случае, хотя есть принятый ответ, я решил немного поработать с этим чтобы увидеть сравнение различных предлагаемых решений с некоторыми из моих собственных.
запуск производительности находится в 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#)
- исходный код C# OP изменен, чтобы не использовать stdout -
Aaron
- идиоматический подход с использованиемSeq
. Как ожидалосьSeq
и производительность обычно не очень хорошо сочетается. -
SteveJ
- @SteveJ попытался имитировать код C# В F# -
Dumetrulo1
- @dumetrulo реализовал алгоритм в хвостовой рекурсии -
Dumetrulo2
- @dumetrulo улучшил алгоритм, сделав шаг +2 вместо +1 (не нужно проверять четные числа). -
Simple
- моя попытка использовать аналогичный подход кDumetrulo2
С хвостовой рекурсией. -
PushStream
- моя попытка чтобы использовать упрощенный поток push (Seq
поток тяги) -
Unstalling
- моя попытка попытаться unstall CPU в случае, если используемые инструкции имеют задержку -
Vectors
- моя попытка, используяSystem.Numerics.Vectors
для выполнения нескольких делений за операцию (он же SIMD). К сожалению, векторы libary не поддерживаютmod
поэтому мне пришлось подражать ему. -
VectorsUnstalling
- моя попытка улучшитьVectors
путем попытки unstall CPU. -
BestAttempt
- какSimple
но только проверяет номера доsqrt n
при определении премьер.
подводя итоги
- петли F# не имеют
continue
, ниbreak
. Хвост-рекурсия в F# - это лучший способ реализации циклов, которые должныbreak
. - при сравнении производительности языков следует сравнивать наилучшую производительность или сравнивать производительность идиоматических решений? Я лично считаю, что наилучшая производительность это правильный путь, но я знаю, что люди не согласны со мной (я написал версия Мандельброта для бенчмарка игры для F# с сопоставимой производительностью с C, но он не был принят, потому что стиль рассматривался как неидиоматический для F#).
-
Seq
в F#, К сожалению, добавляет значительные накладные расходы. Мне трудно заставить себя использовать его, даже когда накладные расходы не актуальны. - современные инструкции CPUs имеют различные номера для пропускной способности и задержка. Это означает, что иногда для ускорения производительности необходимо обработать несколько независимых выборок во внутреннем цикле, чтобы позволить блоку выполнения вне заказа переупорядочить программу, чтобы скрыть задержку. Если ваш процессор имеет hyper threading, и вы запускаете алгоритм на нескольких потоках, hyper threading может уменьшить задержку "автоматически".
- отсутствие
mod
векторов предотвратили попытку использовать SIMD для получения любой производительности над не SIMD решение. - если я изменю
Unstalling
попытка петли такое же количество раз, как код C# конечный результат1100 ms
в F# по сравнению с1343 ms
в C#. Таким образом, F# можно сделать очень похожим на C#. Если применить еще несколько трюков, это займет всего4 ms
но это было бы то же самое и для C#. Во всяком случае, довольно прилично идти от почти15 sec
to4 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
если вы хотите итеративную функцию F#, полностью эквивалентную циклам for В C#, вы можете использовать следующую хвостовую рекурсивную функцию:
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
флаг больше не нужен. Вместо вложенных циклов for вызовите его следующим образом:
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