LINUX.ORG.RU

Ускорение интерпретации

 


0

2

Считается, что значительная просадка производительности интерпретации вызвана проверкой типов в рантайме. Возникла такая идея. Что если на уровне интерпретатора реализовать только базовые типы выражений, минимальное их количество, а уже на уровне пользователя писать метациклический интерпретатор, расширяющий базовые типы. Многие типы, и вообще многие фичи, программисту могут не понадобиться в конкретном приложении, поэтому можно избежать многих ненужных проверок. А целевое приложение писать уже на расширенном языке. Получается своего рода DSL. Дала бы такая техника написания программ значительный прирост производительности?

многие фичи, программисту могут не понадобиться

Да есть много маловажных фич. Одна из них — лисп.
Phill, ты?

Stahl ★★☆
()

Если честно основная просадка производителтности это выделение памяти. Я не про микроконтроллеры. Там-то всё просто: наинкрементил атомарно ячейку и всё ок. Я про реальные приложения. У меня, на шарпе, загружаются 1000+ объектов с каналами за 13сек. Перезагружаются за 9. Итого (13-9)/13=30%.

ziemin ★★
()

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

Ровно так все и делают. Есть тэг в каждом объекте, определяющий базовые типы (fixnum, float, double, bignum, string, object). всё остальное — поверх object (может пару базовых пропустил, там вроде 8 должно быть).

Проблемы две: 1. Вся арифметика имеет переход fixnum -> bignum. В смысле fixnu + fixnum = bignum, fixnum - fixnum = bignum. Но bignum гораздо медленнее. Значит проверка тэга перед каждой арифметической операцией. Аналогично с делением — там целые, рациональные, float, double.

2. Если как в питоне перегружаются операторы, то всё ещё хуже: для a + b перед выполнением надо проверить, есть ли __add у a, есть ли __radd у b и только потом выполнить. В цикле приходится проверять на каждой итерации, так как если a или b поменяли значение, то соответствующий метод мог появиться.

3. Сборщик мусора. Для много-много операций в цикле иногда хотелось бы иметь возможность аллоцировать память в заранее полученном пуле, а по окончании цикла оптом освобождать.

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

Я не про микроконтроллеры. Там-то всё просто: наинкрементил атомарно ячейку и всё ок. Я про реальные приложения. У меня, на шарпе

Найди какой-нибудь pool allocator. Будет как на микроконтроллере :-)

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

anonimous и понимание в одном посте вызывают разрыв шаблона. Оно же в мозговую деятельность неспособно в принципе.

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

Ну вообще да, ты прав. Оно ведь так и не поняло, как работают суперскалярные CPU.

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

Если честно основная просадка производителтности это выделение памяти. Я не про микроконтроллеры. Там-то всё просто: наинкрементил атомарно ячейку и всё ок. Я про реальные приложения. У меня, на шарпе, загружаются 1000+ объектов с каналами за 13сек. Перезагружаются за 9. Итого (13-9)/13=30%.

Это у тебя JIT прогревается. Ну или кеш какой-нибудь.

mashina ★★★★★
()
Последнее исправление: mashina (всего исправлений: 1)

при чём тут тег "лисп"?

В Common Lisp есть статическая типизация, компиляция в нативный код, возможность отключить проверку типов директивой компилятора в нативный код. Если речь идёт о лиспе, то не угодно ли будет автору темы заняться актуальными вопросами?

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

Значит проверка тэга перед каждой арифметической операцией.

SBCL пытается это оптимизировать.

Если как в питоне перегружаются операторы

Да, тут становится понятен смысл деклараций static final или как там оно пишется в Java. С другой стороны, необязательно проверять на каждой итерации, есть и другие варианты. Например, кешировать скомпилированный код и сбрасывать кеш при влияющем на смысл кода изменении системы типов. Так поступает MS SQL: хранимая процедура компилируется при первом вызове. При DDL над зависимостями скомпилированный код процедуры сбрасывается из кеша и при следующем вызове хранимая процедура будет перекомпилирована.

