LINUX.ORG.RU

CL быстрее прочих :)


0

3

Новый, и как всегда красивый, пример кодогенерации от swizard

http://swizard.livejournal.com/158763.html

на этот раз решение задачи http://shootout.alioth.debian.org/u32q/benchmark.php?test=fannkuchredux&lang=...

★★★★★

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

но первое на порядки проще простого второго.

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

> в большинстве случаев обычные замыкания и так работаю быстрее чем eval в функциях, вызывающий макрос (это вообще никогда не нужно делать).

Одно дело константу в комил-тайм знать, другое в рун-тайм.
Пусть например выражение в lambda будет (/ n m), m = 4, разница в возможных оптимизациях с eval и без очевидна (подразумевая fixnum и пр. декларации).

Хотелось бы пролить свет на конкретную логику предлагаемых оптимизаций высшего порядка.

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

Вот есть код:

(defmacro mandelbrot-creator (size &optional (workers 1))
  ...
  `(lambda
     ...))

(defun main (&optional force-n)
  (let* (...
         (worker (eval `(mandelbrot-creator ...
      (funcall worker output-s))))

eval работает в runtime, как и макрос. Это эквивалентно обычным замыканиям:

(defun mandelbrot-closure (size &optional (workers 1))
  ...
  (lambda
    ...))

(defun main (&optional force-n)
  (let* (...
         (worker (mandelbrot-closure ...
      (funcall worker output-s))))

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

Что касается замыканий - может у swizard есть опасения по поводу их эффективности, может они плохо работаю в качестве генераторов в pidigits. Но SBCL специально делался так, чтобы было легко добавлялись как оптимизации отдельных форм и компонентов так и оптимизации более высокого уровня (имеются в виду оптимизации не отдельных компонентов, а их комбинаций на стадии ir1 при трансляции в flow graph, например lambda в mapcar или lambda в funcall и т.д.).

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

> eval работает в runtime, как и макрос

(load «prog.lisp») - тоже работает в runtime в некотором смысле, однако для того что внутри eval - компил-тайм это и есть сам вызов eval со всеми вытекающими оптимизациями

Это эквивалентно обычным замыканиям:


Неужели ? В том коде помимо вставки констант еще и код самой лямбды местами меняется в зависимости от параметров макроса. Это в некотором смысле можно отнести к multi-stage programming

Замыканиям не нужен eval, и работают они быстрее. Тот псто был об этом - я не понял зачем столько низкоуровневых хаков и такая штука рядом. Что касается замыканий - может у swizard есть опасения по поводу их эффективности, может они плохо работаю в качестве генераторов в pidigits


Похоже у тебя все еще нет понимания - чему ты противопоставляеш обычные замыкания

Но SBCL специально делался так, чтобы было легко добавлялись как оптимизации отдельных форм и компонентов так и оптимизации более высокого уровня (имеются в виду оптимизации не отдельных компонентов, а их комбинаций на стадии ir1 при трансляции в flow graph, например lambda в mapcar или lambda в funcall и т.д.).


Еще раз, как конкретно ты получишь эффект достигаемый eval/defmacro ? опиши логику предлагаемых оптимизаций.

ed ★★
()

Чото сверхскоростная CL-программа куда-то делась с шутаута. Она превысила скорость света и улетела в будущее?

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

(load «prog.lisp») - тоже работает в runtime в некотором смысле, однако для того что внутри eval - компил-тайм это и есть сам вызов eval со всеми вытекающими оптимизациями

Вот блин. В каком таком некотором смысле ? :) Почувствуй разницу - в функции используется eval, в компилированном машинном коде есть call-to-eval, при выполнении этой функции происходит вызов eval, а eval достаточно (очень) тяжёлая функция - в итоге наша функция, в которой eval, выполняется очень долго. А вот если там eval нет - машинный код не содержит его вызова и выполнение происходит быстрее.

(defun foo (a) (eval a))
(disassemble #'foo)
; disassembly for FOO
; 0ABA7961:       8BD6             MOV EDX, ESI               ; no-arg-parsing entry point
;       63:       8B053079BA0A     MOV EAX, [#xABA7930]       ; #<FDEFINITION object for EVAL>
;       69:       B904000000       MOV ECX, 4
;       6E:       FF7504           PUSH DWORD PTR [EBP+4]
;       71:       FF6005           JMP DWORD PTR [EAX+5]      ; !!! saenara !!!

http://xach.livejournal.com/131456.html

Неужели ?

Да.

В том коде помимо вставки констант еще и код самой лямбды местами меняется в зависимости от параметров макроса.

Closure (т.е. #<CLOSURE (LAMBDA (...)) {...}>) возвращает разные функцию в зависимости от своих параметров. Я удивлён, что приходится говорить такие вещи - думаю стоит-таки почитать SICP, например. Можно хотя бы третью главу просмотреть.

Похоже у тебя все еще нет понимания - чему ты противопоставляеш обычные замыкания

Да ты не обо мне думай :) Вот ещё:

http://lisper.ru/articles/eval-when

http://www.lispworks.com/documentation/HyperSpec/Body/03_.htm

Еще раз, как конкретно ты получишь эффект достигаемый eval/defmacro ? опиши логику предлагаемых оптимизаций.

Нет, ты меня не понял. Чтобы получить этот «эффект» eval/defmacro ничего делать не нужно (ну вернуть замыкание), всё элементарно. Про оптимизация я просто упомянул - что есть ещё то, в чём можно оптимизировать замыкания, безотносительно этого примера.

Может переписать тот пример с замыканиями?

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

Ты слышал звон да не понял о чем он. К чему ты это все это написал ? eval _ОДИН_ раз вызывается, разверни mandelbrot-creator macroexpand-ом и будет все понятно, и присходит компиляция и оптимизация того что в нем, и то что там получится в результате будет быстрее любых твоих замыканий. Стоимость одного eval ничтожна при какой либо продолжительной работе.

Замыканиям не нужен eval, и работают они быстрее.

Может переписать тот пример с замыканиями?


Замечательно, докажи на mandelbrot

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

И замыкание - _ОДИН_ раз вызывается :) Но замыкание выглядит привычно, в отличии от 0_o/

Замечательно, докажи на mandelbrot

Попозже, неохота пока )

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

... блин забыл и кровь литле схемеров.

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

> И замыкание - _ОДИН_ раз вызывается :)
только в нем не будет например оптимизации констанат на низком уровне, хотя бы этого достаточно - что бы в общем случае опровергнуть утверждение - «Замыканиям не нужен eval, и работают они _быстрее_.» - это зависит от задачи и продолжительности работы.

