LINUX.ORG.RU

ФВП и производительность

 , , ,


2

7

Провёл тест на скорость работы одной и той же функции, написанной нормально и через ФВП. Получил потерю производительности в полтора раза:

SBCL:

(declaim (optimize (speed 3) (safety 0)))

(asdf:oos 'asdf:load-op :alexandria)

(defun f (x)
  (+ (+ (+ x 1) 2) 1))

(let ((f-comp
          (alexandria:compose
            (lambda (x) (+ x 1))
            (lambda (x) (+ x 2))
            (lambda (x) (+ x 1)))))
  (defun f-comp (x)
     (funcall f-comp x)))

(defconstant iter 10000000)

(time
 (dotimes (i iter)
   (f i)))

(time
 (dotimes (i iter)
   (f-comp i)))


Evaluation took:
  0.181 seconds of real time
  0.180000 seconds of total run time (0.180000 user, 0.000000 system)
  99.45% CPU
  325,363,554 processor cycles
  0 bytes consed

Evaluation took:
  0.296 seconds of real time
  0.296000 seconds of total run time (0.296000 user, 0.000000 system)
  100.00% CPU
  530,610,480 processor cycles
  0 bytes consed

Racket:

#lang racket
(define (f x)
  (+ (+ (+ x 1) 2) 1))

(define f-comp
  (compose
   (lambda (x) (+ x 1))
   (lambda (x) (+ x 2))
   (lambda (x) (+ x 1))))

(define iter 1000000)

(time 
 (for ([i iter])
   (f i)))

(time 
 (for ([i iter])
   (f-comp i)))

cpu time: 728 real time: 726 gc time: 0
cpu time: 1244 real time: 1244 gc time: 0

Получается, функции высшего порядка лучше не использовать? А как с этим борются в Haskell, если там без них почти ничего написать нельзя (точнее не принято рекомендуемым стилем программирования)?

★★★★★

Ответ на: комментарий от anonymous

В лиспах, в которых нет лексического связывания, есть ФВП, внезапно, например:).

man необходимо и достаточно.

Для наличия ФВП достаточно наличия замыканий. Не необходимо.

Это не значит, что если есть ФВП, то есть замыкания.

Следовательно, любой язык с замыканиями == ФП.

x3al ★★★★★
()
Ответ на: комментарий от anonymous

К текущей теме не имеет особого отношения, тем более определять ещё модель акторов тут в дополнение к самой ссылочной прозрачности — нафиг нужно.

x3al ★★★★★
()
Ответ на: комментарий от x3al

Если есть замыкания, то есть ФВП. Это не значит, что если есть ФВП, то есть замыкания.

ты что изначально утверждал, ты не забыл?

По моему определению, ФП-язык == язык, позволяющий функции высшего порядка. Следовательно, любой язык с замыканиями == ФП.

Ты съехать что ли решил, я не пойму?

anonymous
()
Ответ на: комментарий от x3al

closures are poor man's objects and vice versa

ну это банальщина тащем. были гораздо поинтереснее статьи разного плана в которых были попытки переформулировать ООП с точки зрения теории типов или были сравнения с выделением преимуществ/недостатков разных подходов.

фраза «подходы разные» правильная и настолько же бесполезная для обсуждения.

x4DA ★★★★★
()
Ответ на: комментарий от x4DA

ну это банальщина тащем.

Это с одной стороны банальщина, а с другой — заблудение, в некотором роде. замыкания сми по себе не могут полноценно реализовать объектную семантику, в общем случае. Они могли бы, если бы имели место быть first-class окружения, но это не общепринято. замыкания имеют как правило подковерные окружения, недоступные прямой для манипуляции извне, поэтому они не могут служить полноценной заменой объектной модели. Это миф.

anonymous
()
Ответ на: комментарий от x4DA

Ну да, а если бы мы имели first-class окружения, это была бы уже по сути, на все 100%, та же ООП модель, вид сбоку.

