среда, 7 сентября 2011 г.

Clojure vs Scala - anecdote

В связи с моим предстоящим выступлением на встрече разработчиков Scala решил накинуть говна на вентилятор перевести появившийся недавно Clojure use case. Оригинал появился сегодня рано утром и взят отсюда.

"Хотелось бы немного поделиться нашим опытом из World Singles...
В ноябре 2009-го мы начали разрабатывать на Scala. Нам нужен был очень длительный процесс, который публиковал бы большие тома изменений из нашей БД пользователей в виде XML-пакетов, опубликованных в кастомном поисковом движке. Отобразить пол дюжины таблиц из БД на XML было довольно сложно, и компания уже перепробовала несколько решений с разным успехом. Я предложил Scala, основываясь на производительности, конкурентности, типобезопасности и краткости (особенно учитывая, что XML -- родная структура данных в Scala).
Мы использовали публикующие демоны на Scala в продакшене больше двух лет. В основном, они работали неплохо, однако при высокой нагрузке часто выкидывали исключения Out of Memory. После длительного ковыряния в коде, мы пришли к выводу, что это происходило благодаря (по крайней мере, частично) дефолтной реализации акторов в Scala. Scala должна скоро объединиться с Akka, и, так или иначе, мы раздумывали о миграции на Akka...
Но после того, как мы начали использовать Clojure в этом году (после экспериментов с Clojure с мая этого года) выяснилось, что довольно просто реализовать Clojure-версию нашего Scala-кода.
Чтобы по-новой создать публикующего демона, потребовалось около 15-ти часов работы, после которых стали проходить все наши функциональные тесты. Теперь мы запускаем полный тест, публикующий более 300 000 профайлов за раз. Scala-код вылетает с OoM-исключением уже на 50 000 профайлов (иногда даже меньше). А Clojure-код вышел сухим из воды и спокойно работает на таких нагрузках. Теперь мы собираемся заменить наш Scala-код на Clojure-код в следующем продакшен-билде.
Другая интересная особенность заключается в том, что Scala-код занял около 1000 строк (примерно 31k символов). Аналог на Clojure занял всего 260 строк и 11.5к символов. Ни тот, ни другой исходник не содержит большого числа комментариев (кгмм... я не горжусь этим, конечно, а просто показываю, что ни в одном из исходников нету лишнего текста, мещающего сравнивать их по объему). В сравнении также не учитывается код юнит-тестов, так что мы сравниваем чистый продакшен-код. Clojure-код написан по тем же принципам, что и Scala-код, в основном с теми же самыми функциями (Scala-код был написан в функциональном стиле), с небольшим рефакторингом для извлечения helper-функций и повышения модульности и упрощения поддержки.
В результате, понятное дело, мы берем в продакшен Clojure-код и полностью отказываемся от Scala.
Слава Ричу Хикки и Сlojure/core-команде за разработку такого великолепного языка общего назначения, решающего большие задачи! Спасибо вам!"

32 комментария:

  1. Я лет пять делал что-то подобное на OCaml/Stream. Стало быть Clojure недурно сворачивает рекурсии в итерации и не пытается вместить все необъятное дерево документа в heap (или что там у нее)?

    ОтветитьУдалить
  2. Да, но там не TCO, а явное указание через recur. Такие вещи Clojure разворачивает в циклы.

    ОтветитьУдалить
  3. Короче, хвостовая там организуется так:
    (defn myfunc [count]
    (if (zero? count)
    0
    (recur (dec count))))

    (myfunc 10)

    ОтветитьУдалить
  4. А, понял. То есть компилятор хочет явно знать точку сворачивания.

    ОтветитьУдалить
  5. Да. Со слов Рича Хикки, это из-за особенностей JVM.

    ОтветитьУдалить
  6. Где-то еще я такое видел. В scheme что ли...

    ОтветитьУдалить
  7. В Scheme recur не используется, там компилятор сам умеет выяснять, где можно выполнить TCO.

    ОтветитьУдалить
  8. Мне кажется, проблема не с хвостовой рекурсией (она распознается и оптимизируется автоматически), а с тем, что на каждый чих [при работе с коллекциями (а XML aka NodeSeq - это одна из разновидностей коллекции в Scala)] создается по объекту, и надо быть очень внимательным, когда пишешь код в "чрезмерно-функциональном" стиле... В тех случаях, когда надо минимизировать object allocation, приходится опускаться, к примеру, до того, что вместо option.map(f), используется if(option.isDefined) Some(f(option.get)) else None

    ОтветитьУдалить
  9. Вообще, стандартная библиотека XML уже давно подвергается критике за прожорливость и неудобство использования, и в одной из ближайших версий ее возможно заменят библиотекой anti-xml авторства Daniel'a Spiewak'a

    ОтветитьУдалить
  10. Разве ж код может быть "чрезмерно-функциональным"? По-моему, как раз наоборот: он может быть чрезмерно императивным, а функциональщина дает массу бонусов в виде строгости формализованных алгоритмов.

    ОтветитьУдалить
  11. Я считаю, что может. К примеру, сам я большой поклонник monadic APIs, и считаю их очень элегантными, но в силу того, что Scala не является "чистым" языком, и необходимость в них не такая острая, как в Haskell, от чистой функциональной парадигмы иногда лучше отсупить в пользу более эффективного [и понятного, для несведущих] императивного решения.

    Как любит повторять Мартин Одерски, Scala не демонизирует изменяемое состояние (хотя оно и не рекомендуется для повсеместного исопльзования).

    ОтветитьУдалить
  12. Василий, ты что, смеешься? На кой черт ориентироваться на этих "несведущих"? ФП -- базовая парадигма, знать ее нужно наравне с ООП. Функциональные языки -- отличный инструмент. Кто не знает элементарных вещей -- за парту! За учебники! Нечего брать в расчет неучей, лучше пусть они тянутся за теми, кто умнее и образованнее их. Тебе школы мало, когда учительница выводит "отличнику" пятерку, только потому что он хоть что-то знает на фоне абсолютных даунов с задней парты? Так мы и профукали наше образование. Теперь наши "лучшие в мире программисты" пишут индусокод не хуже самого певучего в Бангалоре йога.

    ОтветитьУдалить
  13. К слову, [типизованный] лямбда калкулус, который изучается на 2-3 курсе нормальных индийских BTech'ов (скажем, центральный BTech в Мумбаи), у нас входит в программу докторантуры...

    >> На кой черт ориентироваться на этих "несведущих"?
    Ориентироваться надо на то, что рано или поздно проект будет сдан тем самый индейцам на сопровождение :) Если только, конечно, ты не мечтаешь пожизненно и бессменно стать единственным компетентным специалистом в "том самом проекте" :-)

    ОтветитьУдалить
  14. Кроме этого, факт проигрыша в производетельности чистого функционального кода императивному (и вообще, и в случае Scala), всеже имеет место быть... Хотя шаги в направлении оптимизации в последних версиях делаются (к примеру, for-comprehensions и foreach оптимизируеются в for-loops, и т.п.)

    ОтветитьУдалить
  15. одно дело язык, а другое говно-библиотеки которые зачем-то включены в стандартную поставку
    с нетерпением жду выпила xml, actor, stream, io в Scala 3.0

    иначе скоро размер bloatware превысит данный в джаве

    ОтветитьУдалить
  16. зачем они включены - понятно. чтобы можно было смело называть язык general-purpose :) (и, кстати, разве это не прекрасно, что "затычки" медленно но верно заменяются бюолее интересными community-driven библиотеками -- akka, anti-xml, sbt, etc.)

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

    ОтветитьУдалить
  17. кстати, было бы крайне интересно посмотреть на код, о котором идет речь в письме, и попытаться его пооптимизировать.

    не исключено, что их проблемы связаны совсем не с производительностью scala.xml и багами в актерах, а с тем, что был сделан какой-то баг (который они, впоследствие, не повторили в clojure) :-)

    ОтветитьУдалить
  18. >> К слову, [типизованный] лямбда калкулус, который изучается на 2-3 курсе нормальных индийских BTech'ов (скажем, центральный BTech в Мумбаи), у нас входит в программу докторантуры...

    Чего не знал -- того не знал. Называй как хочешь, но по-моему -- это трындец, что только доктора у нас знают про лямбда калкулус. Ну куда уж ниже скатится наше высшее образование? Перед кем будем гордиться? Перед Таджикистаном каким-нибудь? Скоро Равшану и Джамшуту будем доказывать, что мы умнее их...

    ОтветитьУдалить
  19. не помню насчет типизированного, но обычный lambda calculus был в бгуире на третьем курсе (ФИЛП, Якимович)

    ОтветитьУдалить
  20. Я на ФКСиС-е учился, про лямбда исчисление там не слыхал.

    ОтветитьУдалить
  21. ух ты! на самом деле, есть упоминание лямбдя исчислений. более того, в курсе, похоже, затрагивается лисп!

    http://bsuir-helper.ru/predmet/filp/metody/filp-praktika-met-posobie

    я учился на "информатике", и у нас такого не было :-(

    ОтветитьУдалить
  22. нашел забавный коммент по поводу ФИЛП на dev.by ~> http://dev.by/blog/16957#comment-17295

    вполне соответствует моим ожиданиям :D

    ОтветитьУдалить
  23. Вася, на лиспе у нас было около десяти лаб (специальность ПОИТ)
    лабы вел Алексеев "у вас тут слишком много функций, вы знаете сколько стоит вызов функции?"
    лекции - Якимович, но он уехал из Беларуси пару лет назад

    ОтветитьУдалить
  24. То, что у в наших универах что-то по лиспу преподают -- это да, я слыхал. И, к тому же, хорошо себе представляю уровень преподавания. Он, мягко говоря, невысокий. Лучше всего в наших универах преподает молодежь (правда, не все, а у кого есть практический опыт), и то же, не всё, а в основном всякие С++,C# и ООП. Я знаю много преподов, которые работают одновременно где-нибудь на конторе и, таким образом, переносят свой практический опыт на преподавание. Хреново то, что все, что на наших аутсорсинговых конторках не требуется, все это преподают или абы как, или вообще никак. Сюда же попадают и ФП с хаскелем и лиспом, ЛП с прологом и много еще чего.

    ОтветитьУдалить
  25. Якимович в своих лекциях рассматривал и Common Lisp, и Haskell, и лямбда-калкулюс, и даже теорему Гёделя о неполноте. Другое дело, что студенты к такому были явно неготовы: громадный Common Lisp + "категоризированный" Haskell + Prolog + фундаментальные вопросы computer science - для одного семестра это явно многовато :)

    ОтветитьУдалить
  26. Есть подозрение, что версия программы на Scala грузила всё в память, в то время как Clojure использовал ленивые коллекции. По крайней мере в этом случае картина получается вполне логичной.

    ОтветитьУдалить
  27. Это было бы слишком просто :) думаю, автор, все-таки знает о существовании Stream'ов :)
    В случае с большим числом xml'ей, возможно, дал о себе знать известный баг scala.xml: память очищается не полностью, если XML был некорректным. Но все это спекуляции, пока мы не увидим код :)

    ОтветитьУдалить
  28. >> "у вас тут слишком много функций, вы знаете сколько стоит вызов функции?"

    inline that shit ^_^

    ОтветитьУдалить
  29. Я, признаюсь, и сам обалдел, когда это увидел. ФП без функций, это, знаете ли... Если уж так производительность нужна, то бери чистый Си и делай, как там тебе надо. А если корректность программы, читабельность, поддерживаемость и модифицируемость -- тогда уж ФП, но производительность, естественно, немного снизится.

    ОтветитьУдалить
  30. >> Это было бы слишком просто
    Да я, в общем-то, не сомневаюсь, что автор знал про стримы, но использовал ли он их - вот в чём вопрос. Пару недель назад я попался на похожей фишке: в многопоточной программе обрабатывалось сразу много запросов, при этом каждый процесс в какой-то момент генерировал довольно большое количество объектов в памяти (до нескольких сотен мегабайт). 90% этих объектов тут же "умирали", поэтому проблем большую часть времени не возникало. Однако когда случалось, что несколько потоков одновременно входят в эту фазу "большого количества памяти", общий объём живых объектов резко подскакивал и мог превысить выделенную память.
    То же самое может быть с XML: если отдельные XML-файлы имеют небольшой размер, то скорее всего для них в Scala использовался DOM-парсер, который и генерировал большое количество объектов в какой-то небольшой промежуток времени.
    Но, как вы правильно заметили, это всё спекуляции :)

    ОтветитьУдалить