Статьи

Groovy ++ в действии: как заработать 5000 долларов за час


Groovy — отличный язык программирования. Это очень выразительный, не многословный и позволяет избежать много шаблонного кода. У меня есть очень хорошие библиотеки и я прекрасно интегрируюсь с Java и любыми другими языками JVM. Он широко распространен, имеет отличную поддержку IDE и имеет удивительно дружелюбное и профессиональное сообщество. Такие фреймворки Groovy, как Grails и Gradle, доказали свою мощь Groovy как замечательного языка для создания специфичных для предметной области языков и других типов проблем Dont-Repeat-Yourself. Единственная проблема — это медленное сравнение с Java, Scala и другими статически типизированными языками.

Есть два измерения этой медлительности. Прежде всего, будучи полностью динамическим языком, Groovy обеспечивает динамическое выполнение каждого вызова метода (и любой операции в реальности). Это означает, что нам нужно проверить, что является классом экземпляра, найти мета класс, увидеть типы параметров, найти метод, наиболее специфичный для этих параметров, и, наконец, выполнить сам вызов. Кстати, сам вызов также может включать генерацию кода на лету. Второе измерение каким-то образом происходит от первого. В многопоточной среде некоторые из этих проверок требуют изменчивого чтения / записи (для каждого вызова метода и т. Д.), Что заметно замедляет процесс.

Конечно, команда Groovy Core, к которой я имею честь принадлежать, делает все возможное, чтобы ускорить процесс. Мы выполнили очень серьезную работу по ускорению в Groovy 1.5 и 1.6, которая была немного улучшена в 1.7, и сейчас очень многообещающая разработка происходит в поздней версии Groovy 1.8 (бета4 была выпущена всего несколько дней назад).

Тем не менее, на сегодняшний день Groovy не может конкурировать с Java по производительности. Мой опыт работы с Groovy 1.7.x заключается в том, что он в 4-10 раз медленнее, чем код Java. Вы можете попробовать https://github.com/alextkachman/fib-benchmark проект, который запускает те же тесты для Groovy 1.7 / 1.8, Java и Groovy ++.

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

Конечно, вы знаете, что решение существует. Это называется Groovy ++ — статически типизированное расширение Groovy. Вы можете найти более подробную информацию на странице проекта Groovy ++. Если коротко, Groovy ++ позволяет комментировать статический или динамически компилируемый фрагмент кода (класс, метод или весь модуль) (есть также смешанный режим, в котором компилятор позволяет смешивать статический и динамический компоненты). звонит вместе). Такой подход дает вам лучшее из обоих миров — производительность там, где это важно, и динамические функции там, где вам это нужно.

Мой личный опыт работы с Groovy ++ довольно прост — примерно 80% кода на Groovy можно сделать статически типизированным с нулевыми или минимальными усилиями. Благодаря чрезвычайно мощному выводу типов, реализованному в Groovy ++. Другими словами, то же самое: 80% кода Groovy можно ускорить в 4–10 раз с минимальными усилиями. Кстати, единственное необходимое усилие обычно заключается в добавлении типа к параметру метода купола или локальной переменной, остальное обычно происходит волшебным образом.

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

class LockPerf {
public static void main(String[] args) {
def processors = Runtime.runtime.availableProcessors()
for(def threadNum = 1; threadNum <= 1024; threadNum = threadNum < 2*processors ? threadNum+1 : threadNum*2) {
def counter = new AtomicInteger ()
def cdl = new CountDownLatch(threadNum)

def lock = new ReentrantLock()

def start = System.currentTimeMillis()
for(i in 0..<threadNum) {
Thread.start {
for(;;) {
lock.lock()
try {
if(counter.get() == 100000000) {
cdl.countDown()
break
}
else {
counter.incrementAndGet()
}
}
finally {
lock.unlock()
}
}
}
}

cdl.await()
println "$threadNum ${System.currentTimeMillis() - start}"
}
}
}

Что делает этот тест?

Почти ничего.

  • Он перебирает некоторую последовательность чисел (от одного до двойного числа ядер, а затем удваивает его до 1024)
  • Для каждого такого числа запускаются параллельные потоки
  • Каждый поток конкурирует за общую блокировку
  • Когда блокировка получена, она увеличивает общее атомное значение и снимает блокировку
  • Если общий счетчик достигает 100000000, поток останавливается
  • Когда все потоки остановились, итерация теста завершена и напечатано «$ threadNum $ iterationElapsedTimeInMillis»

Мы можем думать об этом тесте как об эмуляции логики синхронизации в сложном многопоточном алгоритме, и мы пытаемся оценить, сколько накладных расходов мы получаем, используя Groovy вместо Java / Scala / Groovy ++

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

public class LockPerf {
public static void main(String[] args) {
int processors = Runtime.getRuntime().availableProcessors();
for (int threadNum = 1; threadNum <= 1024; threadNum = threadNum < 2 * processors ? threadNum + 1 : threadNum * 2) {
final AtomicInteger counter = new AtomicInteger();
final CountDownLatch cdl = new CountDownLatch(threadNum);

final ReentrantLock lock = new ReentrantLock();

long start = System.currentTimeMillis();
for (int i = 0; i < threadNum; ++i) {
new Thread(new Runnable() {
public void run() {
for (;;) {
lock.lock();
try {
if (counter.get() == 100000000) {
cdl.countDown();
break;
} else {
counter.incrementAndGet();
}
} finally {
lock.unlock();
}
}

}
}).start();
}

try {
cdl.await();
} catch (InterruptedException e) {//
}
System.out.println(threadNum + " " + (System.currentTimeMillis() - start));
}
}
}

Вы, вероятно, хотите спросить меня, как насчет кода Groovy ++. Интересно (и я не обманываю вас), что единственное изменение, необходимое в оригинальном коде Grovy, было добавить аннотацию @Typed к классу

@Typed
class LockPerf {
// all the rest is exactly as above
}

Вот результаты тестов на 4-ядерном MacBook Pro

Groovy 1.7.7  Groovy 1.8.0-бета-4   Java 1.6  Groovy ++ 0.4.155
1 9221
2 19889
3 18256
4 18701
5 18036
6 18630
7 19837
8 19271
16 19166
32 19823
64 21144
128 22217
256 22848
512 23197
1024 25301     
1 8804
2 22306
3 19549
4 19376
5 20753
6 20078
7 20267
8 20511
16 21014
32 21496
64 22402
128 23754
256 24919
512 25399
1024 27179
1 2243
2 9513
3 3762
4 3808
5 3981
6 3863
7 3742
8 4112
16 3967
32 4064
64 3964
128 3753
256 4039
512 3658
1024 4017      
1 2791
2 5332
3 3696
4 3560
5 3785
6 3684
7 3528
8 3555
16 3616
32 3751
64 3807
128 3884
256 3942
512 3851
1024 3706

Что мы можем заметить в результатах тестов?

  • Groovy версии более чем в 6 раз медленнее, чем Java
  • Java и Groovy ++ очень сильно конкурируют (что правильно, поскольку теоретически оба должны работать одинаково)
  • Groovy 1.8 немного медленнее, чем 1.7 (что справедливо для еще не выпущенной версии)

Мой вывод очевиден и предсказуем — используйте Groovy и расширяйте его возможности с Groovy ++ там, где это необходимо. Это сделает ваш код сильнее и быстрее.

Надеюсь, вам понравилось и до следующего раза!

Ах, я обещал рассказать вам около 5000 долларов за час

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

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

Я сказал: «Вы знаете, что — я могу сесть с вами на несколько часов и дать нам посмотреть, что мы можем сделать. Если у нас будет отличный результат, вы хорошо заплатите, если не весело проведете время вместе, и вы заработаете мне хороший ужин». честно говоря я многого не ожидал. Мой план состоял в том, чтобы рассмотреть алгоритмы и посмотреть, что может быть оптимизировано или предназначено для переписывания на Java.

Проблемным кодом было более 3000 строк кода Groovy, разбитого на 12 классов. К счастью, он хорошо отделен от логики генерации отчетов и в достаточной степени покрыт тестами (на мой взгляд, единственный способ использовать любой код, особенно нестатически типизированный).

Стало совершенно ясно, что я не могу понять, что происходит за короткое время, поэтому я использовал простой трюк — попытался применить Groovy ++ (имеется в виду, что установил плагин Groovy ++ Grails и поместил @Typed тут и там).

Это удивительно!

У нас было ровно 10 ошибок компиляции:

  • 3 ошибки были исправлены путем добавления типа к параметру метода
  • 2 ошибки были исправлены путем установки правильного универсального типа в коллекцию
  • 1 о настройке правильного типа локальной переменной
  • 2 был настоящий динамический код, требуется смешанный режим компиляции
  • а остальные 2 были абсолютными ошибками в непокрытом коде, который статический компилятор помог нам найти

Эта итерация (за исключением исправления ошибок в непокрытом коде, который я оставил первоначальному автору) заняла менее 30 минут (конечно, это было бы невозможно без IntelliJ и парней, которые знали всю кодовую базу). В результате этой итерации мы уменьшили 7 секунд до 2,5 секунд.

Еще важнее то, что два фрагмента действительно динамического кода, которые мы обнаружили, были действительно странными. Два компонента связывались друг с другом, генерируя и сохраняя большой промежуточный XML-документ в базе данных. Поскольку никто не мог объяснить, почему это так, мы просто заменили все это поколение XML, сохраняя, загружая и анализируя, с прямым вызовом в памяти, и 2,5 секунды стали 1,2 секундами. Все это заняло еще 40 минут (в основном посвященных курению в философской дискуссии о программисты и идиоты).

Последние 0,3 секунды был обнаружен моим локальным другом, который заметил в одной ошибке компилятора, что часть вычислений, ненужных в BigDecimals вместо двойных, из-за использования нотации 1.0 вместо 1.0d. такие случаи. Кстати, я должен признать, что во время учений у нас был один сбой NPE компилятора Groovy ++, который, к счастью, было легко исправить.

Вот и все. Менее чем через полтора часа нам (мне и двум местным парням) удалось ускорить критическую часть приложения от неприемлемой до требуемой производительности, используя Groovy ++, IntelliJ IDEA и немного здравого смысла. Здесь важно то, что посещение Java не было вариантом из-за временных ограничений.

Я должен сказать вам, что это был один из лучших оплачиваемых полтора часа в моей жизни 🙂

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