anonymous
()
Ответ на: комментарий от x4DA

плана в которых были попытки переформулировать ООП с точки зрения теории типов или были сравнения с выделением преимуществ/недостатков разных подходов.

А с точки зрения типов, принятых в ФП, ФП-модель однозначно слабей, поскольку в ООП мы имеем, опять же, первоклассные динамические типы.

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

anonymous
()
Ответ на: комментарий от anonymous

Следовательно, любой язык с замыканиями == ФП.

Любой язык с замыканиями имеет и функции высшего порядка. Следовательно, является в какой-то степени ФП. Какой именно — зависит от. Впрочем, классификация языков — дело не слишком практичное.

x3al ★★★★★
()
Ответ на: комментарий от x3al

Не все функциональные ЯП и не обязательно целиком обладают ссылочной прозрачностью. «Функциональность» — не бинарное понятие, а скорее градация. Чёткого определения «функциональности» ЯП в CS сейчас нет вообще.

Поддерживаю. И похоже, что не будет такого четкого определения никогда. А горячих голов везде полно, даже среди хаскелистов и лисперов. Впрочем, среди жавистов тоже.

dave ★★★★★
()

compose в racket не инлайнится, если определить в текущем модуле то он заинлайнится:

#lang racket
(define (f x)
  (+ (+ (+ x 1) 2) 1))

(define (comp f g)
  (λ (x) (f (g x))))

(define f-comp
  (comp
   (lambda (x) (+ x 1))
   (comp (lambda (x) (+ x 2))
         (lambda (x) (+ x 1)))))


(define iter 100000000)

(time 
 (for ([i (in-range iter)])
   (f i)))

(time 
 (for ([i (in-range iter)])
   (f-comp i)))
->
Добро пожаловать в DrRacket, версия 6.2.900.13--2015-09-04(053aae7/a) [3m].
Язык: racket [выбранный].
cpu time: 561 real time: 557 gc time: 0
cpu time: 577 real time: 577 gc time: 0
и вообще нету разницы.

ну и надо (in-range iter), ускоряет примерно в 10 раз.

anonymous
()
Ответ на: комментарий от dave

Конкретно в CS оно всегда было и будет. Его нет в науке под названием *лор-аналитика*.

В данной науке кроме этого, множество аномалий присутствует. К примеру, нормальный порядок вычислений тут == отсутствие порядка вычислений, отсутствие состояния объектов == отсутствие состояния вычислений(aka глобального состояния), декларативное == функциональное.

Чего только не придумаешь, чтобы выглядеть илитой.

anonymous
()
Ответ на: комментарий от anonymous

(define (comp f g)

(λ (x) (f (g x))))

Это неправильный compose. Правильный выглядит так:

(define (compose f . fs)  
  (cond 
    [(null? fs) f]
    [else 
     (define rest-f (apply compose fs))
     (lambda (x) (f (rest-f x)))]))

ну и надо (in-range iter)

Исправил. Теперь:

cpu time: 84 real time: 84 gc time: 0
cpu time: 500 real time: 496 gc time: 0

P.S. Хотя в С++ я тоже проверял с comp2(f,g)(x) = f(g(x)), compose(f,g,h) = f(comp2(g,h))(x)

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk
(define (compose f . fs)  
  (cond 
    [(null? fs) f]
    [else 
     (define rest-f (apply compose fs))
     (lambda (x) (f (rest-f x)))]))
[/quote]
Что то подумалось, на чистом ООП эти ваши компоузы выглядят в 1000 раз органичней, и понятней. Причем, явную передачу аргументов вообще можно опустить. А код длинней, но самую малость.
Compose := Object clone do(
 run := method(first call(second call(arg)))
)

Compose clone do(
  first := block(x, x + x)
  second := block(x, x * x)
  arg := 3
  run print // 18
)

Реально все эти фапе-стайлы ни о чем. Кроме обфускации и экономии на спичках они нихрена не дают.

anonymous
()
Ответ на: комментарий от anonymous