Но замыкание выглядит привычно, в отличии от 0_o/

Да ладно, напиши эквивалент, а там посмотрим что страшнее будет :)

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

Ок, по пунктам:

1) Версия Peter как раз нормальная - там back-quoted-lambda возвращается из функции, а не из макроса - так как раз можно делать (функция возвращает построение выражение без вычисления, в отличии от макроса который посторонние выражение вычисляет). Далее построенный код компилируется compile, а не вычисляется eval. Подход «построить код-matcher, решающий задачу, скомпилировать его» как раз вполне нормальный (например, уже упоминавшееся, построение DFA по Томпсону из регулярных выражений «строка» -> «код DFA» -> «компиляция» + выполнение этого matcher-а на аргументах - текст + паттерн).

2) Версия Zach - чистые замыкания. Как раз в этом случае построение матчера путём компиляции построенного кода эквивалентно замыканиям.

3) Не вижу разницы во времени выполнения:

(time (test-match.xach 100000))
  8.839656 seconds of total run time (0.000000 user, 8.839656 system)

(time (test-match.peter 100000))
  8.797662 seconds of total run time (0.000000 user, 8.797662 system)
  1 lambda converted -- oops! useless work :)

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

4) Вижу, что замыканее гораздо эффективнее в плане памяти:

(time (test-match.xach 100000))
  37,384 bytes consed
  
(time (test-match.peter 100000))
  122,048 bytes consed

5) А в плане скорости компиляции... Удивительное рядом...

(time (test-compiling.peter 1000))
  3.485470 seconds of total run time (0.000000 user, 3.485470 system)
  91,920,896 bytes consed
  
(time (test-compiling.xach 1000))
  0.000000 seconds of total run time (0.000000 user, 0.000000 system)
  118,320 bytes consed

поделил на ноль :)

6) Не понятно вообще - почему вызов eval наделяется такой магической природой? Какие-то «оптимизации констант» там. Любой код проходит через мясорубку стадий sexp -> IR1 -> IR2 -> Asm -> Machine code. Важно окажется ли в результирующем коде вызов метастадии - runtime компиляции или нет, если этого можно избежать (вызова компилятора во время выполнения) - этого нужно избежать. Что в том примере Zach и сделал. А если избежать нельзя - функции возвращающие код + compile, макро-лямбда-эвалы-то зачем?

7) Ну и вообще, есть два подхода - хардкодить потому что так быстрее, либо делать правильно и при наличии узких мест отправляться улучшать технологию (читай реализацию).

что вообщем и ожидаемо

Почему? Как говорят - «patches welcome» :)

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