Сборщик мусора.

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

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

SBCL пытается это оптимизировать.

Либо явные границы в типе (через declare или cond) либо увидишь пачку предупреждений про «не могу оптимизировать generic-+».

Например, кешировать скомпилированный код и сбрасывать кеш при влияющем на смысл кода изменении системы типов.

Я не про систему типов, а про необходимость проверок на каждой операции, где используются переменные. В Common Lisp'е аналогичная проблема с CLOS. Но в лиспе это дело добровольное, а в питоне — обязательное.

Есть мнение, что сборщик мусора иногда быстрее явного выделения.

Он почти всегда быстрее. Но когда объектов сильно много (например список чисел на пару гигабайт), то он начинает тупить. Кстати вспомнил, в CL можно обойти GC храня данные в массиве примитивного типа (fixnum, например). Но не всё так можно сохранить и это почти всегда неудобно.

monk ★★★★★
()
Ответ на: при чём тут тег "лисп"? от den73

В Common Lisp есть статическая типизация

Да ну? declare почти всегда приводит к динамическим проверкам. Очень мало программ получается полностью привести к статическим типам без проверок в динамике.

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

3. Сборщик мусора. Для много-много операций в цикле иногда хотелось бы иметь возможность аллоцировать память в заранее полученном пуле, а по окончании цикла оптом освобождать.

На самом деле всегда было непонятно почему не реализуют такую фичу. Берем обычный перемещающий сборщик с поколениями - он by-design имеет возможность размечать поколения и запускать сборку конкретного поколения. Почему бы не дать эту возможность напрямую программисту? То есть ставим в некоей точке программы тег «отметить новое поколение» (сборщик ставит «закладку» в памяти) и потом запускаем гц вплоть до этого участка. В чем проблема? Тривиальная вещь, казалось бы. Кроме того известно же что сложность работы перемещающего сборщика пропорциональная _живым_ данным. То есть запуская сборщик в те моменты, когда отмеченное поколение пустеет, можно свести затраты на сборку к ~0.

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

Да ну? declare почти всегда приводит к динамическим проверкам.

Стандартом не определено. В clisp - да, там типизация «декларативная», не влияет на результирующий байткод. В sbcl проверки «внутри» блока/функции я не видел - только на «входе», в «типизированном окружении» проверок не будет.

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

в «типизированном окружении» проверок не будет

Ну давай.

$ cat opt.lisp
(declaim (optimize (speed 3) (safety 1) (debug 1)))

(defun my-plus (list)
  (do ((res 0 (+ res (the fixnum (car l)))) 
       (l list (cdr l)))
      ((null l) res)
    (declare (fixnum res) (list l))))

(my-plus (list 1 2 3))

У меня после компиляции дизассемблируется в проверку на fixnum внутри каждой итерации. Сможешь сделать проверку статической?

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

не включай дурачка: ёжику понятно, что при нетипизированном списке по другому не будет.

Речь ведь шла о ситуации, когда все типы известны (не тебе - компилятору) - не правда ли? :)

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

Речь ведь шла о ситуации, когда все типы известны (не тебе - компилятору) - не правда ли? :)

Моя цитата «Очень мало программ получается полностью привести к статическим типам без проверок в динамике.». Очень мало программ на лиспе ни разу не используют списки и объекты.

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

Сможешь сделать проверку статической?

(defun my-plus (list)
  (declare (optimize (speed 3) (safety 1) (debug 1)))
  (do ((res 0 (+ res (the fixnum (car l)))) 
       (l list (cdr l)))
      ((null l) res)
    (declare (fixnum res) (list l))))