С такой версией действительно работает быстро

(define f-comp
  (comp
   (lambda (x) (+ x 1))
   (comp (lambda (x) (+ x 2))
         (lambda (x) (+ x 1)))))
monk ★★★★★
() автор топика
Ответ на: комментарий от zinfandel

Программы пишутся для работы на машине Неймана

Где ты, например, у Кнута увидел машину Неймана?
Иди, прочитай, как пишутся программы: https://people.freebsd.org/~lstewart/articles/cpumemory.pdf

Железяки пытаются притворяться этой машиной

Да ну?

Manhunt ★★★★★
()
Ответ на: комментарий от monk

Только если выделяется память (т.к. сборщик будет работать медленнее из-за висящего Dr.Racket'a в памяти). Если память не выделяется и затраты на гц = 0, то никакой статистически значимой разницы не будет.

anonymous
()
Ответ на: комментарий от Manhunt

Где ты, например, у Кнута увидел машину Неймана?

А ниче, что именно Нейман запилил комп? А что кнут сделал как инженер? Написал программульку для рисования закорючек? Работа уровня студента средней паршивости. И всю жизнь ведь над ней пыхтел, бедный. Поэтому и щеки надуты: искусство ко-ко-ко, структуры данных ко-ко-ко.

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

anonymous
()
Ответ на: комментарий от anonymous

А ниче, что именно Нейман запилил комп?

Ничё. Совсем ничё. Вообще ни грамма не роляет. Сентиментами своими подотрись, люди из кремниевой долины цинично делают бизнес.

Manhunt ★★★★★
()
Последнее исправление: Manhunt (всего исправлений: 2)
Ответ на: комментарий от ados

ну выиграл вот так ты 0.1 сек,

если вызов ф-ции происходит через каждую 1 с., получил 10% прироста производительности.

next_time ★★★★★
()
Ответ на: комментарий от monk

Это неправильный compose. Правильный выглядит так:

Ну так правильный конечно же медленный, ему надо на каждый вызов твой закомпоженной ф-и разбирать списки. Apply опять же тоже довольно медленный, т.к. со списками работает и вдобавок не инлайнится т.к. импортирован из #%kernel. Вообще вот так вот надо:

(define compose
  (case-lambda
    [() (λ (x) x)]
    [(f1) (λ (x) (f x))]
    [(f1 f2) (λ (x) (f1 (f2 x)))]
    [(f1 f2 f3) (λ (x) (f1 (f2 (f3 x))))]
    [(f . fs) (cond
               [(null? fs) f]
               [else
                (define rest-f (apply compose fs))
                (lambda (x) (f (rest-f x)))])]))

anonymous
()

В haskell f и f_comp работают одинаково:

-- https://www.linux.org.ru/forum/development/11943079
module Main (main) where

import Criterion.Main

f :: Num a => a -> a
f x = (((x + 1) + 2) + 1)

f_comp :: Num a => a -> a
f_comp = (+ 1) . (+ 2) . (+ 1)

main :: IO ()
main =
  defaultMain [
      bench "f (Int)"      $ whnf f (42 :: Int)
    , bench "f_comp (Int)" $ whnf f_comp (42 :: Int)
    ]
$ ghc --numeric-version
7.10.2
$ ghc --make -O2 how-things-inline.hs && ./how-things-inline 
benchmarking f (Int)
time                 5.979 ns   (5.973 ns .. 5.984 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 5.992 ns   (5.988 ns .. 5.999 ns)
std dev              16.30 ps   (10.94 ps .. 27.56 ps)

benchmarking f_comp (Int)
time                 5.823 ns   (5.802 ns .. 5.845 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 5.814 ns   (5.798 ns .. 5.834 ns)
std dev              60.06 ps   (42.45 ps .. 76.74 ps)
variance introduced by outliers: 11% (moderately inflated)
sf ★★★
()
Ответ на: комментарий от sf

Оно и в лиспах одинаково, просто хаскелевский compose - сильно не тот compose. В лиспе compose принимает список.

anonymous
()
Ответ на: комментарий от anonymous

на каждый вызов твой закомпоженной ф-и разбирать списки. Apply опять же тоже довольно медленный

apply применяется только при вызове compose. То есть.

(compose f g h) => (lambda (x) (f ((compose g h) x))) => (lambda (x) (f ((lambda (y) (g ((compose h) y))) x))) => (lambda (x) (f ((lambda (y) (g (h y))) x)))

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

Если руками написать (lambda (x) (f ((lambda (y) (g (h y))) x))), то работает быстро.

Похоже, что компилятор не может инлайнить результат, так как функция появляется только при выполнении. И с curry та же проблема

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

А если у тебя там compose не с двух ф-й а с пяти?

Не вижу препятствий.

Compose := Object clone do(
  value ::= nil
  fill := method(functions, self functions := functions)
  run := method(
    while(functions isNotEmpty, value = functions pop call(value))
    value
  )
)


f := block(x, x + x)


Compose clone do(
 fill(list(f, f, f, f, f))
 setValue(1)
 run print
)

//32

Суть та же самая, только все ясно, без вырвиглазности, и тормозной рекурсии. Это можно легко модифицировать и расширять как угодно, например, получить «аргументы» порциями. ООП рулит.

anonymous
()
Ответ на: комментарий от anonymous

А ниче, что именно Нейман запилил комп?

А ничё, что Нейман своровал комп у Экерта и Мокли, выдав за свой?

как инженер

Нейман сраный математик, а не инженер.

no-such-file ★★★★★
()
Последнее исправление: no-such-file (всего исправлений: 1)
Ответ на: комментарий от anonymous

только все ясно, без вырвиглазности, и тормозной рекурсии.

Думаю, у тебя тормозить будет ещё больше. Сделай тест своего Compose в сравнении с f(f(f(f(f(1))))).

monk ★★★★★
() автор топика
Ответ на: комментарий от no-such-file

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

anonymous
()
Ответ на: комментарий от monk

С чего бы ему тормозить, если там цикл?

anonymous
()
Ответ на: комментарий от monk

apply применяется только при вызове compose.

НеТ, конечно, он применяется при вызове результата compose.

(compose f g h) => (lambda (x) (f ((compose g h) x)))

На этом вычисление compose останавливается, это результат. А дальше редукции пойдут уже тогда, когда ты этот результат применишь к конкретному x.

anonymous
()
Ответ на: комментарий от monk

Похоже, что компилятор не может инлайнить результат, так как функция появляется только при выполнении. И с curry та же проблема

Ты просто порядок редукций перепутал. С инлайнингом результата проблем никаких нет. Всмысле вариант с (define (compose f g) (f (g x))) при отрубленном инлайне тоже будет достаточно быстрый.

anonymous
()
Ответ на: комментарий от next_time

если 1000000*4 вызовов ФПВ происходит через каждую 1 с., получил 10% прироста производительности.

fixed

ados ★★★★★
()
Ответ на: комментарий от ados

да, если вызов ф-ции, в которой происходит 1000000*4 вызовов ФПВ, происходит через каждую 1 с., получил 10% прироста производительности.

именно это и имелось в виду

next_time ★★★★★
()
Ответ на: комментарий от anonymous

На этом вычисление compose останавливается, это результат

Cмотрим ещё раз, внимательней:

(define (compose f . fs)  
  (cond 
    [(null? fs) f]
    [else 
     (define rest-f (apply compose fs))
     (lambda (x) (f (rest-f x)))]))
(compose f g h)
=>
(begin
  (define rest-f (apply compose (list g h))
  (lambda (x) (f (rest-f x))))
=>
(begin
  (define rest-f 
    (begin
       (define rest-f1 (apply compose h))
       (lambda (x) (g (rest-f1 x))))
  (lambda (x) (f (rest-f x))))
=>
(begin
  (define rest-f 
    (begin
       (define rest-f1 h)
       (lambda (x) (g (rest-f1 x))))
  (lambda (x) (f (rest-f x))))
=>
(begin
  (define rest-f 
     (lambda (x) (g (h x))))
  (lambda (x) (f (rest-f x))))
=>
(lambda (x) (f ((lambda (x) (g (h x))) x)))
monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

В haskell бывает compose :: [(a->a)] -> (a->a) ?

Да. compose_rr это тот же compose только с rewrite rule :)

-- https://www.linux.org.ru/forum/development/11943079
module Main (main) where

import Criterion.Main

f :: Num a => a -> a
f x = (((x + 1) + 2) + 1)

f_comp :: Num a => a -> a
f_comp = (+ 1) . (+ 2) . (+ 1)

compose :: [a -> a] -> a -> a
compose fs v = foldl (flip (.)) id fs v

f_comp_list :: Num a => a -> a
f_comp_list = compose [(+ 1), (+ 2), (+ 1)]

-- Extra fun
{-# INLINE[0] compose_rr #-}
compose_rr :: [a -> a] -> a -> a
compose_rr fs v = foldl (flip (.)) id fs v

f_comp_list_rr :: Num a => a -> a
f_comp_list_rr v = compose_rr [(+ 1), (+ 2), (+ 1)] v

{-# RULES "compose_rr/[]" [2]
        forall v . compose_rr [] v = v
  #-}
{-# RULES "compose_rr/[...]" [2]
        forall fn fs v . compose_rr (fn:fs) v = (compose_rr fs . fn) v
  #-}

main :: IO ()
main =
    defaultMain [
        bench "f (Int)"              $ whnf f (42 :: Int)
      , bench "f_comp (Int)"         $ whnf f_comp (42 :: Int)
      , bench "f_comp_list (Int)"    $ whnf f_comp_list (42 :: Int)
      , bench "f_comp_list_rr (Int)" $ whnf f_comp_list_rr (42 :: Int)
      ]
$ ghc --make -O2 how-things-inline.hs -Wall -fforce-recomp && ./how-things-inline 
[1 of 1] Compiling Main             ( how-things-inline.hs, how-things-inline.o )
Linking how-things-inline ...

benchmarking f (Int)
time                 5.558 ns   (5.548 ns .. 5.573 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 5.557 ns   (5.550 ns .. 5.568 ns)
std dev              28.95 ps   (19.28 ps .. 48.15 ps)

benchmarking f_comp (Int)
time                 5.829 ns   (5.804 ns .. 5.877 ns)
                     0.999 R²   (0.998 R² .. 1.000 R²)
mean                 5.857 ns   (5.822 ns .. 5.953 ns)
std dev              194.5 ps   (58.71 ps .. 362.9 ps)
variance introduced by outliers: 56% (severely inflated)

benchmarking f_comp_list (Int)
time                 36.23 ns   (36.06 ns .. 36.40 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 36.28 ns   (36.16 ns .. 36.47 ns)
std dev              507.3 ps   (381.7 ps .. 677.2 ps)
variance introduced by outliers: 16% (moderately inflated)

benchmarking f_comp_list_rr (Int)
time                 5.610 ns   (5.586 ns .. 5.633 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 5.597 ns   (5.588 ns .. 5.610 ns)
std dev              36.53 ps   (26.73 ps .. 49.41 ps)
sf ★★★
()
Ответ на: комментарий от sf

{-# RULES «compose_rr/[...]» [2]

forall fn fs v . compose_rr (fn:fs) v = (compose_rr fs . fn) v #-}

Если список передать аргументом то правило не сработает же

anonymous
()
Ответ на: комментарий от monk

а ну да, действительно. Но apply не инлайнится, т.к. #%kernel.

anonymous
()
Ответ на: комментарий от sf

Да. compose_rr это тот же compose только с rewrite rule

Спасибо. Всё-таки та же разница (около 5 раз). Значит, хочешь быстро — пиши ФВП на макросах/правилах перезаписи.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Если список передать аргументом то правило не сработает же

Зависит от порядка inline, rewrite rule и формы самого аргумента.

Естественно, если аргумент еще реально вычислить надо (на стадии компиляции не видно формы), то и подставлять нечего. Но если это просто отдельный биндинг, то его можно сначала заинлайнить в точку вызова compose_rr, а потом развернуть.

Опции ghc -ddump-rule-firings -ddump-inlinings помогают изучать процесс оптимизации.

-- https://www.linux.org.ru/forum/development/11943079
module Main (main) where

import Criterion.Main

f :: Num a => a -> a
f x = (((x + 1) + 2) + 1)

{-# INLINE[0] compose_rr #-}
compose_rr :: [a -> a] -> a -> a
compose_rr fs v = foldl (flip (.)) id fs v

f_comp_list_rr_arg :: Num a => a -> a
f_comp_list_rr_arg v = compose_rr arg v

arg :: Num a => [a -> a]
arg = [(+ 1), (+ 2), (+ 1)]

{-# RULES "compose_rr/[]" [2]
        forall v . compose_rr [] v = v
  #-}
{-# RULES "compose_rr/[...]" [2]
        forall fn fs v . compose_rr (fn:fs) v = (compose_rr fs . fn) v
  #-}

main :: IO ()
main =
    defaultMain [
        bench "f (Int)"                  $ whnf f (42 :: Int)
      , bench "f_comp_list_rr_arg (Int)" $ whnf f_comp_list_rr_arg (42 :: Int)
      ]
$ ghc --make -O2 how-things-inline.hs -Wall -fforce-recomp && ./how-things-inline 
[1 of 1] Compiling Main             ( how-things-inline.hs, how-things-inline.o )
Linking how-things-inline ...
benchmarking f (Int)
time                 6.001 ns   (5.984 ns .. 6.022 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 6.012 ns   (5.999 ns .. 6.032 ns)
std dev              52.96 ps   (38.10 ps .. 66.68 ps)

benchmarking f_comp_list_rr_arg (Int)
time                 5.876 ns   (5.845 ns .. 5.906 ns)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 5.875 ns   (5.852 ns .. 5.897 ns)
std dev              74.52 ps   (67.90 ps .. 82.52 ps)
variance introduced by outliers: 16% (moderately inflated)
sf ★★★
()
Ответ на: комментарий от monk

Значит, хочешь быстро — пиши ФВП на макросах/правилах перезаписи.

В haskell для композиции обычно хватает (.) точки. Она не скрывает число склеиваемых функций за списками.

compose на списках с ФВП не связан.

sf ★★★
()
Ответ на: комментарий от sf

Зависит от порядка inline, rewrite rule и формы самого аргумента.

Не зависит.

В твоем примере компилятор вообще может сразу constant folding сделать. Сделай так, чтобы список в рантайме строился.

anonymous
()
Ответ на: комментарий от anonymous

> > Если список передать аргументом то правило не сработает же
> Зависит от порядка inline, rewrite rule и формы самого аргумента.
Не зависит.

https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/rewrite-rules...

«7.23.3. How rules interact with INLINE/NOINLINE pragmas»

В твоем примере компилятор вообще может сразу constant folding сделать.

В голой теории может, но GHC этого не делает.

Сделай так, чтобы список в рантайме строился.

Пример ТС (и этот пример) не об этом.

Без rewrite rules он «в рантайме строится».

sf ★★★
()
Ответ на: комментарий от sf

«7.23.3. How rules interact with INLINE/NOINLINE pragmas»

В рассматриваемом случае это ни на что не влияет.

В голой теории может, но GHC этого не делает.

Я бы сказал что не может, а должен. По-этому рассматриват ьподобные случаи вообще не имеет смысла.

Пример ТС (и этот пример) не об этом.

А о чем он? Какой смысл рассматривать оптимизации примеров, которые компилятор вообще может сфолдить в результат?

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.