> ... Далее построенный код компилируется compile, а не вычисляется eval ..

в контексте высокопроизводительных лисп реализаций так ли это важно compile или eval и в какой позе ?

Не вижу разницы во времени выполнения: ... два линейных алгоритма с коэффициентами отличающимися буквально на какие-то проценты.


*example-expression* то не сильно сложный, процентов немного может и прибавится в другом случае

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


в ccl та же картина, так что я бы особо не надеялся и разница эта вполне обяснима

4) Вижу, что замыканее гораздо эффективнее в плане памяти:

5) А в плане скорости компиляции... Удивительное рядом...



это да, но это никто и не оспаривал

Не понятно вообще - почему вызов eval наделяется такой магической природой? Какие-то «оптимизации констант» там.


да дело не в конкретно eval, пусть compile или вообще s-exps в file (load «file») - да, тут важен принцип multi-stage/specialization кода в том числе и по константам, eval упоминается просто потому что с него начали и это не особо важно в «быстрых» лиспах

макро-лямбда-эвалы-то зачем?


может кому то показалось что так удобней отлаживат или ближе к воображаемому решению только и всего

7) Ну и вообще, есть два подхода - хардкодить потому что так быстрее, либо делать правильно и при наличии узких мест отправляться улучшать технологию (читай реализацию).


правильно то по разному может быть в зависимости от целей, например код peter-а меньше и «ближе» к формулировке задачи

Почему? Как говорят - «patches welcome» :)


тут возникает вопрос а киких оптимизации не хватет в SBCL, ответ мы так и не нашли

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

в контексте высокопроизводительных лисп реализаций так ли это важно compile или eval и в какой позе ?

Это просто разные вещи - eval это интерфейс ко всему рантайму (вместе с компилятором), compile это интерфейс именно к компилятору. Если нам нужно построенный код компилировать, то логично попросит именно «компилировать». Это на данный момент используется образный подход - всё вместе, а в перспективе это очень разные вещи.

В SLIME можно написать «EVAL» и с помощью «C-c >» проследить что оно вызывает, что вызывают вызываемые и т.д. Там достаточно далеко этот compile, т.е. тут вопрос консистентного написания.

Потом если нужно построить код который будет строится и выполнятся в toplevel, но просто строится в остальных случаях - логично использовать макросы. Если строящийся код зависит от переменного числа параметров, от «тел», или от переменных - тоже логично строить макросы. А вот если строится параметрический код от атомарных параметров фиксированного количество - его разумно возвращать из функций. А сам код строится с помощью списочных конструкторов/ре-конструкторов (квази-цитирования и комм) - их можно использовать где угодно, не имеют отношения именно к макросам. При этом макросы (с их специальной ролью) никогда не будут попадать под eval (потому что странно), а вот код возвращаемый из функций (признак - back-quote кода в defun) будет попадать под (compile nil ...).

У swizard-а есть обычные макросы, но сама процедура построения кода везде обычная. Скажем, блинчики к замыканиям не сводятся, но там нужно сделать:

-(defmacro deffannkuch (seq-length &key (workers 1) worker-chunk-size)
+(defun make-fannkuch-lambda (seq-length &key (workers 1) worker-chunk-size)
   (let* ((facts (make-facts-vector seq-length))
          (chunk-size (or worker-chunk-size (min (elt facts seq-length) 400)))
          (seq-vars (loop :for i :from 0 :below seq-length :collect (gensym))))
     `(lambda ()
        (declare (optimize (speed 3) (safety 0) (debug 0)))
        ...

(defun main (&optional (n (let ((options (rest sb-ext:*posix-argv*)))
                            (if options
                                (parse-integer (first options))
                                12))))
  (let ((fannkuch-lambda (compile nil (make-fannkuch-lambda n :workers 4 :worker-chunk-size 12000))))
    (multiple-value-bind (checksum max-flips-count)
        (funcall fannkuch-lambda)
      (format t "~a~%Pfannkuchen(~a) = ~a~%" checksum n max-flips-count))))

Чтобы не смешивать роли. А вот mandelbrot уже сводится к замыканиям:

(macrolet ((setf-expander (var n collect-expr)
             `(setf ,@(loop :for i :from 0 :below 4
                            :collect `(elt ,var ,(+ i n))
                            :collect collect-expr)))
           (dummy-setf-expander (var n)
             `(setf ,@(loop :for i :from 0 :below 4
                           :collect `(elt ,var ,(+ i n))
                           :collect `(- (* (/ 2.0 size) (- 3 ,i)) 1.5)))))
  (defun mandelbrot-creator (size &optional (workers 1))
    (let* ((header             (map 'vector #'char-code (format nil "P4~%~d ~d~%" size size)))
           (iter               50)
           (row-width          (ceiling size 8))
           (image-octets-count (* size row-width))
           (smp-p              (> workers 1)))
      #'(lambda (out-stream)
          (declare (type stream out-stream)
                   (optimize (speed 3) (safety 0) (debug 0)))
          (let ((header  (make-array (length header) :element-type '(unsigned-byte 8) :initial-contents header))
                (image   (make-array image-octets-count :element-type '(unsigned-byte 8)))
                (row-idx (if smp-p (make-atomic) 0)))
            (write-sequence header out-stream)
            (labels ((worker-proc ()
                       (let ((v4sf24 (make-array 24 :element-type 'single-float)))
                         (many (setf-expander v4sf24)
                           (4   (/ 2.0 size))
                           (8   -1.0)
                           (12  4.0)
                           (20  (/ 8.0 size)))
                         (dummy-setf-expander v4sf24 16)
                         (loop
                            :for row = (if smp-p
                                           (sb-ext:atomic-incf (atomic-counter row-idx))
                                           (let ((old row-idx))
                                             (progn (setf row-idx (1+ old))
                                                    old)))
                            :while (< row size)
                            :for y = (coerce row 'single-float)
                            :for offset = (* row row-width)
                            :do (setf-expander v4sf24 0 'y)
                            :summing (%sse-mandelbrot-row image offset (+ offset row-width) v4sf24 iter) :into s
                            :do (unless (zerop (mod size 8))
                                  (setf (elt image (+ offset (1- row-width)))
                                        (logand (elt image (+ offset (1- row-width)))
                                                (logxor 255 (1- (ash 1 (- 8 (mod size 8))))))))))))
              (if smp-p
                  (mapc #'sb-thread:join-thread
                        (loop :repeat workers :collect '(sb-thread:make-thread #'worker-proc)))
                  (worker-proc)))
            (write-sequence image out-stream)
            (values))))))

(тут теряются некоторые типы и появляются лишние инструкции в машинном коде - это потому что общий алгоритм и на SMP и «просто», если для одного ядра писать отдельно, для нескольких отдельно - всё будет без изменений но на зымыканиях).

Производительность от этого не меняется, как и общая структура - работает тот же подход построения кода, решающего задачу, что, конечно, хорошо :)

макро-лямбда-эвалы-то зачем?

может кому то показалось что так удобней отлаживат или ближе к воображаемому решению только и всего

Ну ладно, может они и имеют смысл в каких-то случаях (например - общий алгоритм для SMP/не-SMP в Мондельброте). Хотя нет - лучше раскрывать общие места макросом, который будет внутри этой самой функции которая возвращает замыкания - так что я по-прежднему против того Мондельброта :)

----

тут возникает вопрос а киких оптимизации не хватет в SBCL, ответ мы так и не нашли

Скорее вопрос не «чего ещё не сделано в SBCL», а в «что там уже сделано», т.е., на мой взгляд, очень многих вещей нет - wishlist может быть большой. (Did you read SBCL disassembly? написано там в каком-то ридми)

Да и всё равно, это совсем не

(make-awesome)
quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

> ... Это на данный момент используется образный подход - всё вместе, а в перспективе это очень разные вещи. ...

угу, но это был всего лишь намек что когда мы «разбираем» гоночные программы прибитые к SBCL гвоздями и прочие мерялки, особо цеплятся и теоретизориовать такие вещи не имеет смысла - людям было важно другое

тут теряются некоторые типы и появляются лишние инструкции в машинном коде - это потому что общий алгоритм и на SMP и «просто», если для одного ядра писать отдельно, для нескольких отдельно - всё будет без изменений но на зымыканиях


помимо smp там еще есть маленькое ветвление, это уже 4 варианта, ну и всякие (* row 1024[row-size]), константы от параметров - вплоне ожидаемо что при size стремящемся к бесконечности даже замыкание без лишних ветвлений и заточкой под типы чуть приотстанет и это на задаче почти идеально подходящей для замыканий

Производительность от этого не меняется, как и общая структура - работает тот же подход построения кода, решающего задачу, что, конечно, хорошо :)


Допустим производительность не меняется, но мы усложнили «модель» и сделали «эволюцию» решения не такой простой - но что мы выиграли в результате ?

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

Допустим производительность не меняется, но мы усложнили «модель» и сделали «эволюцию» решения не такой простой - но что мы выиграли в результате ?

Всего лишь позанимались буквоедством на тему «надо пейсать правильно» :) Как оказалось - принципиальной разницы нет, но всё-таки хочется видеть нормальные решения, в том числе в деталях.

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

> Всего лишь позанимались буквоедством на тему «надо пейсать правильно» :)
С другой стороны даже это хоть немного конструктивно в отличии от среднего содержания подобных топиков ...

ed ★★
()

Чуть улучшил показатели PHP для этого задания — но все равно впечатляет проигрыш SBCL'у — в более чем 1 час 20 мин...

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

> Когда уже перестанут писать эту ересь? С и С++ --- это разные, очень разные языки.

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

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

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

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

> для всех этих языков есть хорошие библиотеки

которые, в конечном счете, разворачиваются в биндинги к системным библиотекам. Так и в CL есть FFI - пользуйте.

Кстати, посмотрел сегодня pidigits на этом вашем шутауте - вот же объе...вка. Что ни язык, так тут же libgmp. Не спортивно.

написать просто быстрый код _проще_ на Java

Толсто.

no-such-file ★★★★★
()

c++ медленно спустился с холма ...

собственно в fannkuchredux все альтернативные чарты и x64 в руках С++. Качество того с++ не важно :) и на перформанс наивной программы под x86 никто смотреть не будет так как нет смысла. Там бага g++4.4 - если взять 4.5 то он захватит и основной чарт x86.

На этом попрошу лисперов прекратить утверждать что у них есть что-то конкурентоспособное для числодробильни на скорость. И сосредоточиться на том что у них есть хорошего.

sleepy
()
Ответ на: c++ медленно спустился с холма ... от sleepy

Посмеялись, спасибо.

Я тебе щас тоже накатаю на SBCL'овском ассемблере лапшу из SSE-мнемоник, скажу что это лисп такой охеренный. (но вообще, нет, я этого делать не буду, бо не настолько двинутый, как копрофагилюбители плюсов)

Love5an
()
Ответ на: c++ медленно спустился с холма ... от sleepy

Ага. Особенно интересны предварительно вычисленные таблицы. Дык, давай тогда уж просто вычислим результат заранее, и зафигачим один большой (format ....). Завуалировать предывчисения на лиспе элементарно - пишем макросы, еще макросы и еще немного макросов. В результате все вычисления происходят на этапе компиляции.

А если серьезно - чудес не бывает. На одной и то-же машине одинаковый алгоритм скомпилированный равными по оптимизирующим способностям компиляторами будет работать с примерно одинаковой скоростью и дело тут не в языке. То, что sbcl проигрывает gcc/g++ по качеству получаемого машкода - никто и не спорит.

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

>ещё одна «interesting alternative» programs :)

не только ) вчера мне удалось обойти фичи в 4.4 гну кодогене, которые мне мешали, и запостить программку которая обходит лисп в одном потоке и годится в основной чарт.

sleepy
()

Цитирую:

для перестановок N элементов мы генерим N вложенных циклов, причем каждый i-ый цикл объявляет себе (N - i) переменных


Вот и вся суть. Сделать такое на Цэшарпе - полчаса кодирования. Хак, безусловно, интересный, но требует подробнейшего объяснения (как в ЖЖшном посте). А теперь внимание, правильный вопрос: много вы знаете прогеров (включая себя :) ), кто будет ТАК подробно документировать код? А теперь представьте, что ВЕСЬ ВАШ ПРОЕКТ - это сплошное лиспохакерство и кодогенерация. :) Не повесится ли ПМ к моменту сдачи проекта?.....

matumba ★★★★★
()
Ответ на: c++ медленно спустился с холма ... от sleepy

> На этом попрошу лисперов... сосредоточиться на том что у них есть хорошего.

На веру в «код = данные»? :) Тык в мэйнстриме не молиться надо, а выдавать код. Пусть копипасту с заглушками и хардкодингом, но клиент должен быть доволен.

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

> А теперь представьте, что ВЕСЬ ВАШ ПРОЕКТ - это сплошное

лиспохакерство и кодогенерация. :)


Поэтому в реальных проектах подход в стиле swizard никто и не использует.

На веру в «код = данные»? :)


В CL много хорошего.

Тык в мэйнстриме не молиться надо, а выдавать код


Где? «мэйснтрим» это название компании или что такое? А в реальной практической работе CL и позволяет быстрее выдавать полезный по сравнению с тем же C# или Java.

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

> Не повесится ли ПМ к моменту сдачи проекта?.....

действительно как он бедный будет индивидуальные взмахи веслами на галере считать - а то некоторые говорят что макрос это 1 за 10 еще и данные за код выдать пытаются :)

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

можно попросить Вас так не размахивать зондом?

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