(disassemble 'my-plus)

Попробуй сравнить с

(defun my-plus (list)
  (declare (optimize (speed 3) (safety 0)))
  (do ((res 0 (+ res (the fixnum (car l)))) 
       (l list (cdr l)))
      ((null l) res)
    (declare (fixnum res))))

(disassemble 'my-plus)

и не #$% мозги

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

Очень мало программ получается полностью привести к статическим типам без проверок в динамике.

Очень нахрен не нужно приводить ПОЛНОСТЬЮ программу к статическим типам - только «батлнеки».

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

safety 0 — это не статическая проверка, а её полное отсутствие.

(my-plus 5) в этой ситуации скомпилируется и при выполнении вызовет сегфолт

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

это и есть так называемая «статическая проверка».

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

Правильные пацаны сначала пишут и отложивают программу, а уж после расставляют типы для скорости.

В sbcl по умолчанию (safety 2), и если программу пишет лиспер, а не чурбан, то (my-plus 5) выявится задолго до расстановки типов.

Если же заранее не известно что передается функции, то это динамичесуая типизация.

Лисп позволяет совмещать оба вида типизации. Например, динамически проверять, что передан список, но если список, то считать его состоящим из фикснумов

(defun my-plus (list)
  (declare (optimize (speed 3) (safety 0)))
  (check-type list list "параметр не список")
  (do ((res 0 (+ res (the fixnum (car l)))) 
       (l list (cdr l)))
      ((null l) res)
    (declare (fixnum res))))

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

Лисп позволяет совмещать оба вида типизации.

С этим утверждением согласен. Но полностью статическую программу написать нельзя. И поэтому на http://benchmarksgame.alioth.debian.org/u32/benchmark.php?test=all&lang=s... проигрывает Си в среднем в 3-5 раз.

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

Но полностью статическую программу написать нельзя.

Можно. Выполни послеовательно в РЕПЛе

(defun my-plus (list)
  (declare (optimize (speed 3) (safety 0)))
  (do ((res 0 (+ res (the fixnum (car l)))) 
       (l list (cdr l)))
      ((null l) res)
    (declare (fixnum res))))

(defun test ()
  (my-plus 5))

(declaim (ftype (function (list) fixnum) my-plus))

(defun test ()
  (my-plus 5))

. И поэтому на http://benchmarksgame.alioth.debian.org/u32/benchmark.php?test=all&lang=s... проигрывает Си в среднем в 3-5 раз.

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

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

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

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

Хоть один алгоритм бы реализовали. Как proof of concept.

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

Можно хоть один пример?

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

Тебе же уже пояснили, что safety 0 - это не статическая программа. safety и статика/динамика вообще никак не связаны.

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

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

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

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

Да, точно, такое на С пишут годами. И вcе равно ничего не получается, глючит и тормозит

Hint: Кроме того что функцию можно распарсить и посчитать, в С тоже можно делать eval. Например, system("cc ..."); popen/dlopen

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

Я не очень понял, ты пытаешься оспорить ли это что?

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

Само собой лисп быстрее gcc + dlopen.

Только причем тут это.

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

алгебраическое выражение, задающее функцию одной переменной

парсер с нуля пишется день (или есть готовые muparser.beltoforion.de) + таблица производных. Для лиспа можно сэкономить разве что на парсере, если требовать функцию в виде s-exp. Таблица производных та же.

Скорость на лиспе будет меньше, так как чтение элемента из массива (для таблицы производных) по ссылке, вычисления double тоже медленнее.

Если хочешь, можем посоревноваться. Я на чистом Си пишу, ты на SBCL. Грязные хаки типа вызовов FFI не использовать. Предполагаю победу Си по скорости где то 1:2 (если напишешь близко к идеальному).

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

...небольшие статические алгоритмы...
... класс задач...

Можно хоть один пример?

Можно.

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

В лиспе - генеришь макрос со всеми типизациями, компилишь его и передаешь предикатом в функцию проверки. Все в рантайме.

В Си - пишешь Сишный рантайм, аналогичный лисповому со встроенным компилятором и некоей макросистемой... Пожалуй, это самое простое решение.

Подобные задачи нужны не столь часто, как сортировка пузырьком (или что там за тесты), но в подобных случаях делают результаты пиписькомерок бессмысленными.

---------------------------------------------

Тебе же уже пояснили, что safety 0 - это не статическая программа. safety и статика/динамика вообще никак не связаны.

Статическая типизация - это когда проверка проводится только во время коипиляции.

При ЛЮБОМ safety, отличном от 0, в код вставляется проверка типа в рантайме.

Если ты не можешь связать эти два факта - ты просто глуп!

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

парсер с нуля пишется день

парсер алгебраических выражений пишется час, хз

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

Статическая типизация - это когда проверка проводится только во время коипиляции.

Вот именно. А при safety 0 таких проверок нет. safety вообще никак не связано с проверками во время _компиляции_, safety связано с рантаймом (а статическая типизация, в свою очередь, никак не связана с рантаймом).

При ЛЮБОМ safety, отличном от 0, в код вставляется проверка типа в рантайме.

И при чем тут типизация?

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

В Си - пишешь Сишный рантайм, аналогичный лисповому со встроенным компилятором и некоей макросистемой... Пожалуй, это самое простое решение.

Обычно либо оформляется отдельным процессом и перекомпилируется целиком программа обработки сигнала. Или (если сериализовать неудобно), то перекомпилируется плагин. Если время написания программы менее важно, чем время работы (например одна прогонка занимает несколько часов), то Си по-прежнему выгоден.

Исключение: если изменять надо произвольную, неизвестную заранее часть программы, без остановки программы. Тогда только Common Lisp или Erlang.

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

Что за таблица производных

Чтобы посчитать производную произвольной алгебраической функции в символьном виде достаточно сделать преобразование по этим правилам: http://ru.wikipedia.org/wiki/Таблица_производных

Или можно просто вычислить f(x+eps)-f(x-eps)/2/eps. Тогда почти вся программа вот: http://muparser.beltoforion.de/mup_version.html#idExample

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

Надеюсь, ты не включаешь дурака как тот анон с safety... Ок. Подробно, по слогам:

Я утверждал, что пиписькомерки используют задания с точно описанными алгоритмами и заточенные под мейстримные языки со статической типизацией. Но если подобрать другой класс задач, то сложность написания сравнимого по скорости кода на Си (иначе можно обойтись интерпретатором) становится столь трудоемкой задачей, что оправдывает выбор «маргинальных» языков. Соответственно, на таких задачах и лисп, и эрланг, и хаскель (я его не знаю, но наверняка такме задачи есть) будут рвать весь мейнстрим, включая Си, как Тузик грелку. Естесственно, задачи позволяющие выиграть лиспу, эрлангу или хаскелю будау разные.

Ты попросил пример.

Просто внимательно прочти задание ничего не додумывая. Хотя... Ты сам полетишь на Марс перкомпилировать плагин? Ладно, не надо на Марс - спустись в Марианскую впадину ;) Это как вариант почему перекомпиляция не приемлима.

Исключение: если изменять надо произвольную, неизвестную заранее часть программы, без остановки программы.

Определенная, заранее известная часть программы, правда без остановки. Если приведу такую, что на некоторых наборах данных будет рвать Си в разы, под каким столом будешь кричать «кукареку» и «monk осел»? :)

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

А зачем тут нужен компилятор?

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

Откуда медленнее-то? и какие трудозатраты? написание парсера на 30 строк - это трудозатраты по-твоему?

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

Это будет намного медленнее лиспа при намного больших трудозатратах

Ты в лиспе будешь считать производную как-то иначе? Поделись секретом, как?

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

Определенная, заранее известная часть программы, правда без остановки. Если приведу такую, что на некоторых наборах данных будет рвать Си в разы

Не верю. Возможно только в случае, если «на Си» написали тупой интерпретатор для этой части программы вместо перекомпиляции из исходного кода на Си разных вариантов.

Если «известная часть программы» реализуется через плагин, то скорость работы близка к максимальной теоретически возможной (видеокодеки доказывают, да